File size: 21,113 Bytes
300f197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b743b1
 
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
from flask import Flask, render_template, request, jsonify, send_file, session, redirect, url_for, flash
import os
import json
import tempfile
from werkzeug.utils import secure_filename
from exam_question_system import ExamQuestionSystem
from datetime import datetime
import uuid
import threading
import time
from docx import Document
import PyPDF2
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['TEMP_INPUT_FOLDER'] = os.path.join(tempfile.gettempdir(), 'eqg_inputs')
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0  # Disable caching for development
app.secret_key = 'your-secret-key-change-this-in-production'

# Create necessary directories if they don't exist
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['TEMP_INPUT_FOLDER'], exist_ok=True)

# Global variables for question system
question_system = None
system_loading = False
system_load_error = None

def read_file_content(filepath):
    """Read content from a file based on its extension."""
    ext = os.path.splitext(filepath)[1].lower()
    
    try:
        if ext == '.pdf':
            text = ""
            with open(filepath, 'rb') as f:
                reader = PyPDF2.PdfReader(f)
                for page in reader.pages:
                    text += page.extract_text() + "\n"
            return text
            
        elif ext == '.docx':
            doc = Document(filepath)
            return "\n".join([paragraph.text for paragraph in doc.paragraphs])
            
        else:
            # Default to text file
            with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
                return f.read()
                
    except Exception as e:
        raise Exception(f"Error reading {ext} file: {str(e)}")

def get_question_system():
    """Get or initialize the question generation system."""
    global question_system, system_loading, system_load_error
    
    if question_system is None and not system_loading:
        if system_load_error:
            raise Exception(f"System failed to load: {system_load_error}")
        
        system_loading = True
        try:
            print("Initializing question generation system...")
            question_system = ExamQuestionSystem()
            print("Question generation system loaded successfully!")
        except Exception as e:
            system_load_error = str(e)
            system_loading = False
            raise e
        finally:
            system_loading = False
    
    if system_loading:
        raise Exception("System is still loading, please wait...")
    
    return question_system

# Utility: parse human-readable duration text into minutes when possible
def parse_duration_to_minutes(duration_text):
    try:
        if not duration_text:
            return None
        text = duration_text.strip().lower()
        # Normalize
        text = text.replace('hrs', 'h').replace('hr', 'h').replace('hours', 'h').replace('hour', 'h')
        text = text.replace('minutes', 'm').replace('minute', 'm').replace('mins', 'm').replace('min', 'm')
        # Patterns like '2h 30m'
        import re
        hours = 0
        minutes = 0
        # Match hours
        h_match = re.search(r"(\d+)\s*h", text)
        if h_match:
            hours = int(h_match.group(1))
        # Match minutes
        m_match = re.search(r"(\d+)\s*m", text)
        if m_match:
            minutes = int(m_match.group(1))
        if h_match or m_match:
            return hours * 60 + minutes
        # If only a number, treat as minutes
        just_num = re.fullmatch(r"\s*(\d+)\s*", duration_text)
        if just_num:
            return int(just_num.group(1))
        # If something like '3 hour' without m, captured above; if unparseable, return None
        return None
    except Exception:
        return None

@app.route('/')
def index():
    """Welcome page before the step flow."""
    # Clear any existing session data
    session.clear()
    return render_template('welcome.html', project_name='AutoExamGen')

@app.route('/step1', methods=['GET'])
def step1_input():
    """Step 1: Syllabus input page."""
    return render_template('step1_input.html')

