CoolFace
Apppublic

Tman212/question_complexity_scoring

sourceHugging Faceupdated 9mo agoView on Hugging Face
1likes
app.py328 linesDownload Raw Back to root
1import os2import pandas as pd3import torch4import gradio as gr5from scipy.stats import percentileofscore6from huggingface_hub import HfApi7from transformers import AutoModelForSequenceClassification, AutoTokenizer8API        = HfApi()9REPO_ID    = "Tman212/question_complexity_scoring"  # change to your space10HISTORY_FN = "files/score_history.csv"              # track it in 'files/'11# ─── 1) Load model & tokenizer ───────────────────────────────────────────────12MODEL_DIR = "./model"13tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, local_files_only=True)14model = AutoModelForSequenceClassification.from_pretrained(15    MODEL_DIR, local_files_only=True16)17model.eval()18def reveal_history(key: str):19    """Return the history CSV path only if the entered key matches."""20    if key and key == os.environ.get("ADMIN_PASSWORD"):21        return "files/question_history.csv"22    else:23        # returning None makes Gradio show nothing / keep disabled24        return None25# Path for persistent history26HISTORY_FILE = "score_history.csv"27def persist_history():28    """Stage & commit the local CSV back into the Space repo."""29    API.upload_file(30        path_or_fileobj=HISTORY_FN,31        path_in_repo=HISTORY_FN,32        repo_id=REPO_ID,33        repo_type="space",34        token=os.environ["HF_TOKEN"],35        commit_message="🔄 update scoreboard history"36    )37# ─── Coaching helper ───────────────────────────────────────────────────────────38TIP_MAP = {39    1: "❓ To move to Understand, add “Why do you think…?”",40    2: "🔧 For Apply, ask “How would you use X in Y?”",41    3: "🔍 To reach Analyze, prompt “What are the components of…?”",42    4: "⚖️ For Evaluate, ask “What criteria would you use to judge…?”",43    5: "💡 For Create, try “Can you design or propose…?”",44    6: "🎉 You’re at Create: combine ideas for novelty!"45}46def get_coaching(rounded_scores):47    """48    Returns a single coaching tip for the *lowest* complexity question49    in the current session, so you get targeted advice on what to bump up.50    """51    if not rounded_scores:52        return ""53    # 1) Find the minimum level in this session54    min_level = min(rounded_scores)55    # 2) Look up the tip56    tip       = TIP_MAP.get(min_level, "")57    # 3) Build a little message58    html = (59        "<div class='coaching-tips'>"60        "<strong>💡 Coaching Tip:</strong> "61        f"Your lowest‐scoring question is at level {min_level}. "62        f"{tip}"63        "</div>"64    )65    return html66 67# ─── 2) Scoring logic with continuous Bloom & Scoreboard ────────────────────68def score_questions(uploaded_file, text, ignore_validation=False):69    # 1) gather70    if uploaded_file:71        df_in    = pd.read_excel(uploaded_file)72        questions = df_in["response"].astype(str).tolist()73    else:74        questions = [q.strip() for q in text.splitlines() if q.strip()]75 76    if not ignore_validation:77        valid_qs, invalid_qs = [], []78        for q in questions:79            if len(q.split()) >= 2 and q.endswith(("?", "?")):80                valid_qs.append(q)81            else:82                invalid_qs.append(q)83        if invalid_qs:84            warning_html = (85                "<div style='color:red; padding:1rem; border:1px solid #F00;'>"86                "<strong>The entries below weren’t scored because they didn’t look like full questions "87                "(≥2 words + trailing '?'):</strong><br>"88                + "".join(f"– {iq}<br>" for iq in invalid_qs)89                + "</div>"90            )91            # return early (table, slider, bloom, stats, coaching)92            return None, 1, "", warning_html, ""93 94        questions = valid_qs95    else:96        # skip validation, proceed with all questions97        questions = [q for q in questions if q.strip()]98 99    # 3) predict & score100    inputs    = tokenizer(questions, return_tensors="pt", truncation=True, padding=True)101    with torch.no_grad():102        raw_logits = model(**inputs).logits.mean(dim=1).tolist()103    cont_scores    = [((r + 3) / 6) * 5 + 1 for r in raw_logits]104    rounded_scores = [int(round(c)) for c in cont_scores]105 106    # 4) build display table107    df_out = pd.DataFrame({108        "Question": questions,109        "Complexity Score": rounded_scores110    })111 112    # 5) append to full‐history CSV113    os.makedirs("files", exist_ok=True)114    hist_path = "files/question_history.csv"115    new_hist = pd.DataFrame({116      "Question": questions,117      "Rounded Score": rounded_scores,118      "Continuous Score": cont_scores119    })120    if os.path.exists(hist_path):121        old = pd.read_csv(hist_path)122        combined = pd.concat([old, new_hist], ignore_index=True)123    else:124        combined = new_hist125    combined.to_csv(hist_path, index=False)126 127    # 6) scoreboard stats (same as you have) …128    all_hist    = combined["Continuous Score"].tolist()129    overall_avg = sum(all_hist) / len(all_hist)130    session_avg = sum(cont_scores) / len(cont_scores)131    pct         = percentileofscore(all_hist, session_avg, kind="mean")132    record_max  = max(all_hist)133    stats_html  = f"""<div style='…'>… your stats here …</div>"""134 135    # 9) Build Bloom HTML using the rounded session average136    avg_level = max(1, min(6, int(round(session_avg))))137    bloom_map = {138        1: ("Remember",  "Recall facts and basic concepts."),139        2: ("Understand","Explain ideas or concepts."),140        3: ("Apply",     "Use information in new situations."),141        4: ("Analyze",   "Draw connections among ideas."),142        5: ("Evaluate",  "Justify a stance or decision."),143        6: ("Create",    "Produce new or original work.")144    }145    name, desc = bloom_map[avg_level]146    bloom_html = (147        f"<div class='avg-container'>Way to go! Your average rounded complexity score is "148        f"<span class='avg-score'>{avg_level}</span></div>"149        "<div class='avg-container'>This score correlates with the Bloom Category:</div>"150        f"<div class='avg-container'><strong>{name}</strong>: {desc}</div><hr/>"151        "<ul class='bloom-list'>"152        + "".join(f"<li><strong>{i} – {lvl}</strong>: {d}</li>"153                  for i,(lvl,d) in bloom_map.items())154        + "</ul>"155    )156 157    # 10) Coaching HTML158    coaching_html = get_coaching(rounded_scores)159 160    # Return 6 outputs161    return df_out, avg_level, bloom_html, stats_html, coaching_html, hist_path162 163# ─── 3) Custom CSS ─────────────────────────────────────────────────────────────164custom_css = """165body, .gradio-container {166    background-color: #FFFFFF !important;167    font-family: Arial, sans-serif;168    color: #333333 !important;169}170/* Banner */171#banner_img { width:100% !important; max-height:130px!important; object-fit:contain!important; margin-bottom:1rem!important; }172#banner_img .gr-image-tools { display:none!important; }173/* Panels */174.gradio-container .input-area,175.gradio-container .output-area { background-color:#F9F9F9!important; border:1px solid #E0E0E0!important; border-radius:6px!important; padding:1rem!important; }176/* Labels */177.gradio-container label { font-weight:bold!important; }178/* Inputs */179.gradio-container textarea,180.gradio-container input[type="file"] { background-color:#FFFFFF!important; border:1px solid #CCCCCC!important; border-radius:4px!important; padding:0.5rem!important; width:100%!important; box-sizing:border-box; }181/* Button */182.gradio-container .gr-button { background-color:#A0A0A0!important; color:#FFFFFF!important; border:none!important; border-radius:4px!important; padding:0.75rem 1.5rem!important; margin-top:1rem!important; }183.gradio-container .gr-button:hover { background-color:#808080!important; }184/* Slider */185.avg-slider .gr-slider { margin-top:1rem!important; }186/* Avg text */187.avg-container { font-family:"Segoe UI",sans-serif!important; font-weight:bold!important; font-size:1.25rem!important; text-align:center!important; margin:0.5rem 0!important; }188/* Bloom list */189.bloom-list { list-style:none!important; padding-left:0!important; margin-top:0.5rem!important; }190.bloom-list li { margin:0.25rem 0!important; }191/* Citation */192.cite-title { font-size:2rem!important; font-weight:bold!important; text-align:center!important; margin-top:2rem!important; }193.cite-text  { font-size:1rem!important; color:#555555!important; text-align:center!important; max-width:800px!important; margin:0.5rem auto 2rem auto!important; }194"""195 196# ─── 4) Blocks layout ───────────────────────────────────────────────────────────197with gr.Blocks(css=custom_css) as demo:198 199    # ── HEADER: logo + title/blurb ─────────────────────────────────200    with gr.Row():201        with gr.Column(scale=1, min_width=150):202            gr.Image("banner.png", show_label=False, interactive=False, elem_id="banner_img")203        with gr.Column(scale=3):204            gr.Markdown("## PrediQT – Predicting question complexity")205            gr.Markdown(206                "The complexity scores generated using this model are based on a Large "207                "Language Model trained on thousands of human responses. The model is "208                "strongly correlated with human ratings of question complexity.\n\n"209                "PrediQT’s scoring is grounded in the hierarchical Bloom taxonomy framework, "210                "which classifies cognitive tasks from basic recall through creative synthesis. "211                "By aligning the LLM’s continuous outputs to Bloom’s six levels—Remember, Understand, "212                "Apply, Analyze, Evaluate, and Create—the app ensures that each complexity score "213                "reflects well-established educational standards."214            )215 216    # ── INPUTS (Tabs: paste vs upload) ────────────────────────────────217    with gr.Tabs():218        with gr.Tab("Paste questions"):219            text_in = gr.Textbox(220                label="One question per line",221                lines=8,222                placeholder="Why do some materials conduct electricity?\nHow would you design an experiment to test ...?"223            )224            excel = gr.File(visible=False)  # placeholder so score_questions signature stays the same225            ignore_validation = gr.Checkbox(label="Ignore validation (allow any text)", value=False)226            score_btn_paste = gr.Button("Score pasted questions", variant="primary")227 228        with gr.Tab("Upload Excel"):229            excel_up = gr.File(230                label="Upload an Excel (.xlsx) with a “response” column",231                file_types=[".xlsx"],232                type="filepath"233            )234            text_dummy = gr.Textbox(visible=False)  # placeholder so signature stays the same235            ignore_validation_up = gr.Checkbox(label="Ignore validation (allow any text)", value=False)236            score_btn_upload = gr.Button("Score uploaded file", variant="primary")237 238    # ── RESULTS ───────────────────────────────────────────────────────239    gr.Markdown("### Results")240 241    with gr.Row():242        with gr.Column(scale=2):243            df_out = gr.Dataframe(label="Questions & Complexity Scores", interactive=False)244 245            # Big dedicated download button + file output246            download_btn = gr.Button("⬇️ Download session history (CSV)", variant="primary", size="lg")247            hist_download = gr.File(248                label="",249                interactive=False,250                type="filepath"251            )252 253        with gr.Column(scale=1):254            avg_slider = gr.Slider(255                label="Average Complexity Score",256                minimum=1,257                maximum=6,258                step=1,259                interactive=False,260                elem_classes="avg-slider",261            )262            stats_box = gr.HTML()263            coaching_box = gr.HTML()264 265    # Bloom mapping (visible, no accordion)266    gr.Markdown("### Bloom mapping")267    bloom_html = gr.HTML()268 269    # Citation + privacy (visible, no accordion)270    gr.Markdown(271        "**Disclaimer:** All questions asked by users are collected **anonymously** and used only "272        "for scientific purposes. By uploading and scoring questions, you consent to your data being "273        "used for research purposes."274    )275    gr.Markdown("### Cite", elem_classes="cite-title")276    gr.Markdown(277        "Raz, T., Luchini, S. A., Beaty, R. E., & Kenett, Y. N. (2026). "278        "Automated Scoring of Question Complexity with Transformer Language Models. "279        "Thinking Skills and Creativity, 60, 102090. "280        "https://doi.org/10.1016/j.tsc.2025.102090",281        elem_classes="cite-text"282    )283 284    # ── ADMIN DOWNLOAD SECTION (accordion only here) ───────────────────285    with gr.Accordion("Admin", open=False):286        gr.Markdown("Download full question history (admin only).")287        admin_key = gr.Textbox(288            label="Admin key (password)",289            type="password",290            placeholder="Enter admin password…"291        )292        download_hist = gr.File(293            label="Download full question history",294            interactive=False,295            type="filepath"296        )297        reveal_btn = gr.Button("Reveal CSV")298 299        reveal_btn.click(300            fn=reveal_history,301            inputs=[admin_key],302            outputs=[download_hist]303        )304 305    # ── Wiring (IMPORTANT: match score_questions 6 outputs) ─────────────306    score_btn_paste.click(307        fn=score_questions,308        inputs=[excel, text_in, ignore_validation],309        outputs=[df_out, avg_slider, bloom_html, stats_box, coaching_box, hist_download]310    )311 312    score_btn_upload.click(313        fn=score_questions,314        inputs=[excel_up, text_dummy, ignore_validation_up],315        outputs=[df_out, avg_slider, bloom_html, stats_box, coaching_box, hist_download]316    )317 318    # Download button just "reveals" / focuses the file output (no extra backend logic)319    download_btn.click(320        fn=lambda p: p,321        inputs=[hist_download],322        outputs=[hist_download]323    )324 325if __name__ == "__main__":326    demo.launch(server_name="0.0.0.0")327 328