raj999 commited on
Commit
40f0b0a
·
1 Parent(s): da0c238

updated with gradio

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -0
  2. README.md +10 -3
  3. app.py +132 -132
  4. requirements.txt +1 -1
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,6 +1,6 @@
1
  # ResumeTailor
2
 
3
- Generate grounded, tailored resumes from a job description and a PDF resume using Streamlit, OpenAI, and LaTeX templates.
4
 
5
  ## Features
6
  - PDF parsing with `pdfplumber` and `pymupdf` fallback
@@ -10,12 +10,12 @@ Generate grounded, tailored resumes from a job description and a PDF resume usin
10
  - Streamlit UI with API key storage (keyring preferred), template selector, and export buttons
11
  - Keyword alignment and missing/needs-confirmation panel
12
 
13
- ## Quickstart
14
  ```bash
15
  python -m venv .venv
16
  source .venv/bin/activate # Windows: .venv\\Scripts\\activate
17
  pip install -r requirements.txt
18
- streamlit run app.py
19
  ```
20
 
21
  ## Using the app
@@ -27,6 +27,13 @@ streamlit run app.py
27
  6. Review the LaTeX preview, missing/needs-confirmation list, and keyword alignment.
28
  7. Export `.tex` or PDF. PDF export requires `latexmk`.
29
 
 
 
 
 
 
 
 
30
  ## LaTeX compilation
31
  - PDF export uses `latexmk -pdf`. Install TeX Live or MikTeX and ensure `latexmk` is on your PATH.
32
  - If `latexmk` is missing, PDF export is disabled but `.tex` export works.
 
1
  # ResumeTailor
2
 
3
+ Generate grounded, tailored resumes from a job description and a PDF resume using Gradio, OpenAI, and LaTeX templates. Suitable for local runs or Hugging Face Spaces.
4
 
5
  ## Features
6
  - PDF parsing with `pdfplumber` and `pymupdf` fallback
 
10
  - Streamlit UI with API key storage (keyring preferred), template selector, and export buttons
11
  - Keyword alignment and missing/needs-confirmation panel
12
 
13
+ ## Quickstart (local)
14
  ```bash
15
  python -m venv .venv
16
  source .venv/bin/activate # Windows: .venv\\Scripts\\activate
17
  pip install -r requirements.txt
18
+ python app.py
19
  ```
20
 
21
  ## Using the app
 
27
  6. Review the LaTeX preview, missing/needs-confirmation list, and keyword alignment.
28
  7. Export `.tex` or PDF. PDF export requires `latexmk`.
29
 
30
+ ## Docker
31
+ ```bash
32
+ docker build -t resume-tailor .
33
+ docker run -p 7860:7860 resume-tailor
34
+ ```
35
+ Then open http://localhost:7860.
36
+
37
  ## LaTeX compilation
38
  - PDF export uses `latexmk -pdf`. Install TeX Live or MikTeX and ensure `latexmk` is on your PATH.
39
  - If `latexmk` is missing, PDF export is disabled but `.tex` export works.
app.py CHANGED
@@ -1,16 +1,17 @@
1
  import json
2
  import logging
3
  import os
 
4
  from pathlib import Path
5
- from typing import Optional
6
 
7
- import streamlit as st
8
 
9
  from llm.pipeline import run_pipeline
10
  from render.latex import compile_to_tempfile, latexmk_available
11
  from render.templates import list_templates, render_template
12
  from resume_parser.parser import parse_resume_pdf
13
- from schemas.resume import Resume, TailoredResume
14
 
15
  logging.basicConfig(level=logging.INFO)
16
  logger = logging.getLogger("resume_tailor")
@@ -43,7 +44,7 @@ def save_api_key(key: str) -> None:
43
  LOCAL_KEY_PATH.write_text(key)
44
 
45
 
46
- def clear_api_key() -> None:
47
  try:
48
  import keyring # type: ignore
49
 
@@ -52,144 +53,143 @@ def clear_api_key() -> None:
52
  pass
53
  if LOCAL_KEY_PATH.exists():
