STARSRedacter / app.py
natalielamjohnson1's picture
Update app.py
6b18c21 verified
Raw
History Blame Contribute Delete
18.1 kB
"""
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", "")
# --- editable project constants (temporary β€” update here if they change) ----
CONTACT_EMAIL = "buaibuilderhub@gmail.com" # general questions
DELETE_FORM_URL = ( # "delete my report" request
"https://docs.google.com/forms/d/e/"
"1FAIpQLScSpM1ov0dMEXhT3hHhsWmIByu-j1myQYnNvRv7C0FNUW81eg/viewform"
)
# Points at exampleRedactedStars.pdf in this Space repo. st.link_button below
# opens it in a new tab automatically.
SAMPLE_REPORT_URL = (
"https://huggingface.co/spaces/buai-builder-hub/STARSRedacter/blob/main/"
"exampleRedactedStars.pdf"
)
# Screenshots live at the repo root (next to app.py). Shown in this list
# order, top to bottom, as the student scrolls.
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"),
]
# --- submission storage (a private HF Dataset in the org) -------------------
# In-app submission switches on automatically once these Space secrets are set:
# HF_TOKEN a WRITE token with access to the dataset repo
# SUBMISSIONS_REPO (optional) defaults to buai-builder-hub/stars-submissions
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")
# ---- theme polish: USC cardinal + gold, collegiate type --------------------
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,
)
# ============================================================================
# 1) WHAT IS BBH, AND WHY ARE WE DOING THIS
# ============================================================================
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."
)
# A skimmable recap for anyone who won't read every section below.
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."
)
# ============================================================================
# 2) ACCESS CODE
# ============================================================================
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()
# ============================================================================
# 3) SCREENSHOTS β€” WHAT WE KEEP & DON'T KEEP
# ============================================================================
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."
)
# ============================================================================
# 4) LINK TO A FULL SAMPLE REDACTED STARS REPORT
# ============================================================================
st.markdown("### See a full example of a Redacted STARS Report")
st.link_button("πŸ“„ View here", SAMPLE_REPORT_URL)
# ============================================================================
# 5) TRY IT YOURSELF β€” UPLOAD & SEE
# ============================================================================
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:
# Redact ONCE per uploaded file and cache the exact result. Streamlit reruns
# the whole script on every click, so without caching, checking the consent
# box or clicking Submit would re-run redaction and upload a freshly
# -recomputed file β€” not the same bytes the student downloaded and reviewed.
# One redaction, one UID, reused for the download filename and the database
# submission so they always match.
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.")
# ============================================================================
# 6) CONSENT FORM CHECK β€” only needed to SHARE your redacted copy with BBH
# ============================================================================
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.")
# ============================================================================
# 7) YOUR CODE
# ============================================================================
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."
)
# ============================================================================
# 8) REMOVAL LINK
# ============================================================================
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()