CoolFace
Apppublic

ctizzzy0/COLLEGE

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py816 linesDownload Raw Back to root
1 2# CollegeGenius: Ultimate College Apps Copilot (HF Spaces ready)3# ---------------------------------------------------------------4# Features (all-in-one, ~"holy grail" for college apps):5# - Essay Coach: structure/readability analysis, passive voice, cliché detector, thesis finder, originality heuristic, rubric scoring, outline helper6# - Resume/Activity Analyzer: bullet quality, action verbs, quantified impact check, STAR pattern hints, gap finder7# - College Matcher: reach/target/safety based on GPA/test scores, interests, location; tiny builtin dataset + CSV import8# - Spike Builder: turn interests into standout projects with timelines, tasks; export ICS calendar9# - Interview Practice: randomized behavioral & major-specific questions, WPM tracker, sentiment heuristics10# - OCR & Typed PDF: import essays/resumes from images/PDF; basic fallback if OCR libs missing11# - Portfolio Report: generate a polished PDF of insights, charts, and next-steps12# - Privacy-first: runs local in the Space; exports logs/report to your account storage13#14# Made to be "insanely impressive" for demos and practical for real use.15#16# -----------------------------17# Requirements (add to Spaces)18# -----------------------------19# gradio20# numpy21# pandas22# matplotlib23# fpdf24# pypdf25# sentence-transformers26# scikit-learn27# textstat28# easyocr (optional; will gracefully degrade if unavailable)29#30# -----------------------------31# MIT License (trimmed)32# -----------------------------33# Copyright (c) 2025 CollegeGenius34# Permission is hereby granted, free of charge, to any person obtaining a copy35# of this software and associated documentation files...36 37import os38import re39import io40import json41import math42import time43import uuid44import base6445import random46import string47import textwrap48import datetime as dt49from collections import Counter, defaultdict50 51import numpy as np52import pandas as pd53import matplotlib.pyplot as plt54 55import gradio as gr56from fpdf import FPDF57 58# PDFs59from pypdf import PdfReader60 61# NLP (semantic similarity & TF-IDF)62from sklearn.feature_extraction.text import TfidfVectorizer63from sklearn.metrics.pairwise import cosine_similarity64 65# Readability66try:67    import textstat68except Exception:69    textstat = None70 71# Embeddings72_EMBEDDER = None73try:74    from sentence_transformers import SentenceTransformer75    _EMBEDDER = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')76except Exception:77    _EMBEDDER = None78 79# OCR (optional)80_OCR = None81try:82    import easyocr83    _OCR = easyocr.Reader(['en'], gpu=False)84except Exception:85    _OCR = None86 87APP_ID = "college_genius"88DATA_DIR = "cg_data"89os.makedirs(DATA_DIR, exist_ok=True)90 91LOG_CSV = os.path.join(DATA_DIR, "runs.csv")92if not os.path.exists(LOG_CSV):93    pd.DataFrame(columns=[94        "timestamp","user_id","tool","score","meta"95    ]).to_csv(LOG_CSV, index=False)96 97ESSAY_DB = os.path.join(DATA_DIR, "essays.jsonl")98if not os.path.exists(ESSAY_DB):99    open(ESSAY_DB, "w").close()100 101RESUME_DB = os.path.join(DATA_DIR, "resumes.jsonl")102if not os.path.exists(RESUME_DB):103    open(RESUME_DB, "w").close()104 105# ----------------------------106# Tiny demo college dataset107# ----------------------------108_BUILTIN_COLLEGES = pd.DataFrame([109    # name, state, avg_gpa, sat_mid, act_mid, acceptance_rate, stem_strength(1-5), humanities_strength(1-5)110    ("Massachusetts Institute of Technology", "MA", 3.9, 1550, 35, 0.04, 5, 4),111    ("Stanford University", "CA", 3.95, 1540, 35, 0.04, 5, 5),112    ("Harvard University", "MA", 3.95, 1540, 35, 0.04, 5, 5),113    ("University of California, Berkeley", "CA", 3.85, 1470, 33, 0.15, 5, 4),114    ("University of Michigan, Ann Arbor", "MI", 3.85, 1470, 33, 0.20, 5, 4),115    ("Georgia Tech", "GA", 3.85, 1480, 34, 0.17, 5, 3),116    ("University of Texas at Austin", "TX", 3.8, 1420, 32, 0.31, 4, 4),117    ("Carnegie Mellon University", "PA", 3.9, 1530, 35, 0.14, 5, 4),118    ("Princeton University", "NJ", 3.95, 1540, 35, 0.05, 5, 5),119    ("Yale University", "CT", 3.95, 1540, 35, 0.05, 5, 5),120    ("Brown University", "RI", 3.9, 1510, 34, 0.06, 4, 5),121    ("Duke University", "NC", 3.9, 1520, 34, 0.07, 5, 5),122    ("Northwestern University", "IL", 3.9, 1500, 34, 0.07, 4, 5),123    ("New York University", "NY", 3.7, 1450, 33, 0.13, 4, 5),124    ("University of Florida", "FL", 3.8, 1400, 31, 0.31, 4, 3),125    ("Purdue University", "IN", 3.7, 1380, 30, 0.53, 4, 3),126    ("Virginia Tech", "VA", 3.7, 1360, 30, 0.57, 4, 3),127    ("University of Washington", "WA", 3.75, 1410, 32, 0.48, 4, 4),128    ("UIUC", "IL", 3.75, 1420, 32, 0.45, 5, 3),129    ("UCLA", "CA", 3.9, 1500, 34, 0.09, 4, 5)130], columns=["name","state","avg_gpa","sat_mid","act_mid","acceptance_rate","stem_strength","humanities_strength"])131 132# ----------------------------133# Heuristics / lexicons134# ----------------------------135C_LICHES = [136    "ever since I was a child", "since the dawn of time", "follow my dreams",137    "changed my life forever", "made me who I am today", "I have always loved",138    "I learned the true meaning", "I realized that anything is possible",139    "the importance of hard work", "the power of perseverance"140]141 142ACTION_VERBS = [143    "led","built","created","launched","founded","organized","architected",144    "automated","designed","initiated","streamlined","optimized","developed",145    "engineered","authored","deployed","scaled","improved","won","achieved",146    "delivered","analyzed","spearheaded","directed","mentored","taught",147    "coordinated","presented","implemented","transformed","secured"148]149 150MAJOR_QUESTIONS = {151    "Computer Science": [152        "Tell me about a time you debugged a difficult problem.",153        "What's a software project you're proud of and why?",154        "Explain an algorithm you like to a non-technical audience."155    ],156    "Business": [157        "Describe a time you influenced someone without authority.",158        "How did you analyze data to drive a decision?",159        "Pitch a product for college students."160    ],161    "Biology": [162        "What experiment taught you the most?",163        "How do you evaluate the quality of a scientific source?",164        "Explain CRISPR at a high level."165    ],166    "Humanities": [167        "How has a book changed how you see the world?",168        "Tell me about a debate that sharpened your thinking.",169        "What does good writing mean to you?"170    ]171}172 173SPIKE_IDEAS = {174    "AI/ML": [175        "Research sprint: build a model for local nonprofit (e.g., demand forecasting).",176        "Open-source contribution: new dataset or evaluation harness.",177        "Content series: write 8-part blog on ML intuition for teens."178    ],179    "Finance": [180        "Run an investing club with monthly backtests and public notes.",181        "Publish a 'teen finance' newsletter with original surveys.",182        "Build a budgeting app prototype for students."183    ],184    "Biotech": [185        "Wet-lab collaboration with a local university on a safe assay.",186        "Bioinformatics pipeline for public dataset; publish a preprint.",187        "Host a bioethics roundtable and publish proceedings."188    ]189}190 191# ----------------------------192# Utilities193# ----------------------------194def _now():195    return dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")196 197def _clean(text: str) -> str:198    if not isinstance(text, str):199        return ""200    return re.sub(r"\s+", " ", text).strip()201 202def _sentences(text: str):203    # Very light sentence splitter204    text = _clean(text)205    if not text:206        return []207    sents = re.split(r"(?<=[.!?])\s+", text)208    return [s for s in sents if s]209 210def _ngrams(words, n=2):211    return [tuple(words[i:i+n]) for i in range(len(words)-n+1)]212 213def _word_count(text: str):214    return len(re.findall(r"[A-Za-z0-9']+", text))215 216def _passive_voice_score(text: str):217    # heuristic: "was|were|is|are|be" + past participle ending with "ed"218    matches = re.findall(r"\b(was|were|is|are|be|been|being)\b\s+\w+ed\b", text, flags=re.I)219    return len(matches)220 221def _readability(text: str):222    if not textstat:223        return {"flesch_reading_ease": None, "smog_index": None, "grade_level": None}224    try:225        return {226            "flesch_reading_ease": round(textstat.flesch_reading_ease(text), 2),227            "smog_index": round(textstat.smog_index(text), 2),228            "grade_level": round(textstat.text_standard(text, float_output=True), 2)229        }230    except Exception:231        return {"flesch_reading_ease": None, "smog_index": None, "grade_level": None}232 233def _embed(texts):234    if not _EMBEDDER:235        # fallback: TF-IDF vectors236        vec = TfidfVectorizer(stop_words="english", max_features=2048)237        X = vec.fit_transform(texts).toarray().astype(np.float32)238        # L2 norm239        X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-9)240        return X241    E = _EMBEDDER.encode(texts, normalize_embeddings=True)242    return np.array(E)243 244def _cosine(a, b):245    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))246 247def _save_log(tool, score, meta):248    df = pd.read_csv(LOG_CSV)249    row = {250        "timestamp": _now(),251        "user_id": "anon",252        "tool": tool,253        "score": score if score is not None else "",254        "meta": json.dumps(meta)[:1000]255    }256    df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)257    df.to_csv(LOG_CSV, index=False)258 259def _extract_text_from_pdf(path: str) -> str:260    try:261        reader_pdf = PdfReader(path)262        pages = []263        for p in reader_pdf.pages:264            pages.append(p.extract_text() or "")265        return "\n".join(pages)266    except Exception:267        return ""268 269def _ocr_any(file) -> str:270    if not file:271        return ""272    path = file if isinstance(file, str) else file.name273    if path.lower().endswith(".pdf"):274        t = _extract_text_from_pdf(path)275        if t.strip():276            return _clean(t)277        if _OCR:278            # attempt OCR on pages converted to images is non-trivial without poppler;279            # instruct user to upload as image280            return "PDF looks scanned/handwritten. Please upload a clear image (JPG/PNG) for OCR."281        return "Could not read PDF. Try exporting as text PDF or image."282    else:283        if _OCR:284            result = _OCR.readtext(path, detail=0, paragraph=True)285            return _clean(" ".join(result))286        return "OCR not available on this Space. Add `easyocr` to requirements or paste text manually."287 288# ----------------------------289# Essay Coach290# ----------------------------291def find_thesis(text: str):292    # naive: look for sentences with causal markers or stance293    candidates = []294    markers = ["because", "therefore", "although", "however", "should", "must", "argue", "believe"]295    for s in _sentences(text):296        lower = s.lower()297        if any(m in lower for m in markers) and 8 < _word_count(s) < 60:298            candidates.append(s)299    if not candidates:300        # pick the longest sentence as a fallback301        sents = sorted(_sentences(text), key=len, reverse=True)302        return sents[0] if sents else ""303    # choose the one with fewest passive patterns304    scored = sorted(candidates, key=lambda s: _passive_voice_score(s))305    return scored[0]306 307def cliché_hits(text: str):308    hits = []309    low = text.lower()310    for c in C_LICHES:311        if c in low:312            hits.append(c)313    return hits314 315def originality_heuristic(new_text: str):316    # compare against user's own past essays for self-plagiarism and repetition317    docs = []318    try:319        with open(ESSAY_DB, "r") as f:320            for line in f:321                try:322                    docs.append(json.loads(line)["text"])323                except Exception:324                    pass325    except Exception:326        pass327    if not docs:328        return {"max_self_similarity": 0.0, "similar_sample": ""}329    embs = _embed([_clean(new_text)] + [_clean(d) for d in docs])330    sims = cosine_similarity([embs[0]], embs[1:])[0].tolist()331    if not sims:332        return {"max_self_similarity": 0.0, "similar_sample": ""}333    k = int(np.argmax(sims))334    return {"max_self_similarity": round(float(sims[k]), 3), "similar_sample": docs[k][:300]}335 336def essay_rubric(text: str, prompt: str = ""):337    # scores 0-100 via weighted heuristics338    w = {339        "structure": 0.25,340        "clarity": 0.25,341        "style": 0.2,342        "originality": 0.15,343        "mechanics": 0.15344    }345    sents = _sentences(text)346    wc = _word_count(text)347    thesis = find_thesis(text)348    passive = _passive_voice_score(text)349    clichés = cliché_hits(text)350 351    # structure: parag lengths and presence of intro/body/conc heuristics352    paragraphs = [p for p in re.split(r"\n{2,}", text) if _clean(p)]353    avg_par = np.mean([_word_count(p) for p in paragraphs]) if paragraphs else 0354    structure_score = 60355    if 500 <= wc <= 800:356        structure_score += 10357    if 5 <= len(sents) <= 25:358        structure_score += 10359    if 80 <= avg_par <= 180:360        structure_score += 10361    if thesis:362        structure_score += 10363    structure_score = min(100, structure_score)364 365    # clarity: readability & sentence length variance366    read = _readability(text)367    clarity_score = 60368    if read["grade_level"] and 8 <= read["grade_level"] <= 12:369        clarity_score += 15370    if read["flesch_reading_ease"] and read["flesch_reading_ease"] >= 50:371        clarity_score += 10372    if np.std([_word_count(s) for s in sents]) if sents else 0 >= 5:373        clarity_score += 5374    clarity_score = min(100, clarity_score)375 376    # style: passive voice penalties + cliché penalties377    style_score = 85 - min(25, 2*passive) - 5*len(clichés)378    style_score = max(40, min(100, style_score))379 380    # originality: against own corpus381    orig = originality_heuristic(text)382    originality_score = int(100 - 100*orig["max_self_similarity"])383    originality_score = max(30, min(100, originality_score))384 385    # mechanics: naive based on punctuation balance & capitalization386    caps_issues = len(re.findall(r"\bi\b", text))  # lowercase I387    many_exclaims = text.count("!!") + text.count("???")388    mechanics_score = 90 - 5*caps_issues - 3*many_exclaims389    mechanics_score = max(40, min(100, mechanics_score))390 391    total = int(round(392        w["structure"]*structure_score +393        w["clarity"]*clarity_score +394        w["style"]*style_score +395        w["originality"]*originality_score +396        w["mechanics"]*mechanics_score397    ))398 399    feedback = {400        "structure_score": structure_score,401        "clarity_score": clarity_score,402        "style_score": style_score,403        "originality_score": originality_score,404        "mechanics_score": mechanics_score,405        "thesis_guess": thesis,406        "cliche_hits": clichés,407        "readability": read,408        "word_count": wc409    }410    _save_log("essay_coach", total, feedback)411    # persist essay412    try:413        with open(ESSAY_DB, "a") as f:414            f.write(json.dumps({"timestamp": _now(), "text": text, "prompt": prompt})+"\n")415    except Exception:416        pass417    return total, feedback418 419def outline_helper(prompt: str, interests: str, max_sections: int = 6):420    base = [421        "Hook (specific image or moment)",422        "Context (what's at stake, why it mattered)",423        "Catalyst (decision or conflict)",424        "Deepening (what you tried, obstacles, learning)",425        "Turning point (insight/change)",426        "Forward path (how this shapes what you'll do in college)"427    ]428    sections = base[:max_sections]429    # sprinkle interests430    if interests:431        for i in range(len(sections)):432            sections[i] += f" — weave in {interests}"433    return sections434 435# ----------------------------436# Resume / Activity Analyzer437# ----------------------------438def bullet_quality_metrics(bullet: str):439    # action verb, numbers, outcome, passive440    action = any(bullet.lower().startswith(v) for v in ACTION_VERBS)441    nums = bool(re.search(r"\d", bullet))442    outcome = bool(re.search(r"(result|outcome|impact|increased|decreased|grew|reduced|saved|won|revenue|users)", bullet, flags=re.I))443    passive = _passive_voice_score(bullet) > 0444    length_ok = 8 <= _word_count(bullet) <= 30445    score = 60 + 10*action + 10*nums + 10*outcome - 10*passive + 5*length_ok446    score = max(30, min(100, score))447    tags = []448    if action: tags.append("Action verb")449    if nums: tags.append("Quantified")450    if outcome: tags.append("Outcome")451    if passive: tags.append("Passive")452    if length_ok: tags.append("Concise")453    return score, tags454 455def analyze_resume(text: str):456    bullets = [b.strip("-• ") for b in text.split("\n") if b.strip()]457    rows = []458    for b in bullets:459        s, tags = bullet_quality_metrics(b)460        rows.append([b, s, ", ".join(tags)])461    avg = int(np.mean([r[1] for r in rows])) if rows else 0462    _save_log("resume_analyzer", avg, {"n_bullets": len(rows)})463    return rows, avg464 465def activities_gap_finder(activities_text: str):466    # simple coverage by STEM/humanities/leadership/service/creative467    axes = {"STEM":0, "Humanities":0, "Leadership":0, "Service":0, "Creative":0}468    low = activities_text.lower()469    axes["STEM"] += len(re.findall(r"(robot|code|math|science|hack|lab|research)", low))470    axes["Humanities"] += len(re.findall(r"(writing|debate|model un|history|philosophy|language)", low))471    axes["Leadership"] += len(re.findall(r"(president|captain|lead|founded|organized)", low))472    axes["Service"] += len(re.findall(r"(volunteer|tutor|nonprofit|soup kitchen|fundraiser)", low))473    axes["Creative"] += len(re.findall(r"(music|art|design|film|theater|dance)", low))474    return axes475 476# ----------------------------477# College Matcher478# ----------------------------479def match_colleges(gpa, sat, act, interest="STEM", state_pref=""):480    df = _BUILTIN_COLLEGES.copy()481    # score by GPA/test vs school midpoint and interest strength482    if sat and sat > 0:483        df["test_gap"] = (sat - df["sat_mid"])/200.0484    elif act and act > 0:485        df["test_gap"] = (act - df["act_mid"])/4.0486    else:487        df["test_gap"] = 0.0488    df["gpa_gap"] = (gpa - df["avg_gpa"])489    if interest.lower() in ["stem","engineering","cs","math","physics"]:490        df["fit"] = 0.5*df["gpa_gap"] + 0.5*df["test_gap"] + 0.3*(df["stem_strength"]/5.0)491    else:492        df["fit"] = 0.5*df["gpa_gap"] + 0.5*df["test_gap"] + 0.3*(df["humanities_strength"]/5.0)493    if state_pref:494        df.loc[df["state"].str.lower()==state_pref.lower(), "fit"] += 0.15495    # tiers496    tiers = []497    for _, r in df.iterrows():498        if r["fit"] >= 0.3:499            tiers.append("Target")500        elif r["fit"] >= 0.1:501            tiers.append("Reach")502        else:503            tiers.append("High Reach")504    df["tier"] = tiers505    df = df.sort_values("fit", ascending=False)506    cols = ["name","state","tier","fit","avg_gpa","sat_mid","act_mid","acceptance_rate"]507    return df[cols].head(12)508 509# ----------------------------510# Spike Builder & ICS Export511# ----------------------------512def build_spike(interest: str, weeks: int = 8):513    ideas = SPIKE_IDEAS.get(interest, SPIKE_IDEAS[random.choice(list(SPIKE_IDEAS.keys()))])514    # timeline: weekly milestones515    milestones = []516    for w in range(weeks):517        milestones.append({518            "week": w+1,519            "goal": f"Progress on {interest} project – {ideas[w%len(ideas)]}",520            "deliverable": f"Week {w+1} demo/post/writeup"521        })522    return ideas, milestones523 524def export_ics(title: str, milestones):525    # Minimal ICS builder526    now = dt.datetime.now()527    lines = ["BEGIN:VCALENDAR","VERSION:2.0","PRODID:-//CollegeGenius//EN"]528    for m in milestones:529        start = now + dt.timedelta(weeks=m["week"]-1)530        dtstamp = now.strftime("%Y%m%dT%H%M%SZ")531        dtstart = start.strftime("%Y%m%d")532        uid = str(uuid.uuid4()) + "@collegegenius"533        lines += [534            "BEGIN:VEVENT",535            f"UID:{uid}",536            f"DTSTAMP:{dtstamp}",537            f"DTSTART;VALUE=DATE:{dtstart}",538            f"SUMMARY:{title} - Week {m['week']}: {m['deliverable']}",539            f"DESCRIPTION:{m['goal']}",540            "END:VEVENT"541        ]542    lines.append("END:VCALENDAR")543    ics_path = os.path.join(DATA_DIR, "spike_plan.ics")544    with open(ics_path, "w") as f:545        f.write("\n".join(lines))546    return ics_path547 548# ----------------------------549# Interview Practice550# ----------------------------551def interview_question(major: str, behavioral: bool):552    if behavioral:553        qs = [554            "Tell me about a time you failed and what you learned.",555            "Describe a conflict you resolved.",556            "What's a project that best represents you and why?",557            "When did you change your mind about something important?"558        ]559    else:560        qs = MAJOR_QUESTIONS.get(major, MAJOR_QUESTIONS["Humanities"])561    return random.choice(qs)562 563def answer_metrics(answer: str, seconds: int = 90):564    wc = _word_count(answer)565    wpm = int(60*wc/max(1, seconds))566    pos = len(re.findall(r"\b(learn|excited|grateful|curious|proud|growth|impact|collaborate)\b", answer.lower()))567    neg = len(re.findall(r"\b(hate|can't|never|worst|failure)\b", answer.lower()))568    sentiment = "Positive" if pos >= neg else "Neutral" if pos == neg else "Cautious"569    return {"wpm_estimate": wpm, "sentiment": sentiment, "word_count": wc}570 571# ----------------------------572# Portfolio PDF573# ----------------------------574def portfolio_pdf(essay_result, resume_rows, college_df, spike_title, milestones, interview_summary):575    pdf = FPDF()576    pdf.add_page()577    pdf.set_font("Arial", "B", 16)578    pdf.cell(0, 10, "CollegeGenius – Application Portfolio", ln=True)579 580    # Essay581    pdf.set_font("Arial", "", 12)582    pdf.cell(0, 8, "Essay Coach Summary", ln=True)583    for k,v in essay_result.items():584        if isinstance(v, dict): continue585        pdf.cell(0, 6, f"- {k.replace('_',' ').title()}: {v}", ln=True)586 587    # Resume bullets588    pdf.ln(4)589    pdf.set_font("Arial", "", 12)590    pdf.cell(0, 8, "Resume Bullet Quality (top 5)", ln=True)591    for row in resume_rows[:5]:592        bullet, score, tags = row593        pdf.multi_cell(0, 6, f"• [{score}] {bullet} ({tags})")594 595    # Colleges596    pdf.ln(4)597    pdf.set_font("Arial", "", 12)598    pdf.cell(0, 8, "Top College Matches", ln=True)599    for _, r in college_df.head(6).iterrows():600        pdf.cell(0, 6, f"- {r['name']} ({r['tier']}) | Fit={round(r['fit'],3)} | SAT~{r['sat_mid']} | GPA~{r['avg_gpa']}", ln=True)601 602    # Spike603    pdf.ln(4)604    pdf.set_font("Arial", "", 12)605    pdf.cell(0, 8, f"Spike Plan: {spike_title}", ln=True)606    for m in milestones[:6]:607        pdf.cell(0, 6, f"Week {m['week']}: {m['deliverable']} – {m['goal']}", ln=True)608 609    # Interview610    pdf.ln(4)611    pdf.cell(0, 8, "Interview Practice Snapshot", ln=True)612    for k,v in interview_summary.items():613        pdf.cell(0, 6, f"- {k.replace('_',' ').title()}: {v}", ln=True)614 615    out = os.path.join(DATA_DIR, "portfolio.pdf")616    pdf.output(out)617    return out618 619# ----------------------------620# Visualization helpers621# ----------------------------622def bar_image(labels, values, title=""):623    fig, ax = plt.subplots()624    ax.bar(labels, values)625    ax.set_title(title)626    ax.set_ylim(0, max(1, max(values)))627    buf = io.BytesIO()628    plt.tight_layout()629    plt.savefig(buf, format="png")630    plt.close(fig)631    buf.seek(0)632    return buf633 634def radar_like_image(d, title=""):635    labels = list(d.keys())636    values = [d[k] for k in labels]637    return bar_image(labels, values, title)638 639# ----------------------------640# UI Pipelines641# ----------------------------642def essay_coach_pipeline(prompt, essay_text, upload):643    src = essay_text.strip()644    if upload is not None and not src:645        src = _ocr_any(upload)646    if not src.strip():647        return "No essay text detected. Paste text or upload a clear image/PDF.", None, None, None648    score, fb = essay_rubric(src, prompt)649    # visuals650    dims = {651        "Structure": fb["structure_score"],652        "Clarity": fb["clarity_score"],653        "Style": fb["style_score"],654        "Originality": fb["originality_score"],655        "Mechanics": fb["mechanics_score"]656    }657    plot_buf = radar_like_image(dims, title=f"Essay Score: {score}")658    # thesis / outline659    outline = outline_helper(prompt, interests="your central interests", max_sections=6)660    md = f"### Essay Score: **{score}/100**\n"661    md += f"- Thesis guess: `{fb['thesis_guess']}`\n"662    if fb["cliche_hits"]:663        md += f"- Clichés to trim: `{', '.join(fb['cliche_hits'])}`\n"664    md += f"- Readability: {fb['readability']}\n"665    md += f"- Word count: {fb['word_count']}\n"666    md += "\n**Pro Outline:**\n" + "\n".join([f"{i+1}. {s}" for i,s in enumerate(outline)])667    # originality details668    orig = originality_heuristic(src)669    md += f"\n\n**Self-similarity (to your saved drafts):** {orig['max_self_similarity']}"670    if orig["similar_sample"]:671        md += f"\nExample similar snippet: `{orig['similar_sample']}`"672    return md, gr.update(value=plot_buf, visible=True), src[:1000], json.dumps(fb, indent=2)[:2000]673 674def resume_pipeline(resume_text, upload):675    src = resume_text.strip()676    if upload is not None and not src:677        src = _ocr_any(upload)678    rows, avg = analyze_resume(src)679    df = pd.DataFrame(rows, columns=["Bullet","Score","Tags"])680    # plot distribution681    if rows:682        vals = [r[1] for r in rows]683        buf = bar_image(["Avg","Min","Max"], [int(np.mean(vals)), min(vals), max(vals)], title="Resume Bullet Quality")684    else:685        buf = None686    md = f"### Resume Analysis\n- Bullets: **{len(rows)}**\n- Average quality: **{avg}**/100"687    return df, gr.update(value=buf, visible=buf is not None), md688 689def college_match_pipeline(gpa, sat, act, interest, state_pref):690    gpa = float(gpa) if gpa else 0.0691    sat = int(sat) if sat else 0692    act = int(act) if act else 0693    df = match_colleges(gpa, sat, act, interest, state_pref)694    # plot top-5 fits695    top = df.head(5)696    buf = bar_image(top["name"].tolist(), [round(x,3) for x in top["fit"].tolist()], title="Top Fit Scores")697    return df, gr.update(value=buf, visible=True)698 699def spike_pipeline(interest, weeks, title_hint):700    ideas, milestones = build_spike(interest, weeks)701    title = title_hint.strip() or f"{interest} Spike"702    ics_path = export_ics(title, milestones)703    md = "### Spike Plan\n" + "\n".join([f"- Week {m['week']}: {m['deliverable']} — {m['goal']}" for m in milestones])704    return md, ideas, ics_path705 706def interview_pipeline(major, behavioral, answer_text):707    q = interview_question(major, behavioral)708    m = answer_metrics(answer_text or "", seconds=90)709    md = f"### Your Answer Metrics\n- WPM (est): **{m['wpm_estimate']}**\n- Sentiment: **{m['sentiment']}**\n- Word count: **{m['word_count']}**"710    return q, md711 712def portfolio_pipeline(essay_fb_json, resume_df, college_df, spike_title, milestones_json):713    try:714        essay_fb = json.loads(essay_fb_json)715    except Exception:716        essay_fb = {}717    try:718        rows = resume_df.values.tolist() if hasattr(resume_df, "values") else []719    except Exception:720        rows = []721    try:722        colleges = college_df if isinstance(college_df, pd.DataFrame) else _BUILTIN_COLLEGES.head(5)723        if not isinstance(colleges, pd.DataFrame):724            colleges = pd.DataFrame(colleges)725    except Exception:726        colleges = _BUILTIN_COLLEGES.head(5)727    try:728        milestones = json.loads(milestones_json)729    except Exception:730        milestones = [{"week":1,"deliverable":"Kickoff","goal":"Start"}]731 732    pdf_path = portfolio_pdf(essay_fb, rows, colleges, spike_title or "Spike", milestones, {"note":"Auto-generated"})733    return pdf_path734 735# ----------------------------736# Gradio UI737# ----------------------------738with gr.Blocks(title="CollegeGenius – Ultimate College Apps Copilot", theme=gr.themes.Soft()) as demo:739    gr.Markdown("# 🎓 CollegeGenius – Ultimate College Apps Copilot\nAll-in-one Space to **crush your college apps**: essays, activities, college list, spike, interview, and a clean portfolio PDF.")740    with gr.Tab("📝 Essay Coach"):741        with gr.Row():742            prompt = gr.Textbox(label="Prompt (optional)", lines=2, placeholder="E.g., 'Tell us about a time you challenged a belief...'")743            essay_text = gr.Textbox(label="Paste your essay draft (or upload below)", lines=14)744        upload_essay = gr.File(label="Upload essay image/PDF (OCR)", file_count="single")745        btn_essay = gr.Button("Analyze Essay", variant="primary")746        essay_md = gr.Markdown()747        essay_plot = gr.Image(label="Essay Score Breakdown", visible=False)748        essay_excerpt = gr.Textbox(label="First 1000 chars (for your records)", lines=6)749        essay_fb_json = gr.Textbox(label="Raw feedback JSON", lines=8)750        btn_essay.click(essay_coach_pipeline, inputs=[prompt, essay_text, upload_essay],751                        outputs=[essay_md, essay_plot, essay_excerpt, essay_fb_json])752 753    with gr.Tab("📄 Resume & Activities"):754        with gr.Row():755            resume_text = gr.Textbox(label="Paste resume / activities section", lines=12)756            upload_resume = gr.File(label="Upload resume image/PDF (OCR)", file_count="single")757        btn_resume = gr.Button("Analyze Resume/Activities", variant="primary")758        resume_df = gr.Dataframe(headers=["Bullet","Score","Tags"], interactive=False, wrap=True)759        resume_plot = gr.Image(label="Quality Snapshot", visible=False)760        resume_md = gr.Markdown()761        btn_resume.click(resume_pipeline, inputs=[resume_text, upload_resume],762                         outputs=[resume_df, resume_plot, resume_md])763 764    with gr.Tab("🏫 College Matcher"):765        with gr.Row():766            gpa = gr.Textbox(label="Unweighted GPA (e.g., 3.8)")767            sat = gr.Textbox(label="SAT (1600 scale)")768            act = gr.Textbox(label="ACT (36 scale)")769        with gr.Row():770            interest = gr.Dropdown(choices=["STEM","Humanities","Business","Biology","CS","Engineering"], value="STEM", label="Primary Interest")771            state_pref = gr.Textbox(label="State preference (optional, e.g., CA)")772        btn_match = gr.Button("Find Matches", variant="primary")773        college_df = gr.Dataframe(interactive=False, wrap=True)774        college_plot = gr.Image(label="Top Fit Scores", visible=False)775        btn_match.click(college_match_pipeline, inputs=[gpa, sat, act, interest, state_pref],776                        outputs=[college_df, college_plot])777 778    with gr.Tab("🚀 Spike Builder + Calendar"):779        with gr.Row():780            spike_interest = gr.Dropdown(choices=list(SPIKE_IDEAS.keys()), value="AI/ML", label="Interest Area")781            weeks = gr.Slider(4, 16, value=8, step=1, label="Timeline (weeks)")782            spike_title = gr.Textbox(label="Spike Title (optional)", placeholder="AI for Good Project")783        btn_spike = gr.Button("Generate Spike Plan", variant="primary")784        spike_md = gr.Markdown()785        spike_ideas = gr.HighlightedText(label="Idea Starters", combine_adjacent=True)786        spike_ics = gr.File(label="Calendar (.ics)")787        btn_spike.click(spike_pipeline, inputs=[spike_interest, weeks, spike_title],788                        outputs=[spike_md, spike_ideas, spike_ics])789 790    with gr.Tab("🎤 Interview Practice"):791        with gr.Row():792            major = gr.Dropdown(choices=list(MAJOR_QUESTIONS.keys()), value="Computer Science", label="Major")793            behavioral = gr.Checkbox(value=True, label="Behavioral? (uncheck for major-specific)")794        answer_text = gr.Textbox(label="Type your answer (90s)", lines=8, placeholder="Practice answering here...")795        btn_interview = gr.Button("Give me a question + score my answer", variant="primary")796        interview_q = gr.Textbox(label="Your Question", lines=2)797        interview_md = gr.Markdown()798        btn_interview.click(interview_pipeline, inputs=[major, behavioral, answer_text],799                            outputs=[interview_q, interview_md])800 801    with gr.Tab("📚 Portfolio Report"):802        gr.Markdown("Combine everything into a clean PDF you can download or share.")803        with gr.Row():804            spike_title_in = gr.Textbox(label="Spike Title for Report", value="My Spike")805            milestones_json = gr.Textbox(label="Milestones JSON (optional)", lines=4, placeholder='[{"week":1,"deliverable":"Kickoff","goal":"Start"}]')806        btn_pdf = gr.Button("Build Portfolio PDF", variant="primary")807        pdf_out = gr.File()808        btn_pdf.click(portfolio_pipeline,809                      inputs=[essay_fb_json, resume_df, college_df, spike_title_in, milestones_json],810                      outputs=[pdf_out])811 812app = demo813 814if __name__ == "__main__":815    demo.launch()816