Turbiling commited on
Commit
2d506b2
Β·
verified Β·
1 Parent(s): a0ab2cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +976 -279
app.py CHANGED
@@ -1,4 +1,5 @@
1
- # app.py - Enhanced MCQ Test Generator for Hugging Face
 
2
  # Aligned with Pakistani National Curriculum (2006)
3
  # Developer: Najaf Ali Sharqi
4
 
@@ -23,10 +24,50 @@ client = Groq(api_key=GROQ_API_KEY)
23
  # ---------------------------
24
  # Configuration Constants
25
  # ---------------------------
26
- APP_VERSION = "2.0"
27
- MAX_MCQS = 20
28
  MODEL_NAME = "llama-3.3-70b-versatile"
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # ---------------------------
31
  # Grades and subjects aligned with Pakistan National Curriculum
32
  # ---------------------------
@@ -345,296 +386,829 @@ DIFFICULTY_LEVELS = {
345
  }
346
 
347
  # ---------------------------
348
- # Enhanced MCQ generation with proper item writing principles
349
  # ---------------------------
350
- def generate_mcqs(grade, subject, chapter, difficulty, num_questions, test_type, school_name):
351
- """Generate MCQs using AI with enhanced item writing guidelines"""
352
 
353
- try:
354
- # Build comprehensive prompt for better MCQ quality
355
- system_msg = """You are an expert educational item writer and assessment specialist with deep knowledge of the Pakistani National Curriculum (2006).
356
-
357
- Your task is to create high-quality Multiple Choice Questions (MCQs) following these professional item writing principles:
358
-
359
- 1. STEM CLARITY: Write clear, concise question stems that present a single, well-defined problem
360
- 2. PLAUSIBLE DISTRACTORS: Create wrong options that are believable based on common misconceptions
361
- 3. GRAMMATICAL CONSISTENCY: Ensure all options are grammatically consistent with the stem
362
- 4. AVOID CUEING: Don't use "all of the above" or "none of the above"
363
- 5. EQUAL LENGTH: Keep options similar in length to avoid cueing the correct answer
364
- 6. NO NEGATIVE PHRASING: Avoid "NOT" or "EXCEPT" unless absolutely necessary
365
- 7. CURRICULUM ALIGNMENT: Base questions strictly on learning objectives from the National Curriculum
366
- 8. AGE-APPROPRIATE: Use vocabulary and concepts suitable for the grade level
367
-
368
- Format each question exactly as:
369
- Q#. [Question stem]
370
  A) [Option 1]
371
  B) [Option 2]
372
  C) [Option 3]
373
  D) [Option 4]
374
 
375
- Then provide answer key at the end as:
 
 
 
 
 
 
 
 
376
  ANSWER KEY:
377
  1. B
378
  2. A
379
  etc."""
380
 
381
- user_msg = f"""Generate {num_questions} high-quality MCQs for:
382
- - Grade: {grade}
383
- - Subject: {subject}
384
- - Chapter: {chapter}
385
- - Cognitive Level: {difficulty}
386
- - Test Type: {test_type}
387
 
388
  Requirements:
389
- - Align strictly with Pakistani National Curriculum (2006) learning outcomes
390
- - Use {difficulty} level cognitive skills: {DIFFICULTY_LEVELS.get(difficulty, {}).get('description', '')}
391
- - Ensure questions test {', '.join(DIFFICULTY_LEVELS.get(difficulty, {}).get('keywords', [])[:3])}
392
- - Make distractors plausible based on common student errors
393
- - Use clear, unambiguous language appropriate for {grade}
394
- - Balance questions across different topics within the chapter
395
- - Include practical applications where relevant
396
-
397
- Generate exactly {num_questions} questions with the answer key at the end."""
398
-
399
- # Call Groq API
400
- response = client.chat.completions.create(
401
- messages=[
402
- {"role": "system", "content": system_msg},
403
- {"role": "user", "content": user_msg}
404
- ],
405
- model=MODEL_NAME,
406
- temperature=0.7,
407
- max_tokens=4000
408
- )
409
 
410
- text = response.choices[0].message.content
411
-
412
- # Parse MCQs and answers
413
- mcqs, answer_key = parse_mcqs_and_answers(text, num_questions)
414
-
415
- return mcqs, answer_key, None
416
-
417
- except Exception as e:
418
- error_msg = f"Error generating MCQs: {str(e)}"
419
- return [], [], error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  def parse_mcqs_and_answers(text, expected_count):
422
- """Parse AI response to extract MCQs and answer key"""
423
 
424
- # Split by answer key section
 
 
 
 
 
425
  parts = re.split(r'ANSWER\s+KEY:?', text, flags=re.IGNORECASE)
426
 
427
- mcqs = []
428
- answer_key = []
429
 
430
  if len(parts) >= 1:
431
- # Extract MCQs
432
  mcq_text = parts[0]
433
- # Split by question numbers (1., 2., etc.)
434
- questions = re.split(r'\n\s*\d+\.\s+', mcq_text)
435
- questions = [q.strip() for q in questions if q.strip() and len(q.strip()) > 20]
436
- mcqs = questions[:expected_count]
437
 
438
  if len(parts) >= 2:
439
- # Extract answer key
440
  answer_text = parts[1]
