File size: 12,856 Bytes
499abf3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
import streamlit as st
import requests
from urllib.parse import urlparse
import dotenv
import os
from google import genai
import markdown2
import pdfkit
from google.genai import types
# Load environment variables and configure Gemini AI
dotenv.load_dotenv()
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
# ========== Core Functions ==========
def generate_description(readme_content, job_description):
"""Generate tailored project description"""
prompt = f"""Create a concise resume project description using this README:
{readme_content[:5000]}
Requirements:
- Focus on technical achievements and outcomes
- Use action verbs: Developed, Implemented, Optimized
- Max 5 bullet points
- No markdown formatting
- Technical details only"""
if job_description:
prompt += f"\n\nAlign with this job description:\n{job_description[:1000]}"
short_config = types.GenerateContentConfig(max_output_tokens=1550, temperature=0.3)
response = client.models.generate_content(
contents= prompt,
model="gemini-2.0-flash",
config=short_config
)
return response.text.strip()
def generate_category(readme_content, job_description):
"""Classify project into specific category"""
prompt = f"""Classify this project into ONE category:
like = [Data Science, Data Analyst, Web Dev, Backend Dev, Frontend Dev, Full Stack, DevOps, ML, Java Dev, JS Dev, Python Dev, Mobile Dev, Cloud, Security, QA, Database, Embedded, Networking, AI, Robotics, IoT, Blockchain, AR/VR, Game Dev, UI/UX, Tech Writing, Research, Other]
README: {readme_content[:4000]}
Job Context: {job_description[:1000] or "General technical role"}
Respond ONLY with the category name."""
short_config = types.GenerateContentConfig(max_output_tokens=650, temperature=0.3)
response = client.models.generate_content(
contents= prompt,
model="gemini-2.0-flash",
config=short_config
)
return response.text.strip()
def generate_pdf_from_markdown(markdown_text):
"""Convert markdown to PDF"""
html = markdown2.markdown(markdown_text)
full_html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; line-height: 1.6; }}
h1, h2, h3 {{ color: #2c3e50; }}
h1 {{ font-size: 24px; margin-bottom: 20px; }}
h2 {{ font-size: 18px; margin-top: 20px; margin-bottom: 10px; }}
ul {{ margin-left: 20px; }}
</style>
</head>
<body>
{html}
</body>
</html>
"""
return pdfkit.from_string(full_html, False)
def generate_resume(resume_data):
"""Generate a highly tailored, professional resume text in markdown format"""
prompt = f"""Generate a professional resume in markdown format with the following structure:
# CONTACT INFORMATION
- Name: {resume_data['name']}
- Email: {resume_data['email']}
- Phone: {resume_data.get('phone', 'Not provided')}
- GitHub: {resume_data.get('github', 'Not provided')}
- LinkedIn: {resume_data.get('linkedin', 'Not provided')}
# EDUCATION
{resume_data['education']}
# WORK EXPERIENCE
{resume_data['work_experience'] or "No professional experience provided"}
# TECHNICAL PROJECTS
{format_projects(resume_data['projects'])}
# TECHNICAL SKILLS
- Extract and list the most relevant technical skills from the project descriptions, work experience, and education.
- Prioritize skills mentioned in the job description: {resume_data['job_description'][:2500]}
- Include up to 10 skills, formatted as a concise, comma-separated list (e.g., Python, SQL, Docker, AWS).
- If fewer than 5 skills are found, infer additional plausible skills based on project context (e.g., Git for GitHub projects).
Instructions:
- Use markdown formatting: `#` for headers, `-` for bullet points.
- For TECHNICAL PROJECTS:
- Generate exactly three bullet points per project based on the provided description.
- Optimize bullet points to emphasize measurable outcomes (e.g., "Improved X by Y%") where possible, inferring plausible metrics if not explicitly stated.
- Align language with the job description’s keywords and tone (e.g., 'engineered' vs 'built' if the job emphasizes engineering).
- Maintain a professional, concise tone throughout; avoid fluff or vague phrases (e.g., 'worked on').
- Use action verbs (e.g., Developed, Implemented, Optimized, Engineered, Deployed) to start each bullet point.
- Ensure ATS compatibility: avoid special characters, excessive formatting, or jargon not aligned with the job.
- Do not include any additional text, explanations, or sections beyond the specified structure.
- If data is missing or incomplete, use 'Not provided' or infer minimally as needed without fabrication."""
short_config = types.GenerateContentConfig(max_output_tokens=6050, temperature=0.3)
response = client.models.generate_content(
contents= prompt,
model="gemini-2.0-flash",
config=short_config
)
return response.text
def format_projects(projects):
return "\n".join(
f"- {p['name']} ({p['category']}): {p['description']}"
for p in projects
)
def get_repo_name(url):
"""Extract repository name from URL"""
parsed = urlparse(url)
if parsed.netloc != 'github.com':
return None
path_parts = parsed.path.strip('/').split('/')
return path_parts[1] if len(path_parts) >= 2 else None
def get_readme_content(url):
"""Fetch README content from GitHub"""
try:
parsed = urlparse(url)
path_parts = parsed.path.strip('/').split('/')
if len(path_parts) < 2:
return "Invalid URL format"
username, repo = path_parts[:2]
for branch in ['main', 'master']:
raw_url = f"https://raw.githubusercontent.com/{username}/{repo}/{branch}/README.md"
response = requests.get(raw_url)
if response.status_code == 200:
return response.text
return "README not found. Ensure the file exists in the main or master branch."
except Exception as e:
return f"Error: {str(e)}"
# ========== Streamlit UI ==========
st.set_page_config(page_title="AI Resume Builder", layout="wide")
# Custom CSS for professional look
st.markdown("""
<style>
.stApp { max-width: 900px; margin: 0 auto; }
.stTextArea textarea { min-height: 120px; font-size: 14px; }
.stTextInput > div > div > input { font-size: 14px; }
.stButton > button {
background-color: #2c3e50;
color: white;
border-radius: 5px;
padding: 8px 16px;
}
.stButton > button:hover { background-color: #34495e; }
.stDownloadButton > button {
background-color: #27ae60;
width: 100%;
border-radius: 5px;
padding: 10px;
}
.stDownloadButton > button:hover { background-color: #219653; }
.stExpander { border: 1px solid #e0e0e0; border-radius: 5px; }
h1 { color: #2c3e50; font-size: 28px; }
h3 { color: #34495e; font-size: 18px; }
</style>
""", unsafe_allow_html=True)
st.title("Professional Resume Builder")
# Initialize session state
if 'personal_info' not in st.session_state:
st.session_state.personal_info = {}
if 'projects' not in st.session_state:
st.session_state.projects = []
# Personal Information Section
with st.expander("Personal Information", expanded=True):
with st.form("personal_form"):
cols = st.columns(4)
name = cols[0].text_input("Full Name*", key="name_field")
email = cols[1].text_input("Email*", key="email_field")
phone = cols[2].text_input("Phone", key="phone_field")
github = cols[3].text_input("GitHub Profile", key="github_field")
linkedin = st.text_input("LinkedIn Profile", key="linkedin_field")
skills = st.text_input("Skills", key="skills_field", placeholder="e.g., Python, SQL, Machine Learning, Docker, AWS")
education = st.text_area("Education*",
placeholder="e.g., BS in Computer Science, XYZ University, 2018-2022",
key="edu_field")
work_exp = st.text_area("Work Experience",
placeholder="e.g., Software Engineer, ABC Corp, 2022-Present...",
key="work_field")
job_desc = st.text_area("Target Job Description*",
placeholder="Paste the job description you're applying for",
key="jobdesc_field")
if st.form_submit_button("Save Profile"):
if not all([name, email, education, job_desc]):
st.error("Please fill all required fields (*) before saving.")
else:
st.session_state.personal_info = {
"name": name,
"email": email,
"phone": phone,
"github": github,
"linkedin": linkedin,
"skills": skills,
"education": education,
"work_experience": work_exp,
"job_description": job_desc
}
st.success("Profile saved successfully!")
# Project Configuration Section
with st.expander("Add Technical Projects"):
repos = st.text_area("GitHub Repository URLs (one per line)",
help="Enter links to your project repositories (e.g., https://github.com/username/repo)",
key="repo_field")
if st.button("Generate Project Descriptions", key="gen_proj_btn"):
if not repos or not st.session_state.personal_info:
st.error("Please save personal info and add at least one repository URL.")
else:
with st.spinner("Analyzing repositories..."):
projects = []
for url in repos.split("\n"):
url = url.strip()
if not url:
continue
repo_name = get_repo_name(url)
if not repo_name:
st.error(f"Invalid GitHub URL: {url}")
continue
readme = get_readme_content(url)
if not readme or readme.startswith("Error:") or readme.startswith("Invalid"):
st.error(f"Failed to fetch README for {url}: {readme}")
continue
try:
desc = generate_description(
readme,
st.session_state.personal_info["job_description"]
)
category = generate_category(
readme,
st.session_state.personal_info["job_description"]
)
projects.append({
"name": repo_name,
"description": desc,
"category": category
})
except Exception as e:
st.error(f"Error processing {url}: {str(e)}")
continue
st.session_state.projects = projects
st.success(f"Successfully generated {len(projects)} project descriptions!")
# Preview Section
if st.session_state.projects:
with st.expander("Preview Projects"):
for idx, proj in enumerate(st.session_state.projects, 1):
st.subheader(f"Project {idx}: {proj['name']}")
st.caption(f"Category: {proj['category']}")
st.write(proj["description"])
# Resume Generation Section
if st.session_state.personal_info:
if st.button("Generate Resume PDF", key="gen_pdf_btn"):
with st.spinner("Creating your professional resume..."):
resume_data = {
**st.session_state.personal_info,
"projects": st.session_state.projects
}
try:
resume_text = generate_resume(resume_data)
pdf_bytes = generate_pdf_from_markdown(resume_text)
st.download_button(
label="Download Resume PDF",
data=pdf_bytes,
file_name=f"{resume_data['name'].replace(' ', '_')}_Resume.pdf",
mime="application/pdf",
key="download_btn"
)
st.success("Resume generated successfully!")
with st.expander("Preview Resume Text"):
st.text(resume_text)
except Exception as e:
st.error(f"Error generating resume: {str(e)}") |