54
  LOCAL_KEY_PATH.unlink()
 
55
 
56
 
57
- def ensure_state():
58
- st.session_state.setdefault("latex_content", "")
59
- st.session_state.setdefault("resume_json", {})
60
- st.session_state.setdefault("tailored", None)
61
- st.session_state.setdefault("raw_text", "")
62
- st.session_state.setdefault("logs", [])
63
 
64
 
65
- def log(message: str):
66
- st.session_state.logs.append(message)
67
- st.session_state.logs = st.session_state.logs[-8:]
 
 
 
 
 
 
68
 
 
 
69
 
70
- def main():
71
- st.set_page_config(page_title=APP_TITLE, layout="wide")
72
- ensure_state()
 
 
 
73
 
74
- st.title(APP_TITLE)
75
- st.write("Generate tailored resumes with grounded extraction and LaTeX rendering.")
76
 
77
- stored_key = load_api_key()
78
- col1, col2 = st.columns(2)
79
- with col1:
80
- job_description = st.text_area("Job Description", height=220)
81
- api_key = st.text_input(
82
- "OpenAI API Key",
83
- type="password",
84
- value=stored_key or "",
85
- help="Stored securely via system keychain when available.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  )
87
- save_key = st.checkbox("Save locally", value=bool(stored_key))
88
- model = st.text_input("Model name", value="gpt-4o-mini")
89
- template_names = list(list_templates().keys())
90
- template_choice = st.selectbox(
91
- "Template", options=template_names, index=0 if template_names else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  )
93
- if st.button("Clear stored key"):
94
- clear_api_key()
95
- st.success("Stored key cleared.")
96
- with col2:
97
- uploaded_file = st.file_uploader("Upload Resume PDF", type=["pdf"])
98
- st.markdown("**Output Preview**")
99
- st.code(st.session_state.get("latex_content", ""), language="latex")
100
- st.markdown("**Logs**")
101
- st.text("\n".join(st.session_state.get("logs", [])))
102
-
103
- if st.button("Generate Tailored Resume"):
104
- if not api_key:
105
- st.error("API key required.")
106
- return
107
- if not uploaded_file:
108
- st.error("Please upload a resume PDF.")
109
- return
110
- if not job_description.strip():
111
- st.error("Job description required.")
112
- return
113
-
114
- if save_key:
115
- save_api_key(api_key)
116
-
117
- with st.spinner("Parsing resume PDF..."):
118
- import tempfile
119
-
120
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
121
- tmp.write(uploaded_file.getvalue())
122
- temp_pdf = Path(tmp.name)
123
-
124
- result = parse_resume_pdf(str(temp_pdf))
125
- st.session_state["raw_text"] = result.raw_text
126
- log(f"Extracted text using {result.method}")
127
-
128
- with st.spinner("Running LLM pipeline..."):
129
- template_map = list_templates()
130
- template_source = template_map[template_choice].read_text(encoding="utf-8")
131
- resume, tailored = run_pipeline(
132
- api_key=api_key,
133
- model=model,
134
- raw_text=result.raw_text,
135
- job_description=job_description,
136
- template_name=template_choice,
137
- template_source=template_source,
138
- )
139
- st.session_state["resume_json"] = json.loads(resume.json())
140
- st.session_state["tailored"] = tailored
141
-
142
- context = tailored.tailored_resume.dict()
143
- rendered = render_template(template_choice, context)
144
- tailored.latex_content = rendered
145
- st.session_state["latex_content"] = rendered
146
- log("Pipeline completed.")
147
-
148
- tailored: TailoredResume = st.session_state.get("tailored")
149
- resume_data = st.session_state.get("resume_json")
150
-
151
- if tailored:
152
- st.subheader("Missing / Needs Confirmation")
153
- st.write(tailored.missing_items or ["None"])
154
-
155
- st.subheader("Questions for user")
156
- st.write(tailored.questions or ["None"])
157
-
158
- st.subheader("Keyword alignment")
159
- st.json(tailored.keyword_alignment.dict())
160
-
161
- col_export1, col_export2 = st.columns(2)
162
- with col_export1:
163
- if st.session_state.get("latex_content"):
164
- st.download_button(
165
- "Export .tex",
166
- data=st.session_state["latex_content"],
167
- file_name="tailored_resume.tex",
168
- mime="application/x-tex",
169
- )
170
- with col_export2:
171
- if st.session_state.get("latex_content"):
172
- if latexmk_available():
173
- pdf_path = compile_to_tempfile(st.session_state["latex_content"])
174
- if pdf_path and pdf_path.exists():
175
- st.download_button(
176
- "Export PDF",
177
- data=pdf_path.read_bytes(),
178
- file_name="tailored_resume.pdf",
179
- mime="application/pdf",
180
- )
181
- else:
182
- st.info("latexmk not installed. PDF export disabled. See README.")
183
-
184
- st.subheader("Generated LaTeX")
185
- st.code(st.session_state.get("latex_content", ""), language="latex")
186
-
187
- st.subheader("Resume JSON")
188
- if resume_data:
189
- st.json(resume_data)
190
- else:
191
- st.text("Run the pipeline to view parsed resume JSON.")
192
 
193
 
194
  if __name__ == "__main__":
195
- main()
 
 
1
  import json
2
  import logging
3
  import os
4
+ import tempfile
5
  from pathlib import Path
6
+ from typing import Optional, Tuple
7
 
8
+ import gradio as gr
9
 
10
  from llm.pipeline import run_pipeline
11
  from render.latex import compile_to_tempfile, latexmk_available
12
  from render.templates import list_templates, render_template
13
  from resume_parser.parser import parse_resume_pdf
14
+ from schemas.resume import TailoredResume
15
 
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger("resume_tailor")
 
44
  LOCAL_KEY_PATH.write_text(key)
45
 
46
 
47
+ def clear_api_key() -> str:
48
  try:
49
  import keyring # type: ignore
50
 
 
53
  pass
54
  if LOCAL_KEY_PATH.exists():
55
  LOCAL_KEY_PATH.unlink()
56
+ return ""
57
 
58
 
59
+ def _render_latex_from_tailored(tailored: TailoredResume, template_choice: str) -> str:
60
+ context = tailored.tailored_resume.dict()
61
+ return render_template(template_choice, context)
 
 
 
62
 
63
 
64
+ def generate_tailored_resume(
65
+ job_description: str,
66
+ pdf_file,
67
+ api_key: str,
68
+ model: str,
69
+ template_choice: str,
70
+ save_key: bool,
71
+ ) -> Tuple[str, str, str, dict, str, Optional[str], Optional[str], dict]:
72
+ logs = []
73
 
74
+ def log(msg: str):
75
+ logs.append(msg)
76
 
77
+ if not api_key:
78
+ return ("", "API key required.", "", {}, "\n".join(logs), None, None, {})
79
+ if not pdf_file:
80
+ return ("", "Please upload a resume PDF.", "", {}, "\n".join(logs), None, None, {})
81
+ if not job_description.strip():
82
+ return ("", "Job description required.", "", {}, "\n".join(logs), None, None, {})
83
 
84
+ if save_key:
85
+ save_api_key(api_key)
86
 
87
+ try:
88
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
89
+ tmp.write(pdf_file.read())
90
+ pdf_path = Path(tmp.name)
91
+ result = parse_resume_pdf(str(pdf_path))
92
+ log(f"Extracted text using {result.method}")
93
+
94
+ template_map = list_templates()
95
+ template_source = template_map[template_choice].read_text(encoding="utf-8")
96
+
97
+ resume, tailored = run_pipeline(
98
+ api_key=api_key,
99
+ model=model,
100
+ raw_text=result.raw_text,
101
+ job_description=job_description,
102
+ template_name=template_choice,
103
+ template_source=template_source,
104
+ )
105
+
106
+ rendered_latex = _render_latex_from_tailored(tailored, template_choice)
107
+ tailored.latex_content = rendered_latex
108
+ tex_file_path: Optional[str] = None
109
+ pdf_file_path: Optional[str] = None
110
+
111
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".tex") as tex_tmp:
112
+ tex_tmp.write(rendered_latex.encode("utf-8"))
113
+ tex_file_path = tex_tmp.name
114
+
115
+ if latexmk_available():
116
+ try:
117
+ pdf_out = compile_to_tempfile(rendered_latex)
118
+ if pdf_out:
119
+ pdf_file_path = str(pdf_out)
120
+ except Exception as exc:
121
+ log(f"latexmk failed: {exc}")
122
+ else:
123
+ log("latexmk not installed; PDF export disabled.")
124
+
125
+ missing_text = "\n".join(tailored.missing_items) or "None"
126
+ questions_text = "\n".join(tailored.questions) or "None"
127
+
128
+ return (
129
+ rendered_latex,
130
+ missing_text,
131
+ questions_text,
132
+ tailored.keyword_alignment.dict(),
133
+ "\n".join(logs),
134
+ tex_file_path,
135
+ pdf_file_path,
136
+ json.loads(resume.json()),
137
  )