441
- # Find patterns like "1. B" or "1) B" or "1: B"
442
- answers = re.findall(r'\d+[\.\):\s]+([A-D])', answer_text, re.IGNORECASE)
443
- answer_key = [ans.upper() for ans in answers[:expected_count]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
- # If parsing failed, create placeholder
446
- if len(answer_key) < len(mcqs):
447
- answer_key.extend(['A'] * (len(mcqs) - len(answer_key)))
448
 
449
- return mcqs, answer_key
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
  # ---------------------------
452
- # Enhanced DOCX export with professional formatting
453
  # ---------------------------
454
- def export_to_word(mcqs, answers, grade, subject, chapter, difficulty, test_type, school_name, total_marks):
455
- """Export MCQs to professionally formatted Word document"""
456
 
457
  try:
458
- doc = Document()
459
-
460
- # Set margins
461
- sections = doc.sections
462
- for section in sections:
463
- section.top_margin = Inches(0.8)
464
- section.bottom_margin = Inches(0.8)
465
- section.left_margin = Inches(0.9)
466
- section.right_margin = Inches(0.9)
467
-
468
- # Header with school name if provided
469
- if school_name and school_name.strip():
470
- school_heading = doc.add_heading(school_name.upper(), level=0)
471
- school_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
472
- for run in school_heading.runs:
473
- run.font.size = Pt(16)
474
- run.font.color.rgb = RGBColor(0, 51, 102)
475
-
476
- # Test title
477
- test_title = doc.add_heading(f"{test_type.upper()}", level=1)
478
- test_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
479
- for run in test_title.runs:
480
- run.bold = True
481
- run.font.color.rgb = RGBColor(0, 102, 51)
482
- run.font.size = Pt(16)
483
-
484
- # Information table
485
- table = doc.add_table(rows=7, cols=2)
486
- table.style = 'Light Grid Accent 1'
487
-
488
- info_data = [
489
- ("Grade:", grade),
490
- ("Subject:", subject),
491
- ("Chapter:", chapter),
492
- ("Cognitive Level:", difficulty),
493
- ("Total Marks:", str(total_marks)),
494
- ("Date:", datetime.now().strftime("%d-%m-%Y")),
495
- ("Duration:", "60 minutes" if len(mcqs) > 15 else "40 minutes")
496
- ]
497
-
498
- for idx, (label, value) in enumerate(info_data):
499
- row = table.rows[idx]
500
- row.cells[0].text = label
501
- row.cells[1].text = value
502
- row.cells[0].paragraphs[0].runs[0].bold = True
503
- row.cells[0].paragraphs[0].runs[0].font.size = Pt(10)
504
- row.cells[1].paragraphs[0].runs[0].font.size = Pt(10)
505
 
506
- # Instructions
507
- doc.add_paragraph()
508
- instructions = doc.add_paragraph()
509
- instructions.add_run("INSTRUCTIONS:").bold = True
510
- doc.add_paragraph("β€’ Read each question carefully before selecting your answer")
511
- doc.add_paragraph("β€’ Each question carries equal marks")
512
- doc.add_paragraph("β€’ Select the MOST appropriate answer")
513
- doc.add_paragraph("β€’ Use the bubble sheet provided for marking answers")
514
-
515
- # Questions section
516
- doc.add_paragraph()
517
- questions_heading = doc.add_paragraph()
518
- questions_heading.add_run("SECTION A: MULTIPLE CHOICE QUESTIONS").bold = True
519
- questions_heading.runs[0].font.size = Pt(12)
520
- doc.add_paragraph()
521
 
522
- # Add MCQs
523
- for i, q in enumerate(mcqs, start=1):
524
- # Question number and stem
525
- q_para = doc.add_paragraph()
526
- q_para.add_run(f"Q{i}. ").bold = True
527
- q_para.add_run(q)
528
- q_para.space_after = Pt(6)
529
-
530
- doc.add_paragraph()
531
 
532
- # Answer key (on separate page)
533
- doc.add_page_break()
 
 
534
 
535
- ans_heading = doc.add_heading("ANSWER KEY", level=2)
536
- ans_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
537
- for run in ans_heading.runs:
538
- run.font.color.rgb = RGBColor(153, 0, 0)
539
 
540
- doc.add_paragraph("(For Teacher's Use Only)", style='Intense Quote')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
- # Answer table with better formatting
543
- num_rows = len(answers) + 1
544
- ans_table = doc.add_table(rows=num_rows, cols=5)
545
- ans_table.style = 'Medium Grid 1 Accent 1'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
- # Headers
548
- headers = ["Q#", "A", "B", "C", "D"]
549
- for idx, header in enumerate(headers):
550
- cell = ans_table.cell(0, idx)
551
- cell.text = header
552
- cell.paragraphs[0].runs[0].bold = True
 
 
 
553
  cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
554
-
555
- # Fill answers
556
- for r, ans in enumerate(answers, start=1):
557
- ans_table.cell(r, 0).text = str(r)
558
- ans_table.cell(r, 0).paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
559
-
560
- for c, option in enumerate(["A", "B", "C", "D"], start=1):
561
- cell = ans_table.cell(r, c)
562
- if ans == option:
563
- cell.text = "βœ“"
564
- cell.paragraphs[0].runs[0].bold = True
565
- cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(0, 128, 0)
566
- cell.paragraphs[0].runs[0].font.size = Pt(14)
567
- else:
568
- cell.text = ""
569
- cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
570
-
571
- # Footer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  doc.add_paragraph()
573
- footer = doc.add_paragraph()
574
- footer.add_run(f"Generated by MCQ Test Generator v{APP_VERSION} | Developer: Najaf Ali Sharqi | {datetime.now().strftime('%d-%m-%Y %H:%M')}").italic = True
575
- footer.runs[0].font.size = Pt(8)
576
- footer.runs[0].font.color.rgb = RGBColor(128, 128, 128)
577
- footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
578
-
579
- # Save file
580
- safe_chapter = re.sub(r'[\\/*?:"<>|]', "_", chapter)
581
- safe_school = re.sub(r'[\\/*?:"<>|]', "_", school_name) if school_name else "School"
582
- filename = f"{safe_school}_{grade}_{subject}_{safe_chapter}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
583
-
584
- doc.save(filename)
585
- return filename, None
586
-
587
- except Exception as e:
588
- return None, f"Error creating document: {str(e)}"
 
 
 
 
 
589
 
590
  # ---------------------------
591
- # Main generation handler
592
  # ---------------------------
593
- def on_generate(grade, subject, chapter, difficulty, num_questions, test_type, school_name):
594
- """Handle MCQ generation request"""
595
 
596
  # Validation
597
- if not all([grade, subject, chapter, difficulty]):
598
  return "⚠️ Please fill all required fields", None, None
599
 
600
  if not school_name or not school_name.strip():
601
  school_name = "Educational Institution"
602
 
603
  try:
604
- # Show progress
605
- status_msg = f"πŸ”„ Generating {num_questions} MCQs for {grade} {subject}...\n"
606
  status_msg += f"πŸ“– Chapter: {chapter}\n"
607
  status_msg += f"🎯 Cognitive Level: {difficulty}\n"
608
  status_msg += "⏳ Please wait..."
609
 
610
- # Generate MCQs
611
- mcqs, answers, error = generate_mcqs(
612
- grade, subject, chapter, difficulty,
613
- num_questions, test_type, school_name
614
  )
615
 
616
  if error:
617
  return f"❌ Error: {error}", None, None
618
 
619
- if not mcqs or len(mcqs) == 0:
620
- return "❌ No MCQs generated. Please try again.", None, None
621
-
622
  # Calculate marks
623
- total_marks = len(mcqs)
624
-
625
- # Export to Word
626
- file_path, doc_error = export_to_word(
627
- mcqs, answers, grade, subject, chapter,
628
- difficulty, test_type, school_name, total_marks
629
- )
630
 
631
- if doc_error:
632
- return f"❌ {doc_error}", None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
 
634
  # Success message
635
  success_msg = f"βœ… Test generated successfully!\n\n"
636
  success_msg += f"πŸ“Š Statistics:\n"
637
- success_msg += f"β€’ Total Questions: {len(mcqs)}\n"
 
 
638
  success_msg += f"β€’ Total Marks: {total_marks}\n"
639
  success_msg += f"β€’ Grade Level: {grade}\n"
640
  success_msg += f"β€’ Subject: {subject}\n"
@@ -643,15 +1217,18 @@ def on_generate(grade, subject, chapter, difficulty, num_questions, test_type, s
643
  success_msg += f"πŸ“₯ Download your test document below"
644
 
645
  # Preview
646
- preview = "\n\nπŸ“ PREVIEW (First 3 Questions):\n" + "="*50 + "\n\n"
647
- for i, q in enumerate(mcqs[:3], 1):
648
- preview += f"Q{i}. {q}\n\n"
 
 
649
 
650
- return success_msg + preview, file_path, f"πŸŽ‰ File ready: {file_path}"
651
 
652
  except Exception as e:
653
  return f"❌ Unexpected error: {str(e)}", None, None
654
 
 
655
  # ---------------------------
656
  # Enhanced Gradio UI
657
  # ---------------------------
@@ -668,22 +1245,24 @@ def create_ui():
668
  .description {text-align: center; color: #666; margin-bottom: 30px;}
669
  .footer {text-align: center; margin-top: 30px; padding: 20px; color: #888;}
670
  .warning {background-color: #fff3cd; padding: 10px; border-radius: 5px; margin: 10px 0;}
 
671
  """
672
  ) as demo:
673
 
674
  # Header
675
  gr.Markdown(
676
  """
677
- # πŸ“š MCQ Test Generator Pro
678
- ### Professional Assessment Tool Aligned with Pakistani National Curriculum (2006)
679
  """,
680
  elem_classes="header"
681
  )
682
 
683
  gr.Markdown(
684
  """
685
- Generate high-quality Multiple Choice Questions for educational assessments.
686
- Based on Bloom's Taxonomy and best practices in item writing.
 
687
  """,
688
  elem_classes="description"
689
  )
@@ -699,10 +1278,29 @@ def create_ui():
699
  info="Will appear on the test document header"
700
  )
701
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  test_type = gr.Dropdown(
703
  choices=[
704
  "Weekly Test",
705
- "Monthly Test",
706
  "Mid-Term Examination",
707
  "Final Examination",
708
  "Practice Test",
@@ -724,9 +1322,9 @@ def create_ui():
724
  minimum=5,
725
  maximum=50,
726
  value=20,
727
- step=5,
728
  label="Number of Questions",
729
- info="How many MCQs to generate"
730
  )
731
 
732
  subject_dropdown = gr.Dropdown(
@@ -748,7 +1346,6 @@ def create_ui():
748
  info="Select cognitive complexity level"
749
  )
750
 
751
- # Show description of selected difficulty
752
  difficulty_info = gr.Markdown(
753
  """
754
  **Level Description:** Explain ideas or concepts
@@ -777,9 +1374,10 @@ def create_ui():
777
  gr.Markdown(
778
  """
779
  ### πŸ’‘ Tips for Best Results:
 
780
  - Select appropriate cognitive level for your students
781
  - Review generated questions before final use
782
- - Combine multiple difficulty levels for comprehensive assessment
783
  - Ensure chapter content has been taught before testing
784
  """
785
  )
@@ -790,7 +1388,7 @@ def create_ui():
790
  output_text = gr.Textbox(
791
  label="Generation Status & Preview",
792
  lines=20,
793
- placeholder="Click 'Generate Test' to create MCQs...",
794
  interactive=False
795
  )
796
 
@@ -808,13 +1406,35 @@ def create_ui():
808
 
809
  gr.Markdown(
810
  """
811
- ### πŸ“„ Document Includes:
812
  - βœ… Professional header with school name
813
  - βœ… Complete test information table
814
  - βœ… Clear instructions for students
815
  - βœ… Well-formatted questions
816
- - βœ… Separate answer key page
817
- - βœ… Marking grid for easy grading
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  """
819
  )
820
 
@@ -823,7 +1443,8 @@ def create_ui():
823
  """
824
  ---
825
  <div class="footer">
826
- <strong>MCQ Test Generator Pro v2.0</strong><br>
 
827
  Aligned with Pakistani National Curriculum (2006)<br>
828
  Developer: <strong>Najaf Ali Sharqi</strong><br>
829
  Based on Bloom's Taxonomy & Professional Item Writing Standards<br><br>
@@ -835,21 +1456,18 @@ def create_ui():
835
 
836
  # Event handlers
837
  def update_subjects(selected_grade):
838
- """Update subject dropdown when grade changes"""
839
  if selected_grade:
840
  subjects = subjects_by_grade.get(selected_grade, [])
841
  return gr.update(choices=subjects, value=None)
842
  return gr.update(choices=[], value=None)
843
 
844
  def update_chapters(selected_subject, selected_grade):
845
- """Update chapter dropdown when subject or grade changes"""
846
  if selected_grade and selected_subject:
847
  chapters = chapters_by_subject_and_grade.get(selected_grade, {}).get(selected_subject, [])
848
  return gr.update(choices=chapters, value=None)
849
  return gr.update(choices=[], value=None)
850
 
851
  def update_difficulty_info(difficulty):
852
- """Update difficulty level description"""
853
  if difficulty in DIFFICULTY_LEVELS:
854
  info = DIFFICULTY_LEVELS[difficulty]
855
  desc = info.get('description', '')
@@ -861,6 +1479,29 @@ def create_ui():
861
  """
862
  return ""
863
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  # Connect event handlers
865
  grade_dropdown.change(
866
  update_subjects,
@@ -886,9 +1527,22 @@ def create_ui():
886
  outputs=[difficulty_info]
887
  )
888
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  generate_btn.click(
890
  on_generate,
891
  inputs=[
 
892
  grade_dropdown,
893
  subject_dropdown,
894
  chapter_dropdown,
@@ -902,45 +1556,88 @@ def create_ui():
902
 
903
  return demo
904
 
 
905
  # ---------------------------
906
  # Launch application
907
  # ---------------------------
908
- try:
909
- print("πŸš€ Starting MCQ Test Generator Pro v2.0")
910
- print(f"πŸ“¦ Gradio version: {gr.__version__}")
911
-
912
- # Check API key
913
- if not GROQ_API_KEY:
914
- print("⚠️ WARNING: GROQ_API_KEY not found in environment variables")
915
- print("Please set it in Hugging Face Space Settings > Repository secrets")
916
- else:
917
- print(f"βœ… API Key configured (length: {len(GROQ_API_KEY)})")
918
-
919
- # Create and launch
920
- demo = create_ui()
921
- print("βœ… UI created successfully")
922
-
923
- demo.queue() # Enable queue for better handling
924
- demo.launch()
925
-
926
- except Exception as e:
927
- print(f"❌ Critical Error: {str(e)}")
928
- import traceback
929
- traceback.print_exc()
930
-
931
- # Create minimal error interface
932
- with gr.Blocks() as error_demo:
933
- gr.Markdown(f"""
934
- # ❌ Application Error
935
 
936
- **Error Message:** {str(e)}
 
 
937
 
938
- **Common Solutions:**
939
- 1. Check if GROQ_API_KEY is set in Space settings
940
- 2. Verify all dependencies are installed
941
- 3. Check the build logs for errors
 
 
 
942
 
 
 
 
 
 
 
 
 
 
 
 
943
  **Contact:** Developer - Najaf Ali Sharqi
944
  """)
945
 
946
- error_demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```python
2
+ # app.py - Professional Test Generator for Hugging Face
3
  # Aligned with Pakistani National Curriculum (2006)
4
  # Developer: Najaf Ali Sharqi
5
 
 
24
  # ---------------------------
25
  # Configuration Constants
26
  # ---------------------------
27
+ APP_VERSION = "3.0"
 
28
  MODEL_NAME = "llama-3.3-70b-versatile"
29
 
30
+ # ---------------------------
31
+ # Question Types Configuration
32
+ # ---------------------------
33
+ QUESTION_TYPES = {
34
+ "Multiple Choice Questions (MCQs)": {
35
+ "description": "Four-option questions with one correct answer",
36
+ "min_questions": 5,
37
+ "max_questions": 50,
38
+ "default_questions": 20,
39
+ "marks_per_question": 1
40
+ },
41
+ "Fill in the Blanks": {
42
+ "description": "Sentences with missing words to complete",
43
+ "min_questions": 5,
44
+ "max_questions": 40,
45
+ "default_questions": 15,
46
+ "marks_per_question": 1
47
+ },
48
+ "Match the Column": {
49
+ "description": "Two columns to match related items",
50
+ "min_questions": 5,
51
+ "max_questions": 20,
52
+ "default_questions": 10,
53
+ "marks_per_question": 1
54
+ },
55
+ "Short Response Questions": {
56
+ "description": "Brief written answers (2-3 sentences)",
57
+ "min_questions": 3,
58
+ "max_questions": 15,
59
+ "default_questions": 8,
60
+ "marks_per_question": 2
61
+ },
62
+ "Long Response Questions": {
63
+ "description": "Detailed written answers (paragraphs)",
64
+ "min_questions": 2,
65
+ "max_questions": 10,
66
+ "default_questions": 5,
67
+ "marks_per_question": 5
68
+ }
69
+ }
70
+
71
  # ---------------------------
72
  # Grades and subjects aligned with Pakistan National Curriculum
73
  # ---------------------------
 
386
  }
387
 
388
  # ---------------------------
389
+ # Question Generation Functions
390
  # ---------------------------
391
+ def generate_mcqs(grade, subject, chapter, difficulty, num_questions):
392
+ """Generate Multiple Choice Questions"""
393
 
394
+ system_msg = """You are an expert educational item writer specializing in Multiple Choice Questions.
395
+
396
+ CRITICAL RULES:
397
+ 1. Start IMMEDIATELY with "1." - NO introductory text, preambles, or explanations
398
+ 2. Do NOT write phrases like "Here are the MCQs" or "Q1. Here are..."
399
+ 3. Begin directly with the first question
400
+
401
+ Format exactly as:
402
+ 1. [Question stem]
403
+ A) [Option 1]
404
+ B) [Option 2]
405
+ C) [Option 3]
406
+ D) [Option 4]
407
+
408
+ 2. [Question stem]
 
 
409
  A) [Option 1]
410
  B) [Option 2]
411
  C) [Option 3]
412
  D) [Option 4]
413
 
414
+ ITEM WRITING PRINCIPLES:
415
+ - Clear, concise question stems
416
+ - Plausible distractors based on common misconceptions
417
+ - Grammatically consistent options
418
+ - Avoid "all of the above" or "none of the above"
419
+ - Equal length options
420
+ - Age-appropriate vocabulary
421
+
422
+ After all questions, provide:
423
  ANSWER KEY:
424
  1. B
425
  2. A
426
  etc."""
427
 
428
+ user_msg = f"""Generate {num_questions} MCQs for:
429
+ Grade: {grade}
430
+ Subject: {subject}
431
+ Chapter: {chapter}
432
+ Cognitive Level: {difficulty}
 
433
 
434
  Requirements:
435
+ - Align with Pakistani National Curriculum (2006)
436
+ - Use {difficulty} level: {DIFFICULTY_LEVELS.get(difficulty, {}).get('description', '')}
437
+ - Test skills: {', '.join(DIFFICULTY_LEVELS.get(difficulty, {}).get('keywords', [])[:3])}
438
+ - Clear language for {grade}
439
+ - Plausible distractors
440
+ - Practical applications where relevant
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ START IMMEDIATELY WITH "1." - NO INTRODUCTORY TEXT."""
443
+
444
+ response = client.chat.completions.create(
445
+ messages=[
446
+ {"role": "system", "content": system_msg},
447
+ {"role": "user", "content": user_msg}
448
+ ],
449
+ model=MODEL_NAME,
450
+ temperature=0.7,
451
+ max_tokens=4000
452
+ )
453
+
454
+ return response.choices[0].message.content
455
+
456
+
457
+ def generate_fill_blanks(grade, subject, chapter, difficulty, num_questions):
458
+ """Generate Fill in the Blanks Questions"""
459
+
460
+ system_msg = """You are an expert educational item writer specializing in Fill in the Blanks questions.
461
+
462
+ CRITICAL RULES:
463
+ 1. Start IMMEDIATELY with "1." - NO introductory text
464
+ 2. Do NOT write phrases like "Here are the questions"
465
+ 3. Begin directly with the first question
466
+
467
+ Format exactly as:
468
+ 1. [Sentence with __________ for blank]
469
+
470
+ 2. [Sentence with __________ for blank]
471
+
472
+ ITEM WRITING PRINCIPLES:
473
+ - Use __________ to indicate blanks (10 underscores)
474
+ - Only ONE blank per question
475
+ - Blank should test key concepts
476
+ - Context clues should be minimal
477
+ - Unambiguous answers
478
+
479
+ After all questions, provide:
480
+ ANSWER KEY:
481
+ 1. [answer]
482
+ 2. [answer]
483
+ etc."""
484
+
485
+ user_msg = f"""Generate {num_questions} Fill in the Blanks questions for:
486
+ Grade: {grade}
487
+ Subject: {subject}
488
+ Chapter: {chapter}
489
+ Cognitive Level: {difficulty}
490
+
491
+ START IMMEDIATELY WITH "1." - NO INTRODUCTORY TEXT."""
492
+
493
+ response = client.chat.completions.create(
494
+ messages=[
495
+ {"role": "system", "content": system_msg},
496
+ {"role": "user", "content": user_msg}
497
+ ],
498
+ model=MODEL_NAME,
499
+ temperature=0.7,
500
+ max_tokens=3000
501
+ )
502
+
503
+ return response.choices[0].message.content
504
+
505
+
506
+ def generate_match_column(grade, subject, chapter, difficulty, num_questions):
507
+ """Generate Match the Column Questions"""
508
+
509
+ system_msg = """You are an expert educational item writer specializing in Match the Column questions.
510
+
511
+ CRITICAL RULES:
512
+ 1. Start IMMEDIATELY with "COLUMN A" - NO introductory text
513
+ 2. Do NOT write phrases like "Here are the questions"
514
+ 3. Begin directly with the columns
515
+
516
+ Format exactly as:
517
+ COLUMN A COLUMN B
518
+ 1. [Item A1] a) [Item B1]
519
+ 2. [Item A2] b) [Item B2]
520
+ 3. [Item A3] c) [Item B3]
521
+ 4. [Item A4] d) [Item B4]
522
+
523
+ ITEM WRITING PRINCIPLES:
524
+ - Items should be related but not obvious
525
+ - Randomize order in Column B
526
+ - Keep items concise
527
+ - Clear relationships
528
+
529
+ After columns, provide:
530
+ ANSWER KEY:
531
+ 1-b, 2-d, 3-a, 4-c"""
532
+
533
+ user_msg = f"""Generate {num_questions} matching pairs for:
534
+ Grade: {grade}
535
+ Subject: {subject}
536
+ Chapter: {chapter}
537
+ Cognitive Level: {difficulty}
538
+
539
+ START IMMEDIATELY WITH "COLUMN A" - NO INTRODUCTORY TEXT."""
540
+
541
+ response = client.chat.completions.create(
542
+ messages=[
543
+ {"role": "system", "content": system_msg},
544
+ {"role": "user", "content": user_msg}
545
+ ],
546
+ model=MODEL_NAME,
547
+ temperature=0.7,
548
+ max_tokens=2500
549
+ )
550
+
551
+ return response.choices[0].message.content
552
+
553
+
554
+ def generate_short_response(grade, subject, chapter, difficulty, num_questions):
555
+ """Generate Short Response Questions"""
556
+
557
+ system_msg = """You are an expert educational item writer specializing in Short Response questions.
558
+
559
+ CRITICAL RULES:
560
+ 1. Start IMMEDIATELY with "1." - NO introductory text
561
+ 2. Do NOT write phrases like "Here are the questions"
562
+ 3. Begin directly with the first question
563
+
564
+ Format exactly as:
565
+ 1. [Question requiring 2-3 sentence answer]
566
+
567
+ 2. [Question requiring 2-3 sentence answer]
568
+
569
+ ITEM WRITING PRINCIPLES:
570
+ - Questions require brief explanations (2-3 sentences)
571
+ - Test understanding and application
572
+ - Clear expectations
573
+ - Specific enough to guide response length
574
+
575
+ After all questions, provide:
576
+ MARKING GUIDE:
577
+ 1. [Key points to award marks]
578
+ 2. [Key points to award marks]
579
+ etc."""
580
+
581
+ user_msg = f"""Generate {num_questions} Short Response questions for:
582
+ Grade: {grade}
583
+ Subject: {subject}
584
+ Chapter: {chapter}
585
+ Cognitive Level: {difficulty}
586
+
587
+ Each question should require 2-3 sentences to answer.
588
+
589
+ START IMMEDIATELY WITH "1." - NO INTRODUCTORY TEXT."""
590
 
591
+ response = client.chat.completions.create(
592
+ messages=[
593
+ {"role": "system", "content": system_msg},
594
+ {"role": "user", "content": user_msg}
595
+ ],
596
+ model=MODEL_NAME,
597
+ temperature=0.7,
598
+ max_tokens=3000
599
+ )
600
+
601
+ return response.choices[0].message.content
602
+
603
+
604
+ def generate_long_response(grade, subject, chapter, difficulty, num_questions):
605
+ """Generate Long Response Questions"""
606
+
607
+ system_msg = """You are an expert educational item writer specializing in Long Response/Essay questions.
608
+
609
+ CRITICAL RULES:
610
+ 1. Start IMMEDIATELY with "1." - NO introductory text
611
+ 2. Do NOT write phrases like "Here are the questions"
612
+ 3. Begin directly with the first question
613
+
614
+ Format exactly as:
615
+ 1. [Question requiring detailed paragraph answer]
616
+
617
+ 2. [Question requiring detailed paragraph answer]
618
+
619
+ ITEM WRITING PRINCIPLES:
620
+ - Questions require detailed explanations (1-2 paragraphs)
621
+ - Test analysis, evaluation, synthesis
622
+ - Open-ended but focused
623
+ - Clear success criteria
624
+
625
+ After all questions, provide:
626
+ MARKING RUBRIC:
627
+ 1. [Detailed marking criteria with point allocation]
628
+ 2. [Detailed marking criteria with point allocation]
629
+ etc."""
630
+
631
+ user_msg = f"""Generate {num_questions} Long Response questions for:
632
+ Grade: {grade}
633
+ Subject: {subject}
634
+ Chapter: {chapter}
635
+ Cognitive Level: {difficulty}
636
+
637
+ Each question should require 1-2 paragraphs to answer fully.
638
+
639
+ START IMMEDIATELY WITH "1." - NO INTRODUCTORY TEXT."""
640
+
641
+ response = client.chat.completions.create(
642
+ messages=[
643
+ {"role": "system", "content": system_msg},
644
+ {"role": "user", "content": user_msg}
645
+ ],
646
+ model=MODEL_NAME,
647
+ temperature=0.7,
648
+ max_tokens=3500
649
+ )
650
+
651
+ return response.choices[0].message.content
652
+
653
+
654
+ # ---------------------------
655
+ # Parsing Functions
656
+ # ---------------------------
657
  def parse_mcqs_and_answers(text, expected_count):
658
+ """Parse MCQs and extract answer key"""
659
 
660
+ # Remove any introductory text before first question
661
+ match = re.search(r'(?:^|\n)\s*1\.\s+', text, re.MULTILINE)
662
+ if match:
663
+ text = text[match.start():]
664
+
665
+ # Split by answer key
666
  parts = re.split(r'ANSWER\s+KEY:?', text, flags=re.IGNORECASE)
667
 
668
+ questions = []
669
+ answers = []
670
 
671
  if len(parts) >= 1:
 
672
  mcq_text = parts[0]
673
+ # Split by question numbers
674
+ qs = re.split(r'(?:^|\n)\s*\d+\.\s+', mcq_text, flags=re.MULTILINE)
675
+ questions = [q.strip() for q in qs if q.strip() and len(q.strip()) > 20]
676
+ questions = questions[:expected_count]
677
 
678
  if len(parts) >= 2:
 
679
  answer_text = parts[1]
680
+ ans = re.findall(r'\d+[\.\):\s]+([A-D])', answer_text, re.IGNORECASE)
681
+ answers = [a.upper() for a in ans[:expected_count]]
682
+
683
+ # Ensure answer count matches questions
684
+ if len(answers) < len(questions):
685
+ answers.extend(['A'] * (len(questions) - len(answers)))
686
+
687
+ return questions, answers
688
+
689
+
690
+ def parse_fill_blanks(text, expected_count):
691
+ """Parse Fill in the Blanks questions"""
692
+
693
+ # Remove intro text
694
+ match = re.search(r'(?:^|\n)\s*1\.\s+', text, re.MULTILINE)
695
+ if match:
696
+ text = text[match.start():]
697
+
698
+ parts = re.split(r'ANSWER\s+KEY:?', text, flags=re.IGNORECASE)
699
+
700
+ questions = []
701
+ answers = []
702
+
703
+ if len(parts) >= 1:
704
+ q_text = parts[0]
705
+ qs = re.split(r'(?:^|\n)\s*\d+\.\s+', q_text, flags=re.MULTILINE)
706
+ questions = [q.strip() for q in qs if q.strip() and '__' in q]
707
+ questions = questions[:expected_count]
708
+
709
+ if len(parts) >= 2:
710
+ ans_text = parts[1]
711
+ ans = re.findall(r'\d+[\.\):\s]+(.+?)(?=\n\d+[\.\)]|\Z)', ans_text, re.DOTALL)
712
+ answers = [a.strip() for a in ans[:expected_count]]
713
 
714
+ if len(answers) < len(questions):
715
+ answers.extend(['[Answer]'] * (len(questions) - len(answers)))
 
716
 
717
+ return questions, answers
718
+
719
+
720
+ def parse_match_column(text):
721
+ """Parse Match the Column questions"""
722
+
723
+ # Find columns
724
+ match = re.search(r'COLUMN\s+A.*?COLUMN\s+B', text, re.IGNORECASE | re.DOTALL)
725
+ if not match:
726
+ return [], [], []
727
+
728
+ # Extract content after column headers
729
+ content_start = match.end()
730
+ parts = re.split(r'ANSWER\s+KEY:?', text[content_start:], flags=re.IGNORECASE)
731
+
732
+ column_a = []
733
+ column_b = []
734
+ answers = []
735
+
736
+ if len(parts) >= 1:
737
+ pairs_text = parts[0]
738
+ lines = pairs_text.strip().split('\n')
739
+
740
+ for line in lines:
741
+ line = line.strip()
742
+ if not line:
743
+ continue
744
+
745
+ # Match pattern: "1. Item A a) Item B"
746
+ match = re.match(r'(\d+)\.\s+(.+?)\s+([a-z])\)\s+(.+)', line, re.IGNORECASE)
747
+ if match:
748
+ num, item_a, letter, item_b = match.groups()
749
+ column_a.append(f"{num}. {item_a.strip()}")
750
+ column_b.append(f"{letter}) {item_b.strip()}")
751
+
752
+ if len(parts) >= 2:
753
+ ans_text = parts[1]
754
+ # Extract matching like "1-b, 2-d, 3-a"
755
+ matches = re.findall(r'(\d+)-([a-z])', ans_text, re.IGNORECASE)
756
+ answers = [f"{num}-{letter.lower()}" for num, letter in matches]
757
+
758
+ return column_a, column_b, answers
759
+
760
+
761
+ def parse_short_long_response(text, expected_count):
762
+ """Parse Short/Long Response questions with marking guide"""
763
+
764
+ # Remove intro text
765
+ match = re.search(r'(?:^|\n)\s*1\.\s+', text, re.MULTILINE)
766
+ if match:
767
+ text = text[match.start():]
768
+
769
+ parts = re.split(r'(?:MARKING\s+GUIDE|MARKING\s+RUBRIC):?', text, flags=re.IGNORECASE)
770
+
771
+ questions = []
772
+ marking_guide = []
773
+
774
+ if len(parts) >= 1:
775
+ q_text = parts[0]
776
+ qs = re.split(r'(?:^|\n)\s*\d+\.\s+', q_text, flags=re.MULTILINE)
777
+ questions = [q.strip() for q in qs if q.strip() and len(q.strip()) > 15]
778
+ questions = questions[:expected_count]
779
+
780
+ if len(parts) >= 2:
781
+ guide_text = parts[1]
782
+ guides = re.split(r'(?:^|\n)\s*\d+\.\s+', guide_text, flags=re.MULTILINE)
783
+ marking_guide = [g.strip() for g in guides if g.strip()]
784
+ marking_guide = marking_guide[:expected_count]
785
+
786
+ if len(marking_guide) < len(questions):
787
+ marking_guide.extend(['[Marking criteria to be determined]'] * (len(questions) - len(marking_guide)))
788
+
789
+ return questions, marking_guide
790
+
791
 
792
  # ---------------------------
793
+ # Master Generation Function
794
  # ---------------------------
795
+ def generate_questions(question_type, grade, subject, chapter, difficulty, num_questions):
796
+ """Master function to generate any type of question"""
797
 
798
  try:
799
+ if question_type == "Multiple Choice Questions (MCQs)":
800
+ text = generate_mcqs(grade, subject, chapter, difficulty, num_questions)
801
+ questions, answers = parse_mcqs_and_answers(text, num_questions)
802
+ return questions, answers, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
+ elif question_type == "Fill in the Blanks":
805
+ text = generate_fill_blanks(grade, subject, chapter, difficulty, num_questions)
806
+ questions, answers = parse_fill_blanks(text, num_questions)
807
+ return questions, answers, None, None
 
 
 
 
 
 
 
 
 
 
 
808
 
809
+ elif question_type == "Match the Column":
810
+ text = generate_match_column(grade, subject, chapter, difficulty, num_questions)
811
+ col_a, col_b, answers = parse_match_column(text)
812
+ return col_a, col_b, answers, None
 
 
 
 
 
813
 
814
+ elif question_type == "Short Response Questions":
815
+ text = generate_short_response(grade, subject, chapter, difficulty, num_questions)
816
+ questions, marking = parse_short_long_response(text, num_questions)
817
+ return questions, None, marking, None
818
 
819
+ elif question_type == "Long Response Questions":
820
+ text = generate_long_response(grade, subject, chapter, difficulty, num_questions)
821
+ questions, marking = parse_short_long_response(text, num_questions)
822
+ return questions, None, marking, None
823
 
824
+ except Exception as e:
825
+ return None, None, None, f"Error: {str(e)}"
826
+
827
+
828
+ # ---------------------------
829
+ # Enhanced DOCX Export Functions
830
+ # ---------------------------
831
+ def export_mcqs_to_word(questions, answers, grade, subject, chapter, difficulty, test_type, school_name, total_marks):
832
+ """Export MCQs to Word document"""
833
+
834
+ doc = Document()
835
+ setup_document_style(doc)
836
+ add_header(doc, school_name, test_type)
837
+ add_info_table(doc, grade, subject, chapter, difficulty, total_marks, len(questions))
838
+ add_instructions(doc, "Select the MOST appropriate answer for each question")
839
+
840
+ # Add questions
841
+ doc.add_paragraph()
842
+ heading = doc.add_paragraph()
843
+ heading.add_run("SECTION A: MULTIPLE CHOICE QUESTIONS").bold = True
844
+ heading.runs[0].font.size = Pt(12)
845
+ doc.add_paragraph()
846
+
847
+ for i, q in enumerate(questions, 1):
848
+ q_para = doc.add_paragraph()
849
+ q_para.add_run(f"{i}. ").bold = True
850
+ q_para.add_run(q)
851
+ q_para.space_after = Pt(6)
852
+ doc.add_paragraph()
853
+
854
+ # Add answer key
855
+ doc.add_page_break()
856
+ add_answer_key_mcq(doc, answers)
857
+ add_footer(doc)
858
+
859
+ return save_document(doc, school_name, grade, subject, chapter, "MCQ")
860
+
861
+
862
+ def export_fill_blanks_to_word(questions, answers, grade, subject, chapter, difficulty, test_type, school_name, total_marks):
863
+ """Export Fill in the Blanks to Word document"""
864
+
865
+ doc = Document()
866
+ setup_document_style(doc)
867
+ add_header(doc, school_name, test_type)
868
+ add_info_table(doc, grade, subject, chapter, difficulty, total_marks, len(questions))
869
+ add_instructions(doc, "Fill in the blanks with appropriate words")
870
+
871
+ # Add questions
872
+ doc.add_paragraph()
873
+ heading = doc.add_paragraph()
874
+ heading.add_run("SECTION A: FILL IN THE BLANKS").bold = True
875
+ heading.runs[0].font.size = Pt(12)
876
+ doc.add_paragraph()
877
+
878
+ for i, q in enumerate(questions, 1):
879
+ q_para = doc.add_paragraph()
880
+ q_para.add_run(f"{i}. ").bold = True
881
+ q_para.add_run(q)
882
+ q_para.space_after = Pt(8)
883
+
884
+ # Add answer key
885
+ doc.add_page_break()
886
+ add_answer_key_fill_blanks(doc, answers)
887
+ add_footer(doc)
888
+
889
+ return save_document(doc, school_name, grade, subject, chapter, "FillBlanks")
890
+
891
+
892
+ def export_match_column_to_word(col_a, col_b, answers, grade, subject, chapter, difficulty, test_type, school_name, total_marks):
893
+ """Export Match the Column to Word document"""
894
+
895
+ doc = Document()
896
+ setup_document_style(doc)
897
+ add_header(doc, school_name, test_type)
898
+ add_info_table(doc, grade, subject, chapter, difficulty, total_marks, len(col_a))
899
+ add_instructions(doc, "Match items in Column A with Column B")
900
+
901
+ # Add columns
902
+ doc.add_paragraph()
903
+ heading = doc.add_paragraph()
904
+ heading.add_run("SECTION A: MATCH THE COLUMN").bold = True
905
+ heading.runs[0].font.size = Pt(12)
906
+ doc.add_paragraph()
907
+
908
+ # Create table
909
+ table = doc.add_table(rows=len(col_a) + 1, cols=3)
910
+ table.style = 'Light Grid Accent 1'
911
+
912
+ # Headers
913
+ table.cell(0, 0).text = "COLUMN A"
914
+ table.cell(0, 1).text = "COLUMN B"
915
+ table.cell(0, 2).text = "Answer"
916
+
917
+ for cell in table.rows[0].cells:
918
+ cell.paragraphs[0].runs[0].bold = True
919
+ cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
920
+
921
+ # Fill data
922
+ for i in range(len(col_a)):
923
+ if i < len(col_a):
924
+ table.cell(i+1, 0).text = col_a[i]
925
+ if i < len(col_b):
926
+ table.cell(i+1, 1).text = col_b[i]
927
+ table.cell(i+1, 2).text = "____"
928
+
929
+ # Add answer key
930
+ doc.add_page_break()
931
+ add_answer_key_match(doc, answers)
932
+ add_footer(doc)
933
+
934
+ return save_document(doc, school_name, grade, subject, chapter, "MatchColumn")
935
+
936
+
937
+ def export_response_to_word(questions, marking_guide, grade, subject, chapter, difficulty, test_type, school_name, total_marks, response_type):
938
+ """Export Short/Long Response questions to Word document"""
939
+
940
+ doc = Document()
941
+ setup_document_style(doc)
942
+ add_header(doc, school_name, test_type)
943
+ add_info_table(doc, grade, subject, chapter, difficulty, total_marks, len(questions))
944
+
945
+ if response_type == "Short":
946
+ add_instructions(doc, "Answer each question in 2-3 sentences")
947
+ section_title = "SECTION A: SHORT RESPONSE QUESTIONS"
948
+ else:
949
+ add_instructions(doc, "Answer each question in detail (1-2 paragraphs)")
950
+ section_title = "SECTION A: LONG RESPONSE QUESTIONS"
951
+
952
+ # Add questions
953
+ doc.add_paragraph()
954
+ heading = doc.add_paragraph()
955
+ heading.add_run(section_title).bold = True
956
+ heading.runs[0].font.size = Pt(12)
957
+ doc.add_paragraph()
958
+
959
+ for i, q in enumerate(questions, 1):
960
+ q_para = doc.add_paragraph()
961
+ q_para.add_run(f"{i}. ").bold = True
962
+ q_para.add_run(q)
963
+ q_para.space_after = Pt(10)
964
 
965
+ # Add answer space
966
+ doc.add_paragraph("_" * 80)
967
+ doc.add_paragraph("_" * 80)
968
+ if response_type == "Long":
969
+ doc.add_paragraph("_" * 80)
970
+ doc.add_paragraph("_" * 80)
971
+ doc.add_paragraph()
972
+
973
+ # Add marking guide
974
+ doc.add_page_break()
975
+ add_marking_guide(doc, questions, marking_guide, response_type)
976
+ add_footer(doc)
977
+
978
+ file_type = "ShortResponse" if response_type == "Short" else "LongResponse"
979
+ return save_document(doc, school_name, grade, subject, chapter, file_type)
980
+
981
+
982
+ # ---------------------------
983
+ # Document Helper Functions
984
+ # ---------------------------
985
+ def setup_document_style(doc):
986
+ """Setup document margins"""
987
+ sections = doc.sections
988
+ for section in sections:
989
+ section.top_margin = Inches(0.8)
990
+ section.bottom_margin = Inches(0.8)
991
+ section.left_margin = Inches(0.9)
992
+ section.right_margin = Inches(0.9)
993
+
994
+
995
+ def add_header(doc, school_name, test_type):
996
+ """Add document header"""
997
+ if school_name and school_name.strip():
998
+ school_heading = doc.add_heading(school_name.upper(), level=0)
999
+ school_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
1000
+ for run in school_heading.runs:
1001
+ run.font.size = Pt(16)
1002
+ run.font.color.rgb = RGBColor(0, 51, 102)
1003
+
1004
+ test_title = doc.add_heading(f"{test_type.upper()}", level=1)
1005
+ test_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
1006
+ for run in test_title.runs:
1007
+ run.bold = True
1008
+ run.font.color.rgb = RGBColor(0, 102, 51)
1009
+ run.font.size = Pt(16)
1010
+
1011
+
1012
+ def add_info_table(doc, grade, subject, chapter, difficulty, total_marks, num_questions):
1013
+ """Add information table"""
1014
+ table = doc.add_table(rows=7, cols=2)
1015
+ table.style = 'Light Grid Accent 1'
1016
+
1017
+ info_data = [
1018
+ ("Grade:", grade),
1019
+ ("Subject:", subject),
1020
+ ("Chapter:", chapter),
1021
+ ("Cognitive Level:", difficulty),
1022
+ ("Total Marks:", str(total_marks)),
1023
+ ("Date:", datetime.now().strftime("%d-%m-%Y")),
1024
+ ("Duration:", "60 minutes" if num_questions > 15 else "40 minutes")
1025
+ ]
1026
+
1027
+ for idx, (label, value) in enumerate(info_data):
1028
+ row = table.rows[idx]
1029
+ row.cells[0].text = label
1030
+ row.cells[1].text = value
1031
+ row.cells[0].paragraphs[0].runs[0].bold = True
1032
+ row.cells[0].paragraphs[0].runs[0].font.size = Pt(10)
1033
+ row.cells[1].paragraphs[0].runs[0].font.size = Pt(10)
1034
+
1035
+
1036
+ def add_instructions(doc, instruction_text):
1037
+ """Add instructions section"""
1038
+ doc.add_paragraph()
1039
+ instructions = doc.add_paragraph()
1040
+ instructions.add_run("INSTRUCTIONS:").bold = True
1041
+ doc.add_paragraph(f"β€’ {instruction_text}")
1042
+ doc.add_paragraph("β€’ Read each question carefully")
1043
+ doc.add_paragraph("β€’ Write neatly and legibly")
1044
+ doc.add_paragraph("β€’ Manage your time effectively")
1045
+
1046
+
1047
+ def add_answer_key_mcq(doc, answers):
1048
+ """Add MCQ answer key"""
1049
+ ans_heading = doc.add_heading("ANSWER KEY", level=2)
1050
+ ans_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
1051
+ for run in ans_heading.runs:
1052
+ run.font.color.rgb = RGBColor(153, 0, 0)
1053
+
1054
+ doc.add_paragraph("(For Teacher's Use Only)", style='Intense Quote')
1055
+
1056
+ num_rows = len(answers) + 1
1057
+ ans_table = doc.add_table(rows=num_rows, cols=5)
1058
+ ans_table.style = 'Medium Grid 1 Accent 1'
1059
+
1060
+ headers = ["Q#", "A", "B", "C", "D"]
1061
+ for idx, header in enumerate(headers):
1062
+ cell = ans_table.cell(0, idx)
1063
+ cell.text = header
1064
+ cell.paragraphs[0].runs[0].bold = True
1065
+ cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
1066
+
1067
+ for r, ans in enumerate(answers, start=1):
1068
+ ans_table.cell(r, 0).text = str(r)
1069
+ ans_table.cell(r, 0).paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
1070
 
1071
+ for c, option in enumerate(["A", "B", "C", "D"], start=1):
1072
+ cell = ans_table.cell(r, c)
1073
+ if ans == option:
1074
+ cell.text = "βœ“"
1075
+ cell.paragraphs[0].runs[0].bold = True
1076
+ cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(0, 128, 0)
1077
+ cell.paragraphs[0].runs[0].font.size = Pt(14)
1078
+ else:
1079
+ cell.text = ""
1080
  cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
1081
+
1082
+
1083
+ def add_answer_key_fill_blanks(doc, answers):
1084
+ """Add Fill in the Blanks answer key"""
1085
+ ans_heading = doc.add_heading("ANSWER KEY", level=2)
1086
+ ans_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
1087
+ for run in ans_heading.runs:
1088
+ run.font.color.rgb = RGBColor(153, 0, 0)
1089
+
1090
+ doc.add_paragraph("(For Teacher's Use Only)", style='Intense Quote')
1091
+
1092
+ for i, ans in enumerate(answers, 1):
1093
+ para = doc.add_paragraph()
1094
+ para.add_run(f"{i}. ").bold = True
1095
+ para.add_run(ans)
1096
+
1097
+
1098
+ def add_answer_key_match(doc, answers):
1099
+ """Add Match the Column answer key"""
1100
+ ans_heading = doc.add_heading("ANSWER KEY", level=2)
1101
+ ans_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
1102
+ for run in ans_heading.runs:
1103
+ run.font.color.rgb = RGBColor(153, 0, 0)
1104
+
1105
+ doc.add_paragraph("(For Teacher's Use Only)", style='Intense Quote')
1106
+
1107
+ para = doc.add_paragraph()
1108
+ para.add_run("Correct Matches:\n").bold = True
1109
+ para.add_run(", ".join(answers))
1110
+
1111
+
1112
+ def add_marking_guide(doc, questions, marking_guide, response_type):
1113
+ """Add marking guide for response questions"""
1114
+ guide_heading = doc.add_heading("MARKING GUIDE", level=2)
1115
+ guide_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
1116
+ for run in guide_heading.runs:
1117
+ run.font.color.rgb = RGBColor(153, 0, 0)
1118
+
1119
+ doc.add_paragraph("(For Teacher's Use Only)", style='Intense Quote')
1120
+
1121
+ for i, guide in enumerate(marking_guide, 1):
1122
+ para = doc.add_paragraph()
1123
+ para.add_run(f"Question {i}:\n").bold = True
1124
+ para.add_run(guide)
1125
  doc.add_paragraph()
1126
+
1127
+
1128
+ def add_footer(doc):
1129
+ """Add document footer"""
1130
+ doc.add_paragraph()
1131
+ footer = doc.add_paragraph()
1132
+ footer.add_run(f"Generated by Professional Test Generator v{APP_VERSION} | Developer: Najaf Ali Sharqi | {datetime.now().strftime('%d-%m-%Y %H:%M')}").italic = True
1133
+ footer.runs[0].font.size = Pt(8)
1134
+ footer.runs[0].font.color.rgb = RGBColor(128, 128, 128)
1135
+ footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
1136
+
1137
+
1138
+ def save_document(doc, school_name, grade, subject, chapter, doc_type):
1139
+ """Save document with appropriate filename"""
1140
+ safe_chapter = re.sub(r'[\\/*?:"<>|]', "_", chapter)
1141
+ safe_school = re.sub(r'[\\/*?:"<>|]', "_", school_name) if school_name else "School"
1142
+ filename = f"{safe_school}_{grade}_{subject}_{safe_chapter}_{doc_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
1143
+
1144
+ doc.save(filename)
1145
+ return filename
1146
+
1147
 
1148
  # ---------------------------
1149
+ # Main Generation Handler
1150
  # ---------------------------
1151
+ def on_generate(question_type, grade, subject, chapter, difficulty, num_questions, test_type, school_name):
1152
+ """Handle question generation request"""
1153
 
1154
  # Validation
1155
+ if not all([question_type, grade, subject, chapter, difficulty]):
1156
  return "⚠️ Please fill all required fields", None, None
1157
 
1158
  if not school_name or not school_name.strip():
1159
  school_name = "Educational Institution"
1160
 
1161
  try:
1162
+ status_msg = f"πŸ”„ Generating {num_questions} {question_type} for {grade} {subject}...\n"
 
1163
  status_msg += f"πŸ“– Chapter: {chapter}\n"
1164
  status_msg += f"🎯 Cognitive Level: {difficulty}\n"
1165
  status_msg += "⏳ Please wait..."
1166
 
1167
+ # Generate questions
1168
+ result1, result2, result3, error = generate_questions(
1169
+ question_type, grade, subject, chapter, difficulty, num_questions
 
1170
  )
1171
 
1172
  if error:
1173
  return f"❌ Error: {error}", None, None
1174
 
 
 
 
1175
  # Calculate marks
1176
+ marks_per_q = QUESTION_TYPES[question_type]["marks_per_question"]
1177
+ total_marks = len(result1) * marks_per_q if result1 else 0
 
 
 
 
 
1178
 
1179
+ # Export based on type
1180
+ if question_type == "Multiple Choice Questions (MCQs)":
1181
+ file_path = export_mcqs_to_word(
1182
+ result1, result2, grade, subject, chapter,
1183
+ difficulty, test_type, school_name, total_marks
1184
+ )
1185
+ elif question_type == "Fill in the Blanks":
1186
+ file_path = export_fill_blanks_to_word(
1187
+ result1, result2, grade, subject, chapter,
1188
+ difficulty, test_type, school_name, total_marks
1189
+ )
1190
+ elif question_type == "Match the Column":
1191
+ file_path = export_match_column_to_word(
1192
+ result1, result2, result3, grade, subject, chapter,
1193
+ difficulty, test_type, school_name, total_marks
1194
+ )
1195
+ elif question_type == "Short Response Questions":
1196
+ file_path = export_response_to_word(
1197
+ result1, result3, grade, subject, chapter,
1198
+ difficulty, test_type, school_name, total_marks, "Short"
1199
+ )
1200
+ else: # Long Response Questions
1201
+ file_path = export_response_to_word(
1202
+ result1, result3, grade, subject, chapter,
1203
+ difficulty, test_type, school_name, total_marks, "Long"
1204
+ )
1205
 
1206
  # Success message
1207
  success_msg = f"βœ… Test generated successfully!\n\n"
1208
  success_msg += f"πŸ“Š Statistics:\n"
1209
+ success_msg += f"β€’ Question Type: {question_type}\n"
1210
+ success_msg += f"β€’ Total Questions: {len(result1) if result1 else 0}\n"
1211
+ success_msg += f"β€’ Marks Per Question: {marks_per_q}\n"
1212
  success_msg += f"β€’ Total Marks: {total_marks}\n"
1213
  success_msg += f"β€’ Grade Level: {grade}\n"
1214
  success_msg += f"β€’ Subject: {subject}\n"
 
1217
  success_msg += f"πŸ“₯ Download your test document below"
1218
 
1219
  # Preview
1220
+ if result1:
1221
+ preview = "\n\nπŸ“ PREVIEW (First 3 items):\n" + "="*50 + "\n\n"
1222
+ for i, item in enumerate(result1[:3], 1):
1223
+ preview += f"{i}. {item[:150]}...\n\n" if len(item) > 150 else f"{i}. {item}\n\n"
1224
+ success_msg += preview
1225
 
1226
+ return success_msg, file_path, f"πŸŽ‰ File ready: {file_path}"
1227
 
1228
  except Exception as e:
1229
  return f"❌ Unexpected error: {str(e)}", None, None
1230
 
1231
+
1232
  # ---------------------------
1233
  # Enhanced Gradio UI
1234
  # ---------------------------
 
1245
  .description {text-align: center; color: #666; margin-bottom: 30px;}
1246
  .footer {text-align: center; margin-top: 30px; padding: 20px; color: #888;}
1247
  .warning {background-color: #fff3cd; padding: 10px; border-radius: 5px; margin: 10px 0;}
1248
+ .info-box {background-color: #e7f3ff; padding: 15px; border-radius: 5px; margin: 10px 0;}
1249
  """
1250
  ) as demo:
1251
 
1252
  # Header
1253
  gr.Markdown(
1254
  """
1255
+ # πŸ“š Professional Test Generator Pro
1256
+ ### Multi-Format Assessment Tool Aligned with Pakistani National Curriculum (2006)
1257
  """,
1258
  elem_classes="header"
1259
  )
1260
 
1261
  gr.Markdown(
1262
  """
1263
+ Generate high-quality assessment questions in multiple formats: MCQs, Fill in the Blanks,
1264
+ Match the Column, Short Response, and Long Response Questions.
1265
+ Based on Bloom's Taxonomy and professional item writing standards.
1266
  """,
1267
  elem_classes="description"
1268
  )
 
1278
  info="Will appear on the test document header"
1279
  )
1280
 
1281
+ question_type_dropdown = gr.Dropdown(
1282
+ choices=list(QUESTION_TYPES.keys()),
1283
+ value="Multiple Choice Questions (MCQs)",
1284
+ label="Question Type",
1285
+ info="Select the type of questions to generate"
1286
+ )
1287
+
1288
+ # Dynamic info box for question type
1289
+ question_type_info = gr.Markdown(
1290
+ f"""
1291
+ <div class="info-box">
1292
+ <strong>πŸ“ {list(QUESTION_TYPES.keys())[0]}</strong><br>
1293
+ {QUESTION_TYPES[list(QUESTION_TYPES.keys())[0]]['description']}<br>
1294
+ Marks per question: {QUESTION_TYPES[list(QUESTION_TYPES.keys())[0]]['marks_per_question']}
1295
+ </div>
1296
+ """,
1297
+ visible=True
1298
+ )
1299
+
1300
  test_type = gr.Dropdown(
1301
  choices=[
1302
  "Weekly Test",
1303
+ "Monthly Test",
1304
  "Mid-Term Examination",
1305
  "Final Examination",
1306
  "Practice Test",
 
1322
  minimum=5,
1323
  maximum=50,
1324
  value=20,
1325
+ step=1,
1326
  label="Number of Questions",
1327
+ info="How many questions to generate"
1328
  )
1329
 
1330
  subject_dropdown = gr.Dropdown(
 
1346
  info="Select cognitive complexity level"
1347
  )
1348
 
 
1349
  difficulty_info = gr.Markdown(
1350
  """
1351
  **Level Description:** Explain ideas or concepts
 
1374
  gr.Markdown(
1375
  """
1376
  ### πŸ’‘ Tips for Best Results:
1377
+ - Choose question type appropriate for learning objectives
1378
  - Select appropriate cognitive level for your students
1379
  - Review generated questions before final use
1380
+ - Mix different question types for comprehensive assessment
1381
  - Ensure chapter content has been taught before testing
1382
  """
1383
  )
 
1388
  output_text = gr.Textbox(
1389
  label="Generation Status & Preview",
1390
  lines=20,
1391
+ placeholder="Click 'Generate Test' to create questions...",
1392
  interactive=False
1393
  )
1394
 
 
1406
 
1407
  gr.Markdown(
1408
  """
1409
+ ### πŸ“„ Document Features:
1410
  - βœ… Professional header with school name
1411
  - βœ… Complete test information table
1412
  - βœ… Clear instructions for students
1413
  - βœ… Well-formatted questions
1414
+ - βœ… Separate answer key/marking guide
1415
+ - βœ… Format-specific layouts
1416
+
1417
+ ### πŸ“š Supported Question Types:
1418
+
1419
+ **1. Multiple Choice Questions (MCQs)**
1420
+ - Four options with one correct answer
1421
+ - Ideal for knowledge and comprehension
1422
+
1423
+ **2. Fill in the Blanks**
1424
+ - Tests recall and vocabulary
1425
+ - Quick to grade
1426
+
1427
+ **3. Match the Column**
1428
+ - Tests relationships between concepts
1429
+ - Efficient assessment format
1430
+
1431
+ **4. Short Response Questions**
1432
+ - Requires 2-3 sentence answers
1433
+ - Tests understanding and application
1434
+
1435
+ **5. Long Response Questions**
1436
+ - Requires detailed paragraph answers
1437
+ - Tests analysis, evaluation, synthesis
1438
  """
1439
  )
1440
 
 
1443
  """
1444
  ---
1445
  <div class="footer">
1446
+ <strong>Professional Test Generator Pro v3.0</strong><br>
1447
+ Multi-Format Assessment Tool<br>
1448
  Aligned with Pakistani National Curriculum (2006)<br>
1449
  Developer: <strong>Najaf Ali Sharqi</strong><br>
1450
  Based on Bloom's Taxonomy & Professional Item Writing Standards<br><br>
 
1456
 
1457
  # Event handlers
1458
  def update_subjects(selected_grade):
 
1459
  if selected_grade:
1460
  subjects = subjects_by_grade.get(selected_grade, [])
1461
  return gr.update(choices=subjects, value=None)
1462
  return gr.update(choices=[], value=None)
1463
 
1464
  def update_chapters(selected_subject, selected_grade):
 
1465
  if selected_grade and selected_subject:
1466
  chapters = chapters_by_subject_and_grade.get(selected_grade, {}).get(selected_subject, [])
1467
  return gr.update(choices=chapters, value=None)
1468
  return gr.update(choices=[], value=None)
1469
 
1470
  def update_difficulty_info(difficulty):
 
1471
  if difficulty in DIFFICULTY_LEVELS:
1472
  info = DIFFICULTY_LEVELS[difficulty]
1473
  desc = info.get('description', '')
 
1479
  """
1480
  return ""
1481
 
1482
+ def update_question_type_info(q_type):
1483
+ if q_type in QUESTION_TYPES:
1484
+ info = QUESTION_TYPES[q_type]
1485
+ return f"""
1486
+ <div class="info-box">
1487
+ <strong>πŸ“ {q_type}</strong><br>
1488
+ {info['description']}<br>
1489
+ Marks per question: {info['marks_per_question']}<br>
1490
+ Recommended range: {info['min_questions']}-{info['max_questions']} questions
1491
+ </div>
1492
+ """
1493
+ return ""
1494
+
1495
+ def update_slider_range(q_type):
1496
+ if q_type in QUESTION_TYPES:
1497
+ info = QUESTION_TYPES[q_type]
1498
+ return gr.update(
1499
+ minimum=info['min_questions'],
1500
+ maximum=info['max_questions'],
1501
+ value=info['default_questions']
1502
+ )
1503
+ return gr.update()
1504
+
1505
  # Connect event handlers
1506
  grade_dropdown.change(
1507
  update_subjects,
 
1527
  outputs=[difficulty_info]
1528
  )
1529
 
1530
+ question_type_dropdown.change(
1531
+ update_question_type_info,
1532
+ inputs=[question_type_dropdown],
1533
+ outputs=[question_type_info]
1534
+ )
1535
+
1536
+ question_type_dropdown.change(
1537
+ update_slider_range,
1538
+ inputs=[question_type_dropdown],
1539
+ outputs=[num_questions]
1540
+ )
1541
+
1542
  generate_btn.click(
1543
  on_generate,
1544
  inputs=[
1545
+ question_type_dropdown,
1546
  grade_dropdown,
1547
  subject_dropdown,
1548
  chapter_dropdown,
 
1556
 
1557
  return demo
1558
 
1559
+
1560
  # ---------------------------
1561
  # Launch application
1562
  # ---------------------------
1563
+ if __name__ == "__main__":
1564
+ try:
1565
+ print("πŸš€ Starting Professional Test Generator Pro v3.0")
1566
+ print(f"πŸ“¦ Gradio version: {gr.__version__}")
1567
+
1568
+ # Check API key
1569
+ if not GROQ_API_KEY:
1570
+ print("⚠️ WARNING: GROQ_API_KEY not found in environment variables")
1571
+ print("Please set it in Hugging Face Space Settings > Repository secrets")
1572
+ else:
1573
+ print(f"βœ… API Key configured (length: {len(GROQ_API_KEY)})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1574
 
1575
+ # Create and launch
1576
+ demo = create_ui()
1577
+ print("βœ… UI created successfully")
1578
 
1579
+ demo.queue()
1580
+ demo.launch()
1581
+
1582
+ except Exception as e:
1583
+ print(f"❌ Critical Error: {str(e)}")
1584
+ import traceback
1585
+ traceback.print_exc()
1586
 
1587
+ # Create minimal error interface
1588
+ with gr.Blocks() as error_demo:
1589
+ gr.Markdown(f"""
1590
+ # ❌ Application Error
1591
+
1592
+ **Error Message:** {str(e)}
1593
+
1594
+ **Common Solutions:**
1595
+ 1. Check if GROQ_API_KEY is set in Space settings
1596
+ 2. Verify all dependencies are installed
1597
+ 3. Check the build logs for errors
1598
  **Contact:** Developer - Najaf Ali Sharqi
1599
  """)
1600
 
1601
+ error_demo.launch()
1602
+
1603
+ ## Complete! πŸŽ‰
1604
+
1605
+ This is the **complete, production-ready code** with all features:
1606
+
1607
+ ### βœ… Key Features Implemented:
1608
+
1609
+ 1. **5 Question Types:**
1610
+ - Multiple Choice Questions (MCQs)
1611
+ - Fill in the Blanks
1612
+ - Match the Column
1613
+ - Short Response Questions
1614
+ - Long Response Questions
1615
+
1616
+ 2. **Fixed Parsing Issues:**
1617
+ - Removes introductory text like "Here are the MCQs..."
1618
+ - Starts directly with actual questions
1619
+ - Robust regex parsing for all formats
1620
+
1621
+ 3. **Professional Documents:**
1622
+ - Format-specific layouts
1623
+ - Answer keys/marking guides
1624
+ - Professional styling
1625
+
1626
+ 4. **Dynamic UI:**
1627
+ - Question type selector with info boxes
1628
+ - Dynamic slider ranges based on question type
1629
+ - Real-time updates
1630
+
1631
+ ### πŸ“¦ Requirements (`requirements.txt`):
1632
+ gradio>=4.0.0
1633
+ groq>=0.4.0
1634
+ python-docx>=0.8.11
1635
+
1636
+ ### πŸš€ Deployment Steps:
1637
+ 1. Create new Hugging Face Space
1638
+ 2. Upload this `app.py`
1639
+ 3. Create `requirements.txt` with above content
1640
+ 4. Set `GROQ_API_KEY` in Space secrets
1641
+ 5. Deploy!
1642
+
1643
+ The app is ready for production use! 🎯