@app.route('/step2', methods=['GET', 'POST'])
def step2_configuration():
    """Step 2: Question configuration page."""
    if request.method == 'GET':
        # If user tries to access /step2 directly, redirect to step1
        return redirect(url_for('step1_input'))
    
    # Handle POST request (form submission from step1)
    content = None
    
    try:
        # Get text input (from textarea)
        text_input = request.form.get('text_input', '').strip()
        
        # Check if file was uploaded
        if 'file' in request.files:
            file = request.files['file']
            if file and file.filename != '':
                try:
                    # Save the uploaded file
                    filename = secure_filename(file.filename)
                    if not os.path.exists(app.config['UPLOAD_FOLDER']):
                        os.makedirs(app.config['UPLOAD_FOLDER'])
                    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                    file.save(filepath)
                    
                    # Store file path in session
                    session['content_file'] = filepath
                    session.pop('content_text', None)  # Clear any text content if it exists
                    
                    # Read the file content for processing
                    content = read_file_content(filepath)
                    print(f"File uploaded successfully: {filename}, Content length: {len(content)}")
                    
                except Exception as e:
                    error_msg = f'Error processing file: {str(e)}'
                    print(error_msg)
                    flash(error_msg, 'error')
                    return redirect(url_for('step1_input'))
        
        # If no file but text content is provided
        if not content and text_input:
            # For small text content, store directly in session
            if len(text_input) < 2000:
                session['content_text'] = text_input
                content = text_input
            else:
                # For large content, save to a temporary file
                temp_file = os.path.join(app.config['UPLOAD_FOLDER'], f'temp_{int(time.time())}.txt')
                with open(temp_file, 'w', encoding='utf-8') as f:
                    f.write(text_input)
                session['content_file'] = temp_file
                content = text_input
            print(f"Text input processed, Content length: {len(content)}")
        
        # Validate that we have content
        if not content or not content.strip():
            flash('Please provide either a file or paste content', 'error')
            return redirect(url_for('step1_input'))
        
        # Process the content for question generation
        try:
            # Initialize question system if not already done
            print("Initializing question system...")
            question_system = get_question_system()
            print("Question system initialized successfully")
            
            # Store word count for progress display
            word_count = len(content.split())
            session['word_count'] = word_count
            print(f"Content processed: {word_count} words")
            
            # Render the configuration page
            return render_template('step2_config.html', 
                                 word_count=word_count,
                                 has_syllabus=bool(session.get('syllabus_text', '')))
            
        except Exception as e:
            error_msg = f'Error initializing question system: {str(e)}'
            print(error_msg)
            import traceback
            traceback.print_exc()
            flash(error_msg, 'error')
            return redirect(url_for('step1_input'))
            
    except Exception as e:
        error_msg = f'An error occurred: {str(e)}'
        print(error_msg)
        import traceback
        traceback.print_exc()
        flash(error_msg, 'error')
        return redirect(url_for('step1_input'))

