File size: 14,916 Bytes
4104208
d1b1252
c06e820
4104208
d1b1252
 
 
 
 
 
 
 
4104208
c06e820
a1c55ad
d1b1252
 
 
 
 
 
 
 
 
c06e820
d1b1252
 
 
 
 
c06e820
 
d1b1252
c06e820
d1b1252
 
 
c06e820
a1c55ad
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c55ad
d1b1252
c06e820
d1b1252
c06e820
 
 
 
d1b1252
c06e820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522d52
d1b1252
 
c06e820
 
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c06e820
d1b1252
a1c55ad
d1b1252
 
 
 
 
 
 
 
 
 
 
1522d52
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522d52
 
d1b1252
 
 
 
 
 
 
 
 
c06e820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258745f
 
 
 
d1b1252
 
 
 
c06e820
d1b1252
c06e820
d1b1252
 
 
 
c06e820
 
 
 
 
 
d1b1252
 
 
 
 
 
 
 
 
 
 
258745f
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522d52
d1b1252
 
1522d52
d1b1252
 
 
 
 
 
 
 
 
1522d52
d1b1252
1522d52
d1b1252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258745f
 
d1b1252
 
258745f
d1b1252
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
import streamlit as st
import torch
from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM
import random
import time

# Configure page
st.set_page_config(
    page_title="Text-to-Quiz Generator",
    page_icon="🧠",
    layout="wide"
)

# Load the model with caching
@st.cache_resource
def load_model():
    try:
        # Check if PyTorch is available
        print(f"PyTorch version: {torch.__version__}")
        print(f"CUDA available: {torch.cuda.is_available()}")
        
        # Using a smaller, more efficient model that works well for question generation
        model_name = "valhalla/t5-small-e2e-qg"
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
        
        # Set device
        device = "cuda" if torch.cuda.is_available() else "cpu"
        print(f"Using device: {device}")
        
        # Move model to device
        model = model.to(device)
        
        return model, tokenizer, device
    except Exception as e:
        st.error(f"Error loading model: {str(e)}")
        print(f"Error details: {str(e)}")
        return None, None, None

# Custom CSS
def load_css():
    st.markdown("""
    <style>
        .main {
            padding: 2rem;
        }
        .question-box {
            background-color: #f0f7ff;
            padding: 1.5rem;
            border-radius: 10px;
            margin-bottom: 1rem;
            border-left: 5px solid #4361ee;
        }
        .stButton button {
            background-color: #4361ee;
            color: white;
            padding: 0.5rem 1rem;
            border-radius: 5px;
            border: none;
            font-weight: bold;
        }
        .title-box {
            padding: 1rem;
            border-radius: 5px;
            margin-bottom: 2rem;
            text-align: center;
            background: linear-gradient(90deg, #4361ee 0%, #3a0ca3 100%);
            color: white;
        }
        .score-box {
            font-size: 1.5rem;
            padding: 1rem;
            border-radius: 5px;
            text-align: center;
            font-weight: bold;
        }
        .feedback {
            padding: 1rem;
            border-radius: 5px;
            margin: 1rem 0;
        }
    </style>
    """, unsafe_allow_html=True)

# Function to generate questions from a passage
def generate_questions(model, tokenizer, device, text, num_questions=5):
    try:
        # Process text in chunks if it's too long
        max_length = 512
        chunks = []
        
        if len(text) > max_length:
            # Simple chunking based on sentences
            sentences = text.split('. ')
            current_chunk = ""
            
            for sentence in sentences:
                if len(current_chunk) + len(sentence) < max_length:
                    current_chunk += sentence + ". "
                else:
                    chunks.append(current_chunk)
                    current_chunk = sentence + ". "
            
            if current_chunk:
                chunks.append(current_chunk)
        else:
            chunks = [text]
        
        all_generated_texts = []
        
        # Process each chunk
        for chunk in chunks:
            inputs = tokenizer(chunk, return_tensors="pt", max_length=512, truncation=True)
            inputs = {k: v.to(device) for k, v in inputs.items()}
            
            # Generate with beam search for multiple diverse outputs
            with torch.no_grad():
                outputs = model.generate(
                    inputs["input_ids"],
                    max_length=64,
                    num_beams=5,
                    num_return_sequences=min(3, num_questions),  # Generate up to 3 questions per chunk
                    temperature=1.0,
                    diversity_penalty=1.0,
                    num_beam_groups=5,
                    early_stopping=True
                )
            
            decoded_outputs = tokenizer.batch_decode(outputs, skip_special_tokens=True)
            all_generated_texts.extend(decoded_outputs)
            
            # If we have enough questions, stop
            if len(all_generated_texts) >= num_questions:
                break
        
        # Ensure we don't return more than num_questions
        all_generated_texts = all_generated_texts[:num_questions]
        
        # Process and extract questions and answers
        questions_answers = []
        for generated_text in all_generated_texts:
            # Try to find question and answer
            if "?" in generated_text:
                parts = generated_text.split("?", 1)
                if len(parts) > 1:
                    question = parts[0].strip() + "?"
                    answer = parts[1].strip()
                    
                    # Clean up answer if it starts with common patterns
                    for prefix in ["answer:", "a:", " - "]:
                        if answer.lower().startswith(prefix):
                            answer = answer[len(prefix):].strip()
                    
                    if question and answer and len(question) > 10:
                        questions_answers.append({
                            "question": question,
                            "answer": answer
                        })
        
        return questions_answers
    except Exception as e:
        st.error(f"Error generating questions: {str(e)}")
        print(f"Detailed error: {str(e)}")
        return []