138
+ except Exception as exc:
139
+ log(f"Error: {exc}")
140
+ return ("", "An error occurred. Check logs.", "", {}, "\n".join(logs), None, None, {})
141
+
142
+
143
+ def build_ui():
144
+ stored_key = load_api_key() or ""
145
+ templates = list_templates()
146
+ template_names = list(templates.keys()) or ["modern"]
147
+
148
+ with gr.Blocks(title=APP_TITLE) as demo:
149
+ gr.Markdown(f"# {APP_TITLE}\nTailor resumes with grounded extraction and LaTeX rendering.")
150
+ with gr.Row():
151
+ with gr.Column():
152
+ jd = gr.Textbox(label="Job Description", lines=12, placeholder="Paste JD here")
153
+ api = gr.Textbox(label="OpenAI API Key", type="password", value=stored_key)
154
+ save_key = gr.Checkbox(label="Save key locally (keyring preferred)", value=bool(stored_key))
155
+ model = gr.Textbox(label="Model name", value="gpt-4o-mini")
156
+ template_choice = gr.Dropdown(
157
+ label="Template", choices=template_names, value=template_names[0]
158
+ )
159
+ clear_btn = gr.Button("Clear stored key")
160
+ with gr.Column():
161
+ pdf = gr.File(label="Upload Resume PDF", file_types=[".pdf"])
162
+ logs_box = gr.Textbox(label="Logs", lines=10, interactive=False)
163
+
164
+ generate_btn = gr.Button("Generate Tailored Resume")
165
+ latex_preview = gr.Code(label="LaTeX Output", language="latex")
166
+ missing_panel = gr.Textbox(label="Missing / Needs Confirmation", lines=6)
167
+ questions_panel = gr.Textbox(label="Questions for user", lines=4)
168
+ keyword_alignment = gr.JSON(label="Keyword alignment")
169
+ resume_json = gr.JSON(label="Resume JSON (parsed)")
170
+ tex_download = gr.File(label="Export .tex")
171
+ pdf_download = gr.File(label="Export PDF (requires latexmk)")
172
+
173
+ generate_btn.click(
174
+ fn=generate_tailored_resume,
175
+ inputs=[jd, pdf, api, model, template_choice, save_key],
176
+ outputs=[
177
+ latex_preview,
178
+ missing_panel,
179
+ questions_panel,
180
+ keyword_alignment,
181
+ logs_box,
182
+ tex_download,
183
+ pdf_download,
184
+ resume_json,
185
+ ],
186
  )
187
+
188
+ clear_btn.click(fn=clear_api_key, inputs=None, outputs=api)
189
+
190
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
 
193
  if __name__ == "__main__":
194
+ app = build_ui()
195
+ app.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- streamlit
2
  pdfplumber
3
  pymupdf
4
  pydantic
 
1
+ gradio
2
  pdfplumber
3
  pymupdf
4
  pydantic