Spaces:
Sleeping
Sleeping
Commit Β·
b62f15b
1
Parent(s): cfec164
feat: accept .tex resume upload + compile with Tectonic to PDF
Browse filesStreamlit file_uploader now accepts [pdf, tex]. When a .tex file is
uploaded, Tectonic compiles it to PDF in a temp dir and the result is
saved to data/resume/resume.pdf. On failure, stderr is shown inline and
st.stop() halts. Success banner shows the original filename.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HISTORY.md +11 -0
- ui.py +27 -6
HISTORY.md
CHANGED
|
@@ -4,6 +4,17 @@ A running log of everything built, fixed, and changed. Most recent first.
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## 2026-06-23 (PM6) β Fix WebSocket proxy: subprotocol negotiation for Streamlit 1.45+
|
| 8 |
|
| 9 |
Streamlit 1.45+ requires the `streamlit` WebSocket subprotocol to be negotiated
|
|
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
+
## 2026-06-23 (PM7) β LaTeX resume upload support
|
| 8 |
+
|
| 9 |
+
Resume upload page now accepts `.tex` files in addition to PDF. When a `.tex` file is
|
| 10 |
+
uploaded, Tectonic (pre-installed in the Docker image) compiles it to PDF in a temp
|
| 11 |
+
directory and the result is saved as `data/resume/resume.pdf` β same downstream path
|
| 12 |
+
as a direct PDF upload. On compilation failure, a Streamlit error shows the Tectonic
|
| 13 |
+
stderr (last 600 chars) and halts further processing. The success banner shows the
|
| 14 |
+
original uploaded filename so the user can confirm which file was used.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
## 2026-06-23 (PM6) β Fix WebSocket proxy: subprotocol negotiation for Streamlit 1.45+
|
| 19 |
|
| 20 |
Streamlit 1.45+ requires the `streamlit` WebSocket subprotocol to be negotiated
|
ui.py
CHANGED
|
@@ -1172,10 +1172,10 @@ if show_config:
|
|
| 1172 |
</div>""")
|
| 1173 |
|
| 1174 |
resume_file = st.file_uploader(
|
| 1175 |
-
"Drop your resume PDF here",
|
| 1176 |
-
type=["pdf"], key="resume_upload",
|
| 1177 |
label_visibility="collapsed",
|
| 1178 |
-
help="Supported
|
| 1179 |
)
|
| 1180 |
# Save on EVERY new/changed upload (detected by name+size) so a
|
| 1181 |
# re-upload actually REPLACES the old resume. The previous guard
|
|
@@ -1185,8 +1185,28 @@ if show_config:
|
|
| 1185 |
_sig = f"{resume_file.name}:{getattr(resume_file, 'size', 0)}"
|
| 1186 |
if st.session_state.get("_uploaded_sig") != _sig:
|
| 1187 |
os.makedirs("data/resume", exist_ok=True)
|
| 1188 |
-
|
| 1189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1190 |
st.session_state["_uploaded_sig"] = _sig
|
| 1191 |
# Invalidate the parsed-profile cache so it re-parses the
|
| 1192 |
# NEW file (cache is keyed by mtime+size, but delete to be safe)
|
|
@@ -1200,9 +1220,10 @@ if show_config:
|
|
| 1200 |
|
| 1201 |
if has_resume:
|
| 1202 |
fsize = os.path.getsize("data/resume/resume.pdf") // 1024
|
|
|
|
| 1203 |
st.html(f"""
|
| 1204 |
<div class="micro-success">
|
| 1205 |
-
β
<strong>
|
| 1206 |
</div>""")
|
| 1207 |
# ββ Show the profile we built from THIS resume ββ
|
| 1208 |
_render_uploaded_profile()
|
|
|
|
| 1172 |
</div>""")
|
| 1173 |
|
| 1174 |
resume_file = st.file_uploader(
|
| 1175 |
+
"Drop your resume PDF or LaTeX file here",
|
| 1176 |
+
type=["pdf", "tex"], key="resume_upload",
|
| 1177 |
label_visibility="collapsed",
|
| 1178 |
+
help="Supported formats: PDF or LaTeX (.tex). Max size: 10MB.",
|
| 1179 |
)
|
| 1180 |
# Save on EVERY new/changed upload (detected by name+size) so a
|
| 1181 |
# re-upload actually REPLACES the old resume. The previous guard
|
|
|
|
| 1185 |
_sig = f"{resume_file.name}:{getattr(resume_file, 'size', 0)}"
|
| 1186 |
if st.session_state.get("_uploaded_sig") != _sig:
|
| 1187 |
os.makedirs("data/resume", exist_ok=True)
|
| 1188 |
+
if resume_file.name.lower().endswith(".tex"):
|
| 1189 |
+
import tempfile as _tmpfile, subprocess as _sp, shutil as _sh
|
| 1190 |
+
with _tmpfile.TemporaryDirectory() as _td:
|
| 1191 |
+
_tex = os.path.join(_td, "resume.tex")
|
| 1192 |
+
with open(_tex, "wb") as _f:
|
| 1193 |
+
_f.write(resume_file.getvalue())
|
| 1194 |
+
_r = _sp.run(
|
| 1195 |
+
["tectonic", "--outdir", _td, _tex],
|
| 1196 |
+
capture_output=True, text=True, timeout=60,
|
| 1197 |
+
)
|
| 1198 |
+
_pdf = os.path.join(_td, "resume.pdf")
|
| 1199 |
+
if _r.returncode == 0 and os.path.exists(_pdf):
|
| 1200 |
+
_sh.copy(_pdf, "data/resume/resume.pdf")
|
| 1201 |
+
else:
|
| 1202 |
+
st.error(
|
| 1203 |
+
"LaTeX compilation failed. Check your .tex file.\n\n"
|
| 1204 |
+
+ (_r.stderr[-600:] or _r.stdout[-600:])
|
| 1205 |
+
)
|
| 1206 |
+
st.stop()
|
| 1207 |
+
else:
|
| 1208 |
+
with open("data/resume/resume.pdf", "wb") as f:
|
| 1209 |
+
f.write(resume_file.getvalue())
|
| 1210 |
st.session_state["_uploaded_sig"] = _sig
|
| 1211 |
# Invalidate the parsed-profile cache so it re-parses the
|
| 1212 |
# NEW file (cache is keyed by mtime+size, but delete to be safe)
|
|
|
|
| 1220 |
|
| 1221 |
if has_resume:
|
| 1222 |
fsize = os.path.getsize("data/resume/resume.pdf") // 1024
|
| 1223 |
+
_uploaded_name = st.session_state.get("_uploaded_sig", "resume").split(":")[0] or "resume.pdf"
|
| 1224 |
st.html(f"""
|
| 1225 |
<div class="micro-success">
|
| 1226 |
+
β
<strong>{_uploaded_name}</strong> β resume.pdf ({fsize} KB) β Ready for AI matching
|
| 1227 |
</div>""")
|
| 1228 |
# ββ Show the profile we built from THIS resume ββ
|
| 1229 |
_render_uploaded_profile()
|