# Function to create quiz from generated Q&A pairs
def create_quiz(questions_answers, num_options=4):
    quiz_items = []
    
    # First filter out very short answers and duplicates
    filtered_qa = []
    seen_questions = set()
    
    for qa in questions_answers:
        q = qa["question"].strip()
        a = qa["answer"].strip()
        
        # Skip very short answers
        if len(a) < 2 or len(q) < 10:
            continue
            
        # Skip duplicate questions
        q_lower = q.lower()
        if q_lower in seen_questions:
            continue
            
        seen_questions.add(q_lower)
        filtered_qa.append({"question": q, "answer": a})
    
    # Use the filtered Q&A pairs
    all_answers = [qa["answer"] for qa in filtered_qa]
    
    for i, qa in enumerate(filtered_qa):
        correct_answer = qa["answer"]
        
        # Create distractors by selecting random answers from other questions
        other_answers = [a for a in all_answers if a != correct_answer]
        if other_answers:
            # Select random distractors
            num_distractors = min(num_options - 1, len(other_answers))
            distractors = random.sample(other_answers, num_distractors)
            
            # Combine correct answer and distractors
            options = [correct_answer] + distractors
            random.shuffle(options)
            
            quiz_items.append({
                "id": i,
                "question": qa["question"],
                "correct_answer": correct_answer,
                "options": options
            })
    
    return quiz_items

# Alternative question generation using simpler approach
def generate_questions_simple(text, num_questions=5):
    try:
        # Simple question generation for demonstration
        # In a real app, you'd use a proper NLP model
        
        # Extract sentences
        sentences = text.split('.')
        sentences = [s.strip() for s in sentences if len(s.strip()) > 20]
        
        # Select random sentences to turn into questions
        if len(sentences) < num_questions:
            selected_sentences = sentences
        else:
            selected_sentences = random.sample(sentences, num_questions)
        
        questions_answers = []
        
        # Simple transformation of sentences into questions
        for sentence in selected_sentences:
            # Very simple question generation (not ideal but works as fallback)
            words = sentence.split()
            if len(words) < 5:
                continue
                
            # Extract key entities for answer
            potential_answer = " ".join(words[-3:])
            
            # Create question from beginning of sentence
            question_words = words[:len(words)-3]
            question = " ".join(question_words) + "?"
            
            questions_answers.append({
                "question": question,
                "answer": potential_answer
            })
            
        return questions_answers
    except Exception as e:
        print(f"Error in simple question generation: {str(e)}")
        return []

