Tman212/question_complexity_scoring
1
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 