@app.route('/generate', methods=['POST'])
def step3_generate():
    """Step 3: Generate and display question paper."""
    try:
        # Get form data
        num_questions = int(request.form.get('num_questions', 5))
        question_types = request.form.getlist('question_types')
        
        # Get content from session or uploaded file
        content = None
        
        # Check for uploaded file first
        if 'content_file' in session and os.path.exists(session['content_file']):
            try:
                content = read_file_content(session['content_file'])
            except Exception as e:
                flash(f'Error reading uploaded file: {str(e)}', 'error')
                return redirect(url_for('step1_input'))
        # Check for direct text content
        elif 'content_text' in session and session['content_text']:
            content = session['content_text']
        
        # If no content found, redirect to step 1
        if not content:
            flash('No content found. Please provide content first.', 'error')
            return redirect(url_for('step1_input'))
            
        # Initialize question system
        try:
            question_system = get_question_system()
            
            # Helper function to safely get integer values from form
            def get_int(form, key, default=0):
                try:
                    return int(form.get(key, default))
                except (ValueError, TypeError):
                    return default
            
            # Store configuration in session with all required fields and safe defaults
            config = {
                'exam_name': request.form.get('exam_name', 'Sample Exam'),
                'subject_name': request.form.get('subject_name', 'Subject'),
                'duration': get_int(request.form, 'duration', 60),
                'short_questions': get_int(request.form, 'short_questions', 2),
                'short_marks': get_int(request.form, 'short_marks', 2),
                'long_questions': get_int(request.form, 'long_questions', 1),
                'long_marks': get_int(request.form, 'long_marks', 5),
                'long_attempt': get_int(request.form, 'long_attempt', 1),
                'mcq_questions': get_int(request.form, 'mcq_questions', 2),
                'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            }
            session['exam_config'] = config
            
            # Get the syllabus/content text
            content = ""
            if 'content_text' in session:
                content = session['content_text']
            elif 'content_file' in session and os.path.exists(session['content_file']):
                content = read_file_content(session['content_file'])
            
            if not content.strip():
                raise ValueError("No content available for question generation.")
            
            # Generate questions based on content and configuration
            print("Generating questions from content...")
            
            # Get number of questions for each type
            num_mcq = config['mcq_questions']
            num_short = config['short_questions']
            num_long = config['long_questions']
            
            # Generate questions using the question system
            # We request enough questions for all sections, but without auto-MCQ generation
            # so we can handle categorization manually
            total_questions_needed = num_mcq + num_short + num_long
            
            results = question_system.generate_exam_questions(
                input_text=content,
                max_questions=total_questions_needed,
                include_mcq=False,  # We'll generate options manually for specific questions
                syllabus_text=content
            )
            
            all_questions = results.get('questions', [])
            
            # Initialize categories
            generated_questions = {
                'mcq_questions': [],
                'short_questions': [],
                'long_questions': []
            }
            
            # Distribute questions
            # We prioritize Short and Long questions first to ensure they get content, 
            # as MCQs are easier to fallback/generate
            
            # Filter out questions that are too simple for Long answers
            long_candidates = [q for q in all_questions if len(q.get('context', '').split()) > 10]
            short_candidates = [q for q in all_questions if q not in long_candidates]
            
            # If we don't have enough long candidates, take from short
            if len(long_candidates) < num_long:
                needed = num_long - len(long_candidates)
                long_candidates.extend(short_candidates[:needed])
                short_candidates = short_candidates[needed:]
            
            # 3. Process Long Questions (Prioritize these)
            for _ in range(num_long):
                if long_candidates:
                    q = long_candidates.pop(0)
                    q['type'] = 'long_answer'
                    generated_questions['long_questions'].append(q)
                    # Remove from all_questions so we don't reuse
                    if q in all_questions:
                        all_questions.remove(q)
            
            # 2. Process Short Questions
            for _ in range(num_short):
                if short_candidates:
                    q = short_candidates.pop(0)
                    q['type'] = 'short_answer'
                    generated_questions['short_questions'].append(q)
                    if q in all_questions:
                        all_questions.remove(q)
                elif all_questions: # Fallback to any remaining
                    q = all_questions.pop(0)
                    q['type'] = 'short_answer'
                    generated_questions['short_questions'].append(q)

            # 1. Process MCQs (Use remaining questions)
            # Extract global keywords for distractors
            global_keywords = [k[1] for k in results.get('keywords', [])]
            
            for _ in range(num_mcq):
                if all_questions:
                    q = all_questions.pop(0)
                    # Generate options for this question
                    try:
                        mcq_data = question_system.option_generator.create_mcq_options(
                            q['question'],
                            q['context'],
                            correct_answer=q.get('correct_answer'),
                            global_keywords=global_keywords
                        )
                        if mcq_data and 'options' in mcq_data:
                            q.update(mcq_data)
                            q['type'] = 'mcq'
                            generated_questions['mcq_questions'].append(q)
                        else:
                            # Fallback if option generation fails
                            q['type'] = 'short_answer'
                            generated_questions['short_questions'].append(q)
                    except Exception as e:
                        print(f"Error generating options: {e}")
                        q['type'] = 'short_answer'
                        generated_questions['short_questions'].append(q)
            
            # Store the generated questions
            session['generated_questions'] = generated_questions
            
            # Calculate and store total marks
            total_marks = (
                (len(session['generated_questions']['short_questions']) * config['short_marks']) +
                (len(session['generated_questions']['long_questions']) * config['long_marks']) +
                len(session['generated_questions']['mcq_questions'])  # 1 mark per MCQ
            )
            session['total_marks'] = total_marks
            
            # Redirect to results page
            return redirect(url_for('show_results'))
            
        except Exception as e:
            error_msg = f'Error generating questions: {str(e)}'
            print(error_msg)
            flash(error_msg, 'error')
            return redirect(url_for('step1_input'))
            
    except Exception as e:
        error_msg = f'An error occurred: {str(e)}'
        print(error_msg)
        flash(error_msg, 'error')
        return redirect(url_for('step1_input'))

@app.route('/download')
def download_paper():
    """Download the generated question paper as HTML."""
    try:
        if 'question_paper' not in session:
            return redirect(url_for('index'))
        
        # Generate a unique filename
        filename = f"question_paper_{uuid.uuid4().hex[:8]}.html"
        
        # Render the printable version
        # Determine display duration similarly to step3
        cfg = session['config']
        display_duration = cfg.get('exam_duration') if cfg.get('exam_duration') else int(round(session['total_marks'] * 1.5))

        html_content = render_template('printable_paper.html',
                                     question_paper=session['question_paper'],
                                     config=cfg,
                                     total_marks=session['total_marks'],
                                     exam_date=session['exam_date'],
                                     display_duration=display_duration)
        
        # Create a temporary file
        temp_file = os.path.join(tempfile.gettempdir(), filename)
        with open(temp_file, 'w', encoding='utf-8') as f:
            f.write(html_content)
        
        return send_file(temp_file, as_attachment=True, download_name=filename)
        
    except Exception as e:
        return jsonify({'error': f'Error downloading paper: {str(e)}'}), 500

@app.route('/results')
def show_results():
    """Display the generated questions."""
    if 'generated_questions' not in session or 'exam_config' not in session:
        flash('No questions generated yet. Please start from the beginning.', 'error')
        return redirect(url_for('step1_input'))
    
    # Get config with defaults
    config = session.get('exam_config', {})
    questions = session.get('generated_questions', {})
    
    # Ensure all required question types exist in the questions dictionary
    for qtype in ['mcq_questions', 'short_questions', 'long_questions']:
        if qtype not in questions:
            questions[qtype] = []
    
    # Calculate total marks
    total_marks = 0
    if 'mcq_questions' in questions:
        total_marks += len(questions['mcq_questions'])
    if 'short_questions' in questions:
        total_marks += len(questions['short_questions']) * 2  # 2 marks per short question
    if 'long_questions' in questions:
        total_marks += len(questions['long_questions']) * 5  # 5 marks per long question
    
    # Get exam date from config or use current date
    exam_date = config.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    
    return render_template('step3_result.html',
                         question_paper=questions,  # Changed from questions to question_paper
                         config=config,
                         total_marks=total_marks,
                         exam_date=datetime.strptime(exam_date, '%Y-%m-%d %H:%M:%S').strftime('%B %d, %Y'),
                         display_duration=config.get('duration', 60))  # Use configured duration or default to 60 minutes

@app.route('/api/health')
def health_check():
    """Health check endpoint."""
    return jsonify({'status': 'healthy', 'service': 'Exam Question Generator'})

@app.route('/api/warmup')
def warmup():
    """Warmup endpoint to initialize the system."""
    try:
        system = get_question_system()
        return jsonify({
            'status': 'ready', 
            'message': 'Question generation system is ready',
            'uses_transformers': system.question_generator.use_transformers
        })
    except Exception as e:
        return jsonify({
            'status': 'loading' if 'still loading' in str(e) else 'error',
            'message': str(e)
        }), 202 if 'still loading' in str(e) else 500

if __name__ == '__main__':
    print("🌐 Starting Flask Web Application...")
    print("πŸ“ Exam Question Generator Web Interface")
    print("πŸ”— Access the application at: http://localhost:5000")
    print("πŸ’‘ Using rule-based question generation for faster startup")
    print("⚑ System will initialize on first use")
    # Configure Flask for better timeout handling
    app.config['PERMANENT_SESSION_LIFETIME'] = 1800  # 30 minutes

    port = int(os.environ.get("PORT", 7860))  # HF uses 7860
    app.run(host="0.0.0.0", port=port, debug=False)