# Main app
def main():
    load_css()
    
    # App title
    st.markdown('<div class="title-box"><h1>🧠 Text-to-Quiz Generator</h1></div>', unsafe_allow_html=True)
    
    col1, col2 = st.columns([2, 1])
    
    with col1:
        st.markdown("### Enter a passage to generate quiz questions")
        passage = st.text_area(
            "Paste your text here:", 
            height=200,
            placeholder="Enter a paragraph or article here to generate quiz questions..."
        )
    
    with col2:
        st.markdown("### Settings")
        num_questions = st.slider("Number of questions to generate", 3, 10, 5)
        st.markdown("---")
        st.markdown("""
        **Tips for best results:**
        - Use clear, factual content
        - Include specific details
        - Text length: 100-500 words works best
        - Educational content works better than narrative
        """)
    
    # Generate Quiz button with automatic rerun logic
    if "quiz_generated" not in st.session_state:
        st.session_state.quiz_generated = False
    
    if st.button("🧠 Generate Quiz"):
        if passage and len(passage) > 50:
            # Loading the model (with the cached resource)
            with st.spinner("Loading AI model..."):
                model, tokenizer, device = load_model()
            
            if model and tokenizer and device:
                # Generate questions
                with st.spinner("Generating questions..."):
                    # Add a small delay for UX
                    time.sleep(1)
                    questions_answers = generate_questions(model, tokenizer, device, passage, num_questions)
                    
                    # If primary method fails, try fallback approach
                    if not questions_answers:
                        st.warning("Advanced question generation failed. Using simple approach instead.")
                        questions_answers = generate_questions_simple(passage, num_questions)
                
                if questions_answers:
                    # Create quiz
                    quiz_items = create_quiz(questions_answers)
                    
                    if quiz_items:
                        # Store in session state
                        st.session_state.quiz_items = quiz_items
                        st.session_state.user_answers = {}
                        st.session_state.quiz_submitted = False
                        st.session_state.show_explanations = False
                        st.session_state.quiz_generated = True
                    else:
                        st.error("Couldn't create valid quiz questions. Please try a different text or add more content.")
                else:
                    st.error("Failed to generate questions. Please try a different passage.")
            else:
                st.error("Failed to load the question generation model. Please try again.")
        else:
            st.warning("Please enter a longer passage (at least 50 characters).")
    
    # Display quiz if available in session state
    if "quiz_items" in st.session_state and st.session_state.quiz_items:
        st.markdown("---")
        st.markdown("## Your Quiz")
        
        quiz_items = st.session_state.quiz_items
        
        # Create a form for the quiz
        with st.form("quiz_form"):
            for i, item in enumerate(quiz_items):
                st.markdown(f'<div class="question-box"><h3>Question {i+1}</h3><p>{item["question"]}</p></div>', unsafe_allow_html=True)
                
                key = f"question_{item['id']}"
                st.session_state.user_answers[key] = st.radio(
                    "Select your answer:",
                    options=item["options"],
                    key=key
                )
            
            submit_button = st.form_submit_button("Submit Answers")
            
            if submit_button:
                st.session_state.quiz_submitted = True
        
        # Show results if quiz was submitted
        if st.session_state.quiz_submitted:
            score = 0
            
            st.markdown("## Quiz Results")
            
            for i, item in enumerate(quiz_items):
                key = f"question_{item['id']}"
                user_answer = st.session_state.user_answers[key]
                correct = user_answer == item["correct_answer"]
                
                if correct:
                    score += 1
                    st.markdown(f'<div class="feedback" style="background-color: #d4edda; border-left: 5px solid #28a745;"><h4>Question {i+1}: Correct! βœ…</h4><p><strong>Your answer:</strong> {user_answer}</p></div>', unsafe_allow_html=True)
                else:
                    st.markdown(f'<div class="feedback" style="background-color: #f8d7da; border-left: 5px solid #dc3545;"><h4>Question {i+1}: Incorrect ❌</h4><p><strong>Your answer:</strong> {user_answer}<br><strong>Correct answer:</strong> {item["correct_answer"]}</p></div>', unsafe_allow_html=True)
            
            # Show score
            percentage = (score / len(quiz_items)) * 100
            
            if percentage >= 80:
                color = "#28a745"  # Green
                message = "Excellent! πŸ†"
            elif percentage >= 60:
                color = "#17a2b8"  # Blue
                message = "Good job! πŸ‘"
            else:
                color = "#ffc107"  # Yellow
                message = "Keep practicing! πŸ“š"
                
            st.markdown(f'<div class="score-box" style="background-color: {color}15; border-left: 5px solid {color};">{message}<br>Your Score: {score}/{len(quiz_items)} ({percentage:.1f}%)</div>', unsafe_allow_html=True)
            
            # Restart button
            if st.button("Generate Another Quiz"):
                # Clear session state
                for key in ["quiz_items", "user_answers", "quiz_submitted", "show_explanations", "quiz_generated"]:
                    if key in st.session_state:
                        del st.session_state[key]
                # No need for rerun as page will refresh naturally with next event

if __name__ == "__main__":
    main()