saitejatirunagari Claude Sonnet 4.6 commited on
Commit
b62f15b
Β·
1 Parent(s): cfec164

feat: accept .tex resume upload + compile with Tectonic to PDF

Browse files

Streamlit 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>

Files changed (2) hide show
  1. HISTORY.md +11 -0
  2. 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 format: PDF. 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,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
- with open("data/resume/resume.pdf", "wb") as f:
1189
- f.write(resume_file.getvalue())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>resume.pdf</strong> uploaded ({fsize} KB) β€” Ready for AI matching
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()