CoolFace
Apppublic

sahil-12kumar/IL_CMS_Tools

sourceHugging Faceupdated 6d agoView on Hugging Face
1likes
make_sample_docx.py216 linesDownload Raw Back to backend
1#!/usr/bin/env python32"""make_sample_docx.py — build samples/sample_questions.docx, the Word template3SMEs download and type over.4 5The file holds ONLY the four line kinds the parser needs:6 7    question  ->  options (unless numerical)  ->  answer  ->  solution8 9No headings, no title page, no "Question Type:"/"Difficulty Level:" lines — an SME10replaces the sample questions in place and uploads. Option markers may be a) b) c) d)11or 1) 2) 3) 4); both are shown, and either may be used throughout.12 13    python make_sample_docx.py            # -> samples/sample_questions.docx14    python make_sample_docx.py --out x.docx15    python make_sample_docx.py --check    # regenerate, then parse it back16 17Format rules: samples/WORD_FORMAT.md.18"""19import argparse20import io21import sys22from pathlib import Path23 24from docx import Document25from docx.shared import Inches, Pt26from lxml import etree27 28HERE = Path(__file__).resolve().parent29M_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/math'30 31 32# ── Word equations (OMML), written the way Word's equation editor writes them ──33def _r(text):34    return f'<m:r><m:t>{text}</m:t></m:r>'35 36 37def _omath(inner):38    return etree.fromstring(f'<m:oMath xmlns:m="{M_NS}">{inner}</m:oMath>')39 40 41def _sqrt(inner):42    # degHide is what makes the converter emit <msqrt> rather than an empty <mroot>43    return (f'<m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/>'44            f'<m:e>{inner}</m:e></m:rad>')45 46 47def _frac(num, den):48    return f'<m:f><m:num>{num}</m:num><m:den>{den}</m:den></m:f>'49 50 51def _sup(base, sup):52    return f'<m:sSup><m:e>{base}</m:e><m:sup>{sup}</m:sup></m:sSup>'53 54 55EQ_SQRT_2GH = _sqrt(_r('2gh'))                                        # √(2gh)56EQ_HALF_MV2 = f'{_frac(_r("1"), _r("2"))}{_r("m")}{_sup(_r("v"), _r("2"))}'   # ½mv²57EQ_G_FORMULA = _frac(f'{_r("4")}{_sup(_r("&#960;"), _r("2"))}{_r("L")}',58                     _sup(_r('T'), _r('2')))                          # 4π²L/T²59 60 61def circuit_png():62    """A small, legible figure — stands in for diagrams SMEs paste from Word."""63    from PIL import Image, ImageDraw64    im = Image.new('RGB', (420, 200), 'white')65    d = ImageDraw.Draw(im)66    d.rectangle([30, 40, 390, 160], outline='black', width=3)67    d.rectangle([150, 25, 270, 55], fill='white', outline='black', width=3)68    d.text((186, 33), 'R = 4 ohm', fill='black')69    d.line([30, 100, 8, 100], fill='black', width=3)70    d.line([8, 85, 8, 115], fill='black', width=3)71    d.line([20, 92, 20, 108], fill='black', width=5)72    d.text((36, 105), '12 V', fill='black')73    buf = io.BytesIO()74    im.save(buf, 'PNG')75    return buf.getvalue()76 77 78class Paper:79    def __init__(self):80        self.doc = Document()81        style = self.doc.styles['Normal']82        style.font.name = 'Calibri'83        style.font.size = Pt(11)84 85    def para(self, text=''):86        return self.doc.add_paragraph(text)87 88    def mixed(self, *parts):89        """A paragraph of interleaved text / ('math', omml) / ('img', bytes)."""90        p = self.doc.add_paragraph()91        for part in parts:92            if isinstance(part, str):93                p.add_run(part)94            elif part[0] == 'math':95                p._p.append(_omath(part[1]))96            elif part[0] == 'img':97                p.add_run().add_picture(io.BytesIO(part[1]), width=Inches(2.2))98        return p99 100    def gap(self):101        self.doc.add_paragraph()102 103    def save(self, path):104        Path(path).parent.mkdir(parents=True, exist_ok=True)105        self.doc.save(str(path))106 107 108def build(path):109    d = Paper()110 111    # The very first line must be a question — any other opening line would be112    # parsed as one. So the file starts here, with no title and no headings.113 114    # ── Q1 — options lettered a) b) c) d), equation in the stem ──────────────115    d.mixed('1. A body is released from rest at a height h. Its speed on reaching the '116            'ground is v = ', ('math', EQ_SQRT_2GH),117            '. Find the speed after falling 20 m. (Take g = 10 m/s²)')118    d.para('a) 10 m/s')119    d.para('b) 20 m/s')120    d.para('c) 30 m/s')121    d.para('d) 40 m/s')122    d.para('Answer: b')123    d.mixed('Using v = ', ('math', EQ_SQRT_2GH), ' with h = 20 m and g = 10 m/s², '124            'v = square root of (2 × 10 × 20) = square root of 400 = 20 m/s.')125    d.para('Hence option b is correct.')126    d.gap()127 128    # ── Q2 — options numbered 1) 2) 3) 4) on one line, with a figure ─────────129    d.para('2. For the circuit shown below, find the current through the resistor R.')130    d.mixed(('img', circuit_png()))131    d.para('1) 1 A   2) 2 A   3) 3 A   4) 4 A')132    d.para('Answer: 3')133    d.para("By Ohm's law, I = V / R = 12 / 4 = 3 A.")134    d.para('Hence option 3 is correct.')135    d.gap()136 137    # ── Q3 — numerical: NO options at all ───────────────────────────────────138    d.mixed('3. A simple pendulum of length L = 1 m has a time period of 2 s. '139            'Using g = ', ('math', EQ_G_FORMULA), ', find g in m/s².')140    d.para('Answer: 9.87')141    d.mixed('Substituting L = 1 m and T = 2 s into g = ', ('math', EQ_G_FORMULA),142            ' gives g = 4 × 9.8696 × 1 / 4.')143    d.para('So g = 9.8696 ≈ 9.87 m/s².')144    d.gap()145 146    # ── Q4 — numerical with an accepted band (hyphen form is the one that parses) ──147    d.para('4. A 2 kg block moving at 3 m/s is brought to rest by friction over 1.5 m. '148           'Find the coefficient of friction. (g = 10 m/s²)')149    d.para('Answer: 0.28-0.32')150    d.mixed('By the work-energy theorem ', ('math', EQ_HALF_MV2),151            ' = μmgs, so μ = v² / (2gs).')152    d.para('μ = 9 / (2 × 10 × 1.5) = 0.30, so any value from 0.28 to 0.32 is accepted.')153    d.gap()154 155    # ── Q5 — options lettered (A) (B) (C) (D), equations as the options ──────156    d.para('5. Which expression gives the kinetic energy of a body of mass m moving '157           'with speed v?')158    d.mixed('(A) ', ('math', _r('mgh')))159    d.mixed('(B) ', ('math', EQ_HALF_MV2))160    d.mixed('(C) ', ('math', EQ_SQRT_2GH))161    d.mixed('(D) ', ('math', _r('mv')))162    d.para('Answer: B')163    d.mixed('Kinetic energy is ', ('math', EQ_HALF_MV2),164            '. Option A is potential energy and the others are not energies at all.')165    d.para('Hence option B is correct.')166 167    d.save(path)168    return path169 170 171def check(path):172    """Parse the generated file back and report what the uploader would show."""173    sys.path.insert(0, str(HERE))174    import app175 176    qs = app.parse_word(str(path))177    app.validate_questions(qs)178    print(f'\nparsed {len(qs)} question(s) from {Path(path).name}')179    warn = app.omml_support_warning()180    if warn:181        print(f'WARNING: {warn}')182    ok = True183    for i, q in enumerate(qs, 1):184        opts = [q.get(f'option_{k}', '') for k in 'abcd']185        filled = sum(1 for o in opts if app._option_filled(o))186        maths = sum(o.count('<math') for o in opts) + q['question_text'].count('<math')187        imgs = q['question_text'].count('<img')188        band = f' band={q["answer_range"]}' if q.get('answer_range') else ''189        print(f'  Q{i}: type={app.normalise_qtype(q["question_type"]):<24} '190              f'ans={q["correct_answer"]!r:<12} opts={filled} math={maths} img={imgs} '191              f'sol={"yes" if q["solution"].strip() else "NO":<3} '192              f'{q["confidence"]}{band}')193        if q['issues']:194            print(f'       issues: {q["issues"]}')195        if q['confidence'] != 'ok' or not q['solution'].strip():196            ok = False197    print('\nEvery question parsed cleanly with an answer and a solution.' if ok198          else '\nSome questions did NOT parse cleanly — see above.')199    return ok200 201 202def main():203    ap = argparse.ArgumentParser(description='Build the Word sample paper')204    ap.add_argument('--out', default=str(HERE.parent / 'samples' / 'sample_questions.docx'))205    ap.add_argument('--check', action='store_true',206                    help='parse the generated file back and report')207    args = ap.parse_args()208    path = build(args.out)209    print(f'wrote {path}')210    if args.check and not check(path):211        raise SystemExit(1)212 213 214if __name__ == '__main__':215    main()216