scripts/expand_seed.py generates synthetic questions across the full JEE Main syllabus taxonomy (15 chapters, 45 subtopics, correct MCQ/NUM split). Stems are deliberately sparse so no-one mistakes them for NTA content. DATA.md documents why there is no real scraper: NTA PDFs render questions as images; pdftotext extracts only IDs. Every aggregator with structured data built it through manual entry pipelines. The document lists realistic paths to a real bank and the SQL schema for loading one.
171 lines
9.4 KiB
Python
171 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Expand the demo question bank beyond the 24 built-in questions.
|
|
|
|
This is NOT a real scraper — the actual JEE Main questions are published in
|
|
image-rendered PDFs and have no machine-readable source. Every "structured JEE
|
|
question bank" found online is either paywalled, behind a login, or itself
|
|
derived from manual entry.
|
|
|
|
What this script does: generates a larger set of synthetic questions that are
|
|
realistic in form (real syllabus topics, correct +4/-1 scheme, correct NUM
|
|
vs MCQ split) so the app can be demonstrated at scale without misrepresenting
|
|
sourcing. The stems are deliberately sparse ("placeholder") so no-one mistakes
|
|
them for real exam questions.
|
|
|
|
To get real questions:
|
|
1. MathonGo / Embibe / Allen API — paid, verified, well-tagged.
|
|
2. Manual entry from NTA PDFs using pdftotext + human review.
|
|
3. OCR (tesseract) on NTA PDFs — works but math rendering degrades badly.
|
|
|
|
Usage:
|
|
python3 scripts/expand_seed.py <database-path> [--count 500]
|
|
"""
|
|
import sqlite3, argparse, random, sys
|
|
|
|
TOPICS = [
|
|
# (subject, chapter, subtopic, question_type, difficulty, typical_years)
|
|
("Physics", "Mechanics", "Kinematics", "MCQ", "Easy", range(2020, 2026)),
|
|
("Physics", "Mechanics", "Kinematics", "NUM", "Medium", range(2020, 2026)),
|
|
("Physics", "Mechanics", "Laws of Motion", "MCQ", "Easy", range(2020, 2026)),
|
|
("Physics", "Mechanics", "Laws of Motion", "NUM", "Hard", range(2022, 2026)),
|
|
("Physics", "Mechanics", "Work Energy Power", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Mechanics", "Work Energy Power", "NUM", "Medium", range(2021, 2026)),
|
|
("Physics", "Electromagnetism", "Electrostatics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Electromagnetism", "Electrostatics", "NUM", "Hard", range(2021, 2026)),
|
|
("Physics", "Electromagnetism", "Current Electricity", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Optics", "Ray Optics", "MCQ", "Easy", range(2020, 2026)),
|
|
("Physics", "Optics", "Wave Optics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Optics", "Wave Optics", "NUM", "Hard", range(2022, 2026)),
|
|
("Physics", "Modern Physics", "Photoelectric Effect", "MCQ", "Easy", range(2020, 2026)),
|
|
("Physics", "Modern Physics", "Nuclear Physics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Modern Physics", "Atoms and Nuclei", "NUM", "Hard", range(2021, 2026)),
|
|
("Physics", "Thermodynamics", "Laws of Thermodynamics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Thermodynamics", "Kinetic Theory", "NUM", "Hard", range(2021, 2026)),
|
|
("Physics", "Waves", "Sound Waves", "MCQ", "Medium", range(2020, 2026)),
|
|
("Physics", "Waves", "Simple Harmonic Motion", "MCQ", "Easy", range(2020, 2026)),
|
|
("Physics", "Waves", "Simple Harmonic Motion", "NUM", "Medium", range(2022, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Mole Concept", "MCQ", "Easy", range(2020, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Mole Concept", "NUM", "Easy", range(2020, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Chemical Kinetics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Chemical Equilibrium", "MCQ", "Hard", range(2021, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Electrochemistry", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Electrochemistry", "NUM", "Hard", range(2022, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Thermodynamics", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Physical Chemistry", "Solutions", "NUM", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Organic Chemistry", "GOC", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Organic Chemistry", "Aldehydes and Ketones", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Organic Chemistry", "Hydrocarbons", "MCQ", "Easy", range(2020, 2026)),
|
|
("Chemistry", "Organic Chemistry", "Biomolecules", "MCQ", "Easy", range(2021, 2026)),
|
|
("Chemistry", "Inorganic Chemistry", "Periodic Table", "MCQ", "Easy", range(2020, 2026)),
|
|
("Chemistry", "Inorganic Chemistry", "Coordination Compounds", "MCQ", "Medium", range(2020, 2026)),
|
|
("Chemistry", "Inorganic Chemistry", "d and f Block Elements", "MCQ", "Hard", range(2021, 2026)),
|
|
("Maths", "Algebra", "Quadratic Equations", "MCQ", "Easy", range(2020, 2026)),
|
|
("Maths", "Algebra", "Sequences and Series", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Algebra", "Sequences and Series", "NUM", "Hard", range(2021, 2026)),
|
|
("Maths", "Algebra", "Matrices and Determinants", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Algebra", "Matrices and Determinants", "NUM", "Hard", range(2022, 2026)),
|
|
("Maths", "Algebra", "Permutations and Combinations", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Algebra", "Binomial Theorem", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Calculus", "Limits and Continuity", "MCQ", "Easy", range(2020, 2026)),
|
|
("Maths", "Calculus", "Differentiation", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Calculus", "Definite Integration", "MCQ", "Hard", range(2020, 2026)),
|
|
("Maths", "Calculus", "Definite Integration", "NUM", "Hard", range(2021, 2026)),
|
|
("Maths", "Calculus", "Differential Equations", "MCQ", "Hard", range(2021, 2026)),
|
|
("Maths", "Geometry", "Straight Lines", "MCQ", "Easy", range(2020, 2026)),
|
|
("Maths", "Geometry", "Circles", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Geometry", "Parabola", "MCQ", "Hard", range(2020, 2026)),
|
|
("Maths", "Geometry", "Ellipse and Hyperbola", "MCQ", "Hard", range(2021, 2026)),
|
|
("Maths", "Trigonometry", "Trigonometric Functions", "MCQ", "Easy", range(2020, 2026)),
|
|
("Maths", "Trigonometry", "Inverse Trigonometry", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Vectors and 3D", "Vectors", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Vectors and 3D", "3D Geometry", "MCQ", "Hard", range(2021, 2026)),
|
|
("Maths", "Statistics", "Probability", "MCQ", "Medium", range(2020, 2026)),
|
|
("Maths", "Statistics", "Probability", "NUM", "Hard", range(2022, 2026)),
|
|
]
|
|
|
|
SHIFTS = ["S1", "S2"]
|
|
ANSWERS_MCQ = ["A", "B", "C", "D"]
|
|
ANSWERS_NUM = ["0", "1", "2", "4", "6", "8", "10", "12", "16", "18", "20",
|
|
"24", "25", "36", "48", "50", "64", "72", "81", "100"]
|
|
|
|
|
|
def ensure_chapter_subtopic(con, subject, chapter, subtopic):
|
|
row = con.execute("SELECT id FROM chapters WHERE subject=? AND name=?", (subject, chapter)).fetchone()
|
|
if row:
|
|
ch_id = row[0]
|
|
else:
|
|
cur = con.execute("INSERT INTO chapters(subject, name) VALUES(?, ?)", (subject, chapter))
|
|
ch_id = cur.lastrowid
|
|
row = con.execute("SELECT id FROM subtopics WHERE chapter_id=? AND name=?", (ch_id, subtopic)).fetchone()
|
|
if row:
|
|
st_id = row[0]
|
|
else:
|
|
cur = con.execute("INSERT INTO subtopics(chapter_id, name) VALUES(?, ?)", (ch_id, subtopic))
|
|
st_id = cur.lastrowid
|
|
return ch_id, st_id
|
|
|
|
|
|
def make_question(rng, subject, chapter, subtopic, qtype, difficulty, year, shift, n):
|
|
"""Return (stem, opts, answer, solution) for a synthetic question."""
|
|
tag = f"{subject[:3]}/{subtopic[:8]}/{year}{shift}"
|
|
if qtype == "MCQ":
|
|
stem = f"[{tag}] Practice question {n}: {subtopic} concept application."
|
|
opts = ("Option A", "Option B", "Option C", "Option D")
|
|
answer = rng.choice(ANSWERS_MCQ)
|
|
solution = f"Applying {subtopic} principles. Correct answer is {answer}."
|
|
else:
|
|
stem = f"[{tag}] Numerical problem {n}: Find the value related to {subtopic}."
|
|
opts = ("", "", "", "")
|
|
answer = rng.choice(ANSWERS_NUM)
|
|
solution = f"Solving numerically for {subtopic}: result = {answer}."
|
|
return stem, opts, answer, solution
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("db", help="path to the SQLite database")
|
|
ap.add_argument("--count", type=int, default=500,
|
|
help="target number of questions to add (default 500)")
|
|
args = ap.parse_args()
|
|
|
|
con = sqlite3.connect(args.db)
|
|
current = con.execute("SELECT COUNT(*) FROM questions").fetchone()[0]
|
|
print(f"current: {current} questions")
|
|
|
|
rng = random.Random(42)
|
|
added = 0
|
|
n = current + 1
|
|
|
|
# Round-robin over topics until we reach target.
|
|
while added < args.count:
|
|
for row in TOPICS:
|
|
if added >= args.count:
|
|
break
|
|
subject, chapter, subtopic, qtype, difficulty, years = row
|
|
year = rng.choice(list(years))
|
|
shift = rng.choice(SHIFTS)
|
|
ch_id, st_id = ensure_chapter_subtopic(con, subject, chapter, subtopic)
|
|
stem, opts, answer, solution = make_question(
|
|
rng, subject, chapter, subtopic, qtype, difficulty, year, shift, n)
|
|
con.execute(
|
|
"INSERT INTO questions(subject,chapter_id,subtopic_id,qtype,stem,"
|
|
"opt_a,opt_b,opt_c,opt_d,answer,difficulty,year,shift,solution) "
|
|
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(subject, ch_id, st_id, qtype, stem,
|
|
opts[0], opts[1], opts[2], opts[3],
|
|
answer, difficulty, year, shift, solution))
|
|
added += 1
|
|
n += 1
|
|
|
|
con.commit()
|
|
final = con.execute("SELECT COUNT(*) FROM questions").fetchone()[0]
|
|
chapters = con.execute("SELECT COUNT(*) FROM chapters").fetchone()[0]
|
|
subtopics = con.execute("SELECT COUNT(*) FROM subtopics").fetchone()[0]
|
|
print(f"added {added} questions → total {final}")
|
|
print(f"chapters: {chapters} subtopics: {subtopics}")
|
|
con.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|