Spaces:
Sleeping
Sleeping
# main.py | |
from fastapi import FastAPI, HTTPException, status, Request | |
from fastapi.middleware.cors import CORSMiddleware | |
from fastapi.responses import FileResponse, HTMLResponse | |
from fastapi.staticfiles import StaticFiles | |
from fastapi.templating import Jinja2Templates | |
from pydantic import BaseModel | |
from typing import Optional, List | |
import requests | |
from urllib.parse import urlparse | |
from google import genai | |
import os | |
import markdown2 | |
import pdfkit | |
from google.genai import types | |
import tempfile | |
import dotenv | |
from datetime import datetime | |
# Load environment variables from .env file | |
dotenv.load_dotenv() | |
app = FastAPI(title="Smart Resume Generator") | |
# Configure CORS | |
app.add_middleware( | |
CORSMiddleware, | |
allow_origins=["*"], | |
allow_credentials=True, | |
allow_methods=["*"], | |
allow_headers=["*"], | |
) | |
# Mount static files | |
app.mount("/static", StaticFiles(directory="static"), name="static") | |
# Setup Jinja2 templates | |
templates = Jinja2Templates(directory="templates") | |
# Initialize Gemini client | |
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) | |
# ========== Pydantic Models ========== | |
class GenerateDescriptionRequest(BaseModel): | |
readme_content: str | |
job_description: str | |
class GenerateCategoryRequest(BaseModel): | |
readme_content: str | |
job_description: str | |
class ProjectData(BaseModel): | |
name: str | |
description: str | |
category: str | |
class ResumeData(BaseModel): | |
name: str | |
email: str | |
phone: Optional[str] = None | |
github: Optional[str] = None | |
linkedin: Optional[str] = None | |
skills: Optional[str] = None | |
education: str | |
work_experience: Optional[str] = None | |
job_description: str | |
projects: List[ProjectData] | |
class GeneratePDFRequest(BaseModel): | |
markdown_text: str | |
# ========== Frontend Routes ========== | |
async def index(request: Request): | |
"""Serve the main application page""" | |
return templates.TemplateResponse("index.html", {"request": request}) | |
# ========== API Endpoints ========== | |
def api_root(): | |
return {"message": "Welcome to the Smart Resume Generator API!"} | |
def health_check(): | |
return {"status": "OK", "message": "API is running!"} | |
async def generate_description_endpoint(request: GenerateDescriptionRequest): | |
try: | |
prompt = f"""Create a concise resume project description using this README: | |
{request.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 request.job_description: | |
prompt += f"\n\nAlign with this job description:\n{request.job_description[:1000]}" | |
config = types.GenerateContentConfig(max_output_tokens=1550, temperature=0.3) | |
response = client.models.generate_content( | |
contents=prompt, | |
model="gemini-2.0-flash", | |
config=config | |
) | |
return {"description": response.text.strip()} | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"Error generating description: {str(e)}" | |
) | |
async def generate_category_endpoint(request: GenerateCategoryRequest): | |
try: | |
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: {request.readme_content[:4000]} | |
Job Context: {request.job_description[:1000] or "General technical role"} | |
Respond ONLY with the category name.""" | |
config = types.GenerateContentConfig(max_output_tokens=650, temperature=0.3) | |
response = client.models.generate_content( | |
contents=prompt, | |
model="gemini-2.0-flash", | |
config=config | |
) | |
return {"category": response.text.strip()} | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"Error generating category: {str(e)}" | |
) | |
async def generate_resume_endpoint(resume_data: ResumeData): | |
try: | |
prompt = f"""Generate a professional resume in markdown format with the following structure, optimized for relevance to the job description below. | |
Job description: {resume_data.job_description[:4500]} | |
# CONTACT INFORMATION | |
- Name: {resume_data.name} | |
- Email: {resume_data.email} | |
- Phone: {resume_data.phone or 'Not provided'} | |
- GitHub: {resume_data.github or 'Not provided'} | |
- LinkedIn: {resume_data.linkedin or 'Not provided'} | |
# OBJECTIVE | |
Craft a concise, role-focused objective (2-3 lines) summarizing the candidate's intent and qualifications. Tailor this section based on the job description provided below and the candidate's strengths in projects, education, or experience. | |
# EDUCATION | |
{resume_data.education} # Format as: Degree, Major, University, Year. | |
Example:'BSc in Computer Science, XYZ University, 2023 | |
Msc in Data Science, ABC University, 2024' | |
{f"# WORK EXPERIENCE\\n{resume_data.work_experience}" if resume_data.work_experience else ""} | |
# 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 that match the job description. | |
- Include up to 10 skills in a comma-separated list (e.g., Python, SQL, Docker, AWS). | |
- If fewer than 5 skills are found, infer additional plausible skills based on context (e.g., Git if GitHub is mentioned). | |
Instructions: | |
- Use markdown formatting: `#` for headers, `-` for bullet points. | |
- For TECHNICAL PROJECTS: | |
- Write exactly three bullet points per project based on the description. | |
- Emphasize measurable outcomes (e.g., “Increased accuracy by 20%”), inferring realistic metrics where applicable. | |
- Align language and terminology with keywords from the job description. | |
- Maintain a professional, concise tone throughout. | |
- Start each bullet point with strong action verbs (e.g., Developed, Deployed, Engineered). | |
- Avoid vague or generic phrases (e.g., “worked on”, “helped with”). | |
- Ensure the resume is ATS-friendly: no special characters, excessive formatting, or unrelated jargon. | |
- Do not include any text, explanation, or sections outside the defined structure. | |
""" | |
config = types.GenerateContentConfig(max_output_tokens=6050, temperature=0.3) | |
response = client.models.generate_content( | |
contents=prompt, | |
model="gemini-2.0-flash", | |
config=config | |
) | |
return {"resume_markdown": response.text} | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"Error generating resume: {str(e)}" | |
) | |
async def get_readme_content(url: str): | |
try: | |
parsed = urlparse(url) | |
path_parts = parsed.path.strip('/').split('/') | |
if len(path_parts) < 2: | |
return {"error": "Invalid URL format"} | |
username, repo = path_parts[:2] | |
# Get actual repository name (second path component) | |
repo_name = path_parts[1] if len(path_parts) >= 2 else None | |
for branch in ['main', 'master']: | |
raw_url = f"https://raw.githubusercontent.com/{username}/{repo_name}/{branch}/README.md" | |
response = requests.get(raw_url) | |
if response.status_code == 200: | |
return {"content": response.text, "repo_name": repo_name} | |
return {"error": "README not found"} | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"Error fetching README: {str(e)}" | |
) | |
async def generate_pdf_endpoint(request: GeneratePDFRequest): | |
try: | |
# Convert markdown to HTML with extra features enabled | |
html = markdown2.markdown(request.markdown_text, extras=["tables", "cuddled-lists", "header-ids"]) | |
# Create a professional and elegant HTML template | |
full_html = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;600&display=swap'); | |
body {{ | |
font-family: 'Open Sans', Arial, sans-serif; | |
color: #333; | |
line-height: 1.6; | |
margin: 0; | |
padding: 12px 10px; | |
max-width: 100%; | |
font-size: 11pt; | |
background: #fff; | |
}} | |
.resume-container {{ | |
max-width: 8.5in; | |
margin: 0 auto; | |
border: none; | |
box-shadow: none; | |
}} | |
h1 {{ | |
font-family: 'Roboto', sans-serif; | |
font-size: 18pt; | |
font-weight: 700; | |
color: #1a365d; | |
margin-bottom: 10px; | |
padding-bottom: 5px; | |
border-bottom: 2px solid #3182ce; | |
}} | |
h2 {{ | |
font-family: 'Roboto', sans-serif; | |
font-size: 14pt; | |
font-weight: 600; | |
color: #2c5282; | |
margin-top: 20px; | |
margin-bottom: 10px; | |
padding-bottom: 3px; | |
border-bottom: 1px solid #bee3f8; | |
}} | |
ul {{ | |
margin-top: 8px; | |
margin-left: 20px; | |
padding-left: 0; | |
}} | |
ul li {{ | |
margin-bottom: 6px; | |
list-style-type: square; | |
}} | |
/* Contact information section */ | |
.contact-info {{ | |
display: flex; | |
flex-wrap: wrap; | |
gap: 12px; | |
margin-bottom: 15px; | |
font-size: 10pt; | |
}} | |
.contact-info p {{ | |
margin: 0; | |
display: inline-block; | |
}} | |
/* Education section */ | |
.education-entry {{ | |
margin-bottom: 12px; | |
}} | |
/* Projects section */ | |
.project-title {{ | |
font-weight: 600; | |
}} | |
/* Skills section */ | |
.skills-list {{ | |
display: flex; | |
flex-wrap: wrap; | |
gap: 8px; | |
margin-top: 8px; | |
}} | |
.skill-item {{ | |
background-color: #e2e8f0; | |
padding: 3px 8px; | |
border-radius: 3px; | |
font-size: 9pt; | |
}} | |
/* Page break handling */ | |
.page-break {{ | |
page-break-after: always; | |
}} | |
/* Print-specific styles */ | |
@media print {{ | |
body {{ | |
padding: 0; | |
margin: 12mm 10mm 12mm 10mm; | |
}} | |
.resume-container {{ | |
box-shadow: none; | |
border: none; | |
}} | |
}} | |
</style> | |
</head> | |
<body> | |
<div class="resume-container"> | |
{html} | |
</div> | |
</body> | |
</html> | |
""" | |
# Configure PDF rendering options for better quality | |
options = { | |
'quiet': '', | |
'encoding': "UTF-8", | |
'enable-local-file-access': '', | |
'margin-top': '12mm', # 1.2 cm top margin | |
'margin-right': '10mm', # 1.0 cm right margin | |
'margin-bottom': '12mm', # 1.2 cm bottom margin | |
'margin-left': '10mm', # 1.0 cm left margin | |
'page-size': 'Letter', | |
'dpi': '300', | |
'image-quality': '100', | |
'enable-smart-shrinking': '', | |
} | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: | |
try: | |
pdfkit.from_string(full_html, tmp.name, options=options) | |
return FileResponse( | |
tmp.name, | |
media_type="application/pdf", | |
filename="resume.pdf" | |
) | |
except Exception as e: | |
# If wkhtmltopdf is not installed, raise a more specific error | |
error_message = str(e) | |
if "No wkhtmltopdf executable found" in error_message: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"PDF generation failed: {error_message}\nPlease install wkhtmltopdf - https://github.com/JazzCore/python-pdfkit/wiki/Installing-wkhtmltopdf" | |
) | |
else: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"PDF generation failed: {error_message}" | |
) | |
except HTTPException: | |
raise | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=f"PDF generation failed: {str(e)}" | |
) | |
# ========== Helper Functions ========== | |
def format_projects(projects: List[ProjectData]) -> str: | |
return "\n".join( | |
f"- {p.name} ({p.category}): {p.description}" | |
for p in projects | |
) | |
if __name__ == "__main__": | |
import uvicorn | |
# Get port from environment variable or use default 8000 7860 FOR HUGGINGFACE SPACES. | |
port = int(os.environ.get("PORT", 7860)) | |
uvicorn.run(app, host="0.0.0.0", port=port) |