| """ |
| Streamlit front end for the STARS report redactor (student-facing). |
| |
| Privacy design: the uploaded PDF is redacted entirely IN MEMORY via |
| `engine.redact_bytes` and is never written to the server's disk. Only the |
| redacted copy is offered back to the student for download; nothing is stored |
| unless the student explicitly chooses to submit it to BBH. |
| |
| Deployment: Hugging Face Space (Streamlit SDK). Set a Space secret named |
| APP_PASSPHRASE to the access phrase before going live. |
| |
| Page layout (top to bottom): |
| 1) What is BBH, and why are we doing this |
| 2) Access code |
| 3) Screenshots - what we keep / don't keep |
| 4) Link to a full sample redacted STARS report |
| 5) Try it yourself - upload & see |
| 6) Consent form check (only required to SHARE your redacted copy) |
| 7) Your code (shown after you submit) |
| 8) Removal link |
| """ |
| import os |
| import uuid |
|
|
| import streamlit as st |
|
|
| from redactors import engine |
|
|
| PASSPHRASE = os.environ.get("APP_PASSPHRASE", "") |
|
|
| |
| CONTACT_EMAIL = "buaibuilderhub@gmail.com" |
| DELETE_FORM_URL = ( |
| "https://docs.google.com/forms/d/e/" |
| "1FAIpQLScSpM1ov0dMEXhT3hHhsWmIByu-j1myQYnNvRv7C0FNUW81eg/viewform" |
| ) |
| |
| |
| SAMPLE_REPORT_URL = ( |
| "https://huggingface.co/spaces/buai-builder-hub/STARSRedacter/blob/main/" |
| "exampleRedactedStars.pdf" |
| ) |
|
|
| |
| |
| KEEP_DONT_KEEP_IMAGES = [ |
| ("explanationSS1.png", "Your name and USC ID are redacted"), |
| ("explanationSS2.png", |
| "Major, minor, and class level are kept β an unusual combination could " |
| "still be identifiable"), |
| ("explanationSS3.png", |
| "Every grade becomes Pass/No Pass, and GPA is removed entirely"), |
| ] |
|
|
| |
| |
| |
| |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") |
| SUBMISSIONS_REPO = os.environ.get("SUBMISSIONS_REPO", |
| "buai-builder-hub/redacted-stars-submissions") |
|
|
|
|
| def _submissions_enabled(): |
| return bool(HF_TOKEN) |
|
|
|
|
| def _upload_submission(data, uid): |
| """Save the REDACTED bytes to the private dataset repo as <uid>.pdf.""" |
| from huggingface_hub import HfApi |
| HfApi(token=HF_TOKEN).upload_file( |
| path_or_fileobj=data, |
| path_in_repo=f"{uid}.pdf", |
| repo_id=SUBMISSIONS_REPO, |
| repo_type="dataset", |
| commit_message=f"submission {uid}", |
| ) |
|
|
|
|
| def footer(): |
| """Always-visible byline (rendered at the bottom whether or not the page is |
| unlocked).""" |
| st.divider() |
| st.caption("Powered by the BBH (BUAI Builder Hub). " |
| "Tools by students, for students.") |
|
|
|
|
| st.set_page_config(page_title="STARS Report Redactor", page_icon="π‘οΈ", |
| layout="centered") |
|
|
| |
| st.markdown( |
| """ |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,600&family=Inter:wght@400;600&display=swap'); |
| html, body, [class*="css"], .stMarkdown, p, li { font-family: 'Inter', system-ui, sans-serif; } |
| h1, h2, h3 { font-family: 'Newsreader', Georgia, serif !important; color: #990000; letter-spacing: .2px; } |
| h1 { border-bottom: 3px solid #FFCC00; padding-bottom: .35rem; } |
| .stButton > button, .stDownloadButton > button, .stLinkButton > a { |
| background-color: #990000; color: #ffffff; border: 0; font-weight: 600; border-radius: 8px; |
| } |
| .stButton > button:hover, .stDownloadButton > button:hover, .stLinkButton > a:hover { |
| background-color: #7a0000; color: #FFCC00; |
| } |
| div[data-testid="stExpander"] { border: 1px solid #ece2cc; border-radius: 8px; } |
| div[data-testid="stExpander"] summary { font-weight: 600; color: #990000; } |
| </style> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| st.markdown( |
| """ |
| <div style="text-align:center; margin: 0 0 .25rem 0;"> |
| <svg width="74" height="86" viewBox="0 0 74 86" xmlns="http://www.w3.org/2000/svg" |
| role="img" aria-label="BBH emblem"> |
| <path d="M37 3 L69 13 L69 43 C69 65 53 79 37 83 C21 79 5 65 5 43 L5 13 Z" |
| fill="#990000" stroke="#FFCC00" stroke-width="3"/> |
| <text x="37" y="50" text-anchor="middle" |
| font-family="Newsreader, Georgia, serif" font-size="22" |
| font-weight="700" fill="#FFCC00">BBH</text> |
| </svg> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| |
| |
| st.title("Please Share Your STARS Report to Support Our Student Tool!") |
|
|
| st.markdown( |
| "**BUAI Builder Hub** is working on creating tools to support students in " |
| "**degree planning, course registration, and picking classes**. We need your " |
| "help sourcing STARS reports to test against our tool!\n\n" |
| "It takes 2 minutes. Upload your STARS report and we'll strip out all " |
| "sensitive info." |
| ) |
|
|
| |
| with st.container(border=True): |
| st.markdown("**π TL;DR**") |
| st.markdown( |
| "- Your **name, USC ID, address, grades, and GPA** are never stored β " |
| "only a de-identified copy, and only if you choose to share it.\n" |
| "- Only **BBH student developer teams** can ever access it. Private, " |
| "access-controlled, never sold or made public.\n" |
| "- You can **try the tool and see your redacted file first** β sharing " |
| "it with BBH is a separate, optional step.\n" |
| "- Changed your mind later? You can request that we **delete your report** " |
| "anytime using the code we give you.\n" |
| "- Takes about **two minutes**, start to finish." |
| ) |
|
|
| |
| |
| |
| if not PASSPHRASE: |
| st.error("This app isn't set up yet (no passphrase configured). Ping the team.") |
| footer() |
| st.stop() |
|
|
| st.markdown("### Enter your access code") |
| entered = st.text_input("Passphrase", type="password") |
| st.caption("If you're a USC student using this, whoever referred you should've " |
| "shared a passphrase.") |
| if entered.strip() != PASSPHRASE: |
| if entered: |
| st.warning("Hmm, that's not it β double-check with whoever sent you here.") |
| footer() |
| st.stop() |
|
|
| |
| |
| |
| st.markdown("### What we keep β and what we don't") |
|
|
| for path, caption in KEEP_DONT_KEEP_IMAGES: |
| try: |
| st.image(path, caption=caption, width="stretch") |
| except Exception as e: |
| st.info(f"(Couldn't load `{path}`: {e})") |
|
|
| with st.expander("Wait β are my grades going in a database?"): |
| st.markdown( |
| "Nope. Before anything gets saved, we swap **every grade for a plain " |
| "\"passed / didn't pass\" marker** and **wipe your GPA**. The copy we keep " |
| "doesn't have your letter grades in it β we can tell a class was " |
| "finished, not what you got in it." |
| ) |
|
|
| with st.expander("Who actually sees my report?"): |
| st.markdown( |
| "Only **BBH student developer teams** β students building tools to improve " |
| "the USC student experience. Your redacted report is kept somewhere " |
| "**private and access-controlled** (not public, never for sale), and used " |
| "only to build and test those tools." |
| ) |
|
|
| |
| |
| |
| st.markdown("### See a full example of a Redacted STARS Report") |
| st.link_button("π View here", SAMPLE_REPORT_URL) |
|
|
| |
| |
| |
| st.markdown("### Try it yourself") |
|
|
| with st.expander("How do I even get my STARS report?"): |
| st.markdown( |
| "1. Go to **experience.usc.edu** β **My Academics**.\n" |
| "2. Open your **STARS** (Degree Progress) report.\n" |
| "3. Hit the **red Print button** up top.\n" |
| "4. In the print pop-up, pick **Save as PDF** and save it.\n" |
| "5. Upload that PDF here. (Grab the real PDF from the site β a screenshot " |
| "or scan won't work.)" |
| ) |
|
|
| uploaded = st.file_uploader( |
| "Upload your STARS report (the PDF you saved from experience.usc.edu)", |
| type=["pdf"], accept_multiple_files=False) |
| st.caption("π Your upload is handled in memory only and is discarded the moment " |
| "you close this tab. You can preview and download your redacted file " |
| "below β to share it, you must click submit further down the page.") |
|
|
| if uploaded is not None: |
| |
| |
| |
| |
| |
| |
| file_key = getattr(uploaded, "file_id", None) or f"{uploaded.name}:{uploaded.size}" |
| if st.session_state.get("uid_key") != file_key: |
| with st.spinner("Scrubbing your infoβ¦"): |
| try: |
| res, out = engine.redact_bytes(uploaded.getvalue(), {"pii", "grades"}) |
| except Exception: |
| res, out = {"status": "FAILED", "code": "ERROR"}, None |
| st.session_state["uid_key"] = file_key |
| st.session_state["uid"] = uuid.uuid4().hex[:8].upper() |
| st.session_state["res"] = res |
| st.session_state["out"] = out |
| st.session_state["submitted"] = False |
|
|
| res = st.session_state["res"] |
| out = st.session_state["out"] |
| uid = st.session_state["uid"] |
| status = res.get("status") |
|
|
| if status in ("REDACTED", "REVIEW") and out: |
| g = res.get("grades", {}) |
| st.success("Done! Your redacted report is ready below. π") |
| st.write( |
| f"We removed **{len(res.get('pii', {}))}** personal detail(s), turned " |
| f"**{g.get('grades_replaced', 0)}** grade(s) into pass/fail markers, and " |
| f"wiped **{g.get('gpa_replaced', 0)}** GPA/points number(s)." |
| ) |
| if res.get("fragments"): |
| st.info("Part of your name might still show up inside a course title β " |
| "we'll double-check that on our end.") |
|
|
| st.download_button("β¬οΈ Download my redacted report", data=out, |
| file_name=f"REDACTED_{uid}.pdf", |
| mime="application/pdf") |
| st.caption(f"Saved as `REDACTED_{uid}.pdf` β take a look and make sure it " |
| "looks right. Sharing it with BBH is a separate, optional step " |
| "below.") |
|
|
| elif status == "SKIPPED": |
| st.error("This looks like a **scan or photo** (no text in it), so it can't " |
| "be redacted here. Grab the actual PDF via **Print β Save as PDF** " |
| "on experience.usc.edu and try again.") |
| elif res.get("code") == "NOT_STARS": |
| st.error("Hmm β this doesn't look like a **STARS Degree Progress Report**. " |
| "Double-check you uploaded the right PDF (the one from " |
| "experience.usc.edu β My Academics β STARS), not another document.") |
| elif res.get("code") == "UNREADABLE": |
| st.error("We couldn't read that file β it might be corrupted or not a real " |
| "PDF. Try re-saving it with **Print β Save as PDF** on " |
| "experience.usc.edu and uploading again.") |
| else: |
| st.error("Something didn't check out, so we **didn't produce a file** β " |
| f"better safe than sorry. Please email {CONTACT_EMAIL}, and " |
| "don't share the original in the meantime.") |
|
|
| |
| |
| |
| have_valid_redaction = bool( |
| uploaded is not None |
| and st.session_state.get("out") |
| and st.session_state.get("res", {}).get("status") in ("REDACTED", "REVIEW") |
| ) |
|
|
| if have_valid_redaction: |
| st.markdown("### Want to share it with BBH?") |
| st.caption("Totally optional β you've already got your redacted file above. " |
| "This part is only about whether you'd like to contribute a copy " |
| "to BBH's project.") |
|
|
| with st.expander("Sharing Consent Acknowledgement", expanded=True): |
| st.markdown( |
| "- **Submitting is voluntary.** You don't have to do it, and you can " |
| "close this page anytime.\n" |
| "- **What we'd keep:** only the **redacted** version of your STARS " |
| "report β the file this tool just handed you after removing your " |
| "personal info.\n" |
| "- **What's removed before anything is stored:** your **name, USC ID, " |
| "mailing address, sport/team**, every **letter grade** (each becomes a " |
| "simple pass/fail marker), and your **GPA**.\n" |
| "- **What stays in the file:** your **major/minor, the courses and " |
| "terms on your record, and your degree-progress details**.\n" |
| "- **Identifiability:** We remove your name, ID, grades, and GPA, but " |
| "we **keep your major(s), minor, and the list of classes you've " |
| "taken**. If your combination is unusual (e.g. the only USC student " |
| "double-majoring in BUAI and Anthropology), someone who sees the file " |
| "could figure out it's yours. They still **won't** see your grades, " |
| "GPA, or student ID.\n" |
| "- **Your original file is never saved.** It's handled right here in " |
| "the page's memory, **never written to disk**, and **automatically " |
| "discarded the moment you close this tab**. If you submit, we save " |
| "**only the redacted copy** to a **private BBH archive**, tagged with " |
| "a random code we show you. You can use this code to request that " |
| "your report be removed anytime.\n" |
| "- **How we use it:** to build and test tools that help USC students " |
| "plan degrees and register for classes, and β because it's " |
| "de-identified β possibly for research about the student experience. " |
| "We won't sell it or try to identify you.\n" |
| "- **Want to remove your report?** You can ask us to delete your " |
| "redacted report anytime, using the code we give you at submission." |
| ) |
|
|
| consent = st.checkbox( |
| "I've read the above and I consent to sharing my **redacted** STARS " |
| "report with BBH.") |
|
|
| if not consent: |
| st.info("Check the box above if you'd like to submit your redacted copy.") |
| else: |
| st.markdown("**Share it with BBH**") |
| if _submissions_enabled(): |
| if st.button("π€ Submit my redacted report"): |
| try: |
| _upload_submission(st.session_state["out"], st.session_state["uid"]) |
| st.session_state["submitted"] = True |
| st.success("Submitted β thank you so much! π") |
| except Exception: |
| st.error("Submission didn't go through β sorry! Your redacted " |
| "file is still available above; please try submitting " |
| "again in a moment.") |
| else: |
| st.caption("Direct submission is unavailable right now β you can still " |
| "download your redacted file above.") |
|
|
| |
| |
| |
| if st.session_state.get("submitted"): |
| st.markdown("### Your code") |
| st.warning( |
| f"**Your report ID is `{st.session_state['uid']}`.**\n\n" |
| "**Save this code now** (screenshot it, write it down β whatever works). " |
| "Your report is stored with **no name attached**, so this ID is the " |
| "**only** way to delete it later. If you lose it, we can't find your " |
| "report to remove it." |
| ) |
|
|
| |
| |
| |
| st.markdown("### Requesting removal of your report?") |
| st.markdown( |
| "When you submit, we give you a **report ID code** (see above). Fill out the " |
| "form below with your code, and we'll remove your report." |
| ) |
| st.link_button("ποΈ Request removal", DELETE_FORM_URL) |
| st.caption( |
| "β οΈ Save your report ID somewhere safe. Your report is stored with **no name " |
| "attached**, so the ID is the *only* way we can find it. If you lose it, we " |
| "won't be able to delete your report." |
| ) |
|
|
| footer() |