Spaces:
Sleeping
Sleeping
import gradio as gr | |
import os | |
import tempfile | |
from backend import ( | |
extract_text_from_document, | |
analyze_resume_job_match, | |
tailor_resume, | |
create_word_document, | |
create_tailored_resume_from_template, | |
get_available_templates, | |
set_openai_api_key, | |
set_openai_model | |
) | |
import plotly.graph_objects as go | |
import time as import_time | |
import json | |
# Define sample resumes | |
SAMPLE_RESUMES = { | |
"Excellent Match Resume": "excellent_match_resume.docx", | |
"Good Match Resume": "good_match_resume.docx", | |
"Average Match Resume": "average_match_resume.docx", | |
"Poor Match Resume": "poor_match_resume.docx" | |
} | |
# Check if sample files exist | |
for sample_name, sample_path in SAMPLE_RESUMES.items(): | |
if not os.path.exists(sample_path): | |
print(f"Warning: Sample resume file not found: {sample_path}") | |
# Read default job description | |
def get_default_job_description(): | |
try: | |
with open("job_desc.txt", "r") as file: | |
return file.read() | |
except: | |
return "Software Architect position requiring cloud expertise, microservices architecture, and leadership skills." | |
# Function to get color based on percentage | |
def get_color_for_percentage(percentage): | |
if percentage < 40: | |
return "#FF4B4B" # Red for poor match | |
elif percentage < 60: | |
return "#FFA500" # Orange for average match | |
elif percentage < 80: | |
return "#2E86C1" # Blue for good match | |
else: | |
return "#2ECC71" # Green for excellent match | |
# Function to create match gauge chart | |
def create_match_gauge(match_percentage): | |
if match_percentage < 40: | |
color = "#FF4B4B" # Red | |
elif match_percentage < 60: | |
color = "#FFA500" # Orange | |
elif match_percentage < 80: | |
color = "#2E86C1" # Blue | |
else: | |
color = "#2ECC71" # Green | |
fig = go.Figure(go.Indicator( | |
mode="gauge+number", | |
value=match_percentage, | |
domain={'x': [0, 1], 'y': [0, 1]}, | |
title={'text': "Match Percentage"}, | |
gauge={ | |
'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, | |
'bar': {'color': color}, | |
'bgcolor': "white", | |
'borderwidth': 2, | |
'bordercolor': "gray", | |
'steps': [ | |
{'range': [0, 40], 'color': 'rgba(255, 75, 75, 0.2)'}, # Light red | |
{'range': [40, 60], 'color': 'rgba(255, 165, 0, 0.2)'}, # Light orange | |
{'range': [60, 80], 'color': 'rgba(46, 134, 193, 0.2)'}, # Light blue | |
{'range': [80, 100], 'color': 'rgba(46, 204, 113, 0.2)'} # Light green | |
], | |
} | |
)) | |
fig.update_layout( | |
height=250, | |
margin=dict(l=20, r=20, t=50, b=20), | |
paper_bgcolor="rgba(0,0,0,0)", | |
plot_bgcolor="rgba(0,0,0,0)", | |
font={'color': "#444", 'family': "Arial"} | |
) | |
return fig | |
# Function to handle resume upload | |
def handle_resume_upload(file): | |
if file is None: | |
return None, None | |
# Save uploaded file | |
temp_dir = tempfile.mkdtemp() | |
temp_path = os.path.join(temp_dir, file.name) | |
with open(temp_path, "wb") as f: | |
f.write(file) | |
# Extract text | |
resume_text = extract_text_from_document(temp_path) | |
template_path = temp_path if temp_path.endswith('.docx') else None | |
return resume_text, template_path | |
# Function to handle sample resume selection | |
def handle_sample_resume(sample_name): | |
if not sample_name: | |
return None, None | |
resume_path = SAMPLE_RESUMES[sample_name] | |
if os.path.exists(resume_path): | |
# Extract text | |
resume_text = extract_text_from_document(resume_path) | |
template_path = resume_path | |
return resume_text, template_path | |
else: | |
print(f"Sample resume file not found: {resume_path}") | |
return None, None | |
# Function to handle template selection | |
def handle_template_selection(use_template, selected_template): | |
if use_template and selected_template != "None": | |
return selected_template | |
return None | |
# Function to analyze resume | |
def analyze_resume(resume_text, job_description, creativity_level): | |
if not resume_text or not job_description: | |
return None, "Please provide both a resume and job description." | |
try: | |
analysis_results = analyze_resume_job_match( | |
resume_text, | |
job_description, | |
creativity_level | |
) | |
# Create the match gauge chart | |
match_percentage = analysis_results.get("match_percentage", 0) | |
gauge_chart = create_match_gauge(match_percentage) | |
# Determine match category and description | |
if match_percentage < 40: | |
match_category = "Poor Match" | |
match_description = "Your resume needs significant improvements to match this job description." | |
elif match_percentage < 60: | |
match_category = "Average Match" | |
match_description = "Your resume partially matches the job description but could use improvements." | |
elif match_percentage < 80: | |
match_category = "Good Match" | |
match_description = "Your resume matches well with the job description with some room for improvement." | |
else: | |
match_category = "Excellent Match" | |
match_description = "Your resume is very well aligned with the job description!" | |
# Format the analysis results for display | |
formatted_results = f"## Match Analysis\n\n" | |
formatted_results += f"**Match Category:** {match_category}\n\n" | |
formatted_results += f"**Match Description:** {match_description}\n\n" | |
# Add skill breakdown | |
if "skill_breakdown" in analysis_results: | |
formatted_results += "## Skill Breakdown\n\n" | |
skill_breakdown = analysis_results["skill_breakdown"] | |
for skill_type, skill_data in skill_breakdown.items(): | |
formatted_results += f"### {skill_type.replace('_', ' ').title()}\n" | |
formatted_results += f"**Percentage:** {skill_data.get('percentage', 0)}%\n" | |
formatted_results += f"**Comments:** {skill_data.get('comments', '')}\n\n" | |
# Add key matches | |
if "key_matches" in analysis_results: | |
formatted_results += "## Key Matches\n\n" | |
for match in analysis_results["key_matches"]: | |
formatted_results += f"- {match}\n" | |
formatted_results += "\n" | |
# Add gaps | |
if "gaps" in analysis_results: | |
formatted_results += "## Gaps Identified\n\n" | |
for gap in analysis_results["gaps"]: | |
formatted_results += f"- {gap}\n" | |
formatted_results += "\n" | |
# Add suggestions | |
if "suggestions" in analysis_results: | |
formatted_results += "## Improvement Suggestions\n\n" | |
for suggestion in analysis_results["suggestions"]: | |
formatted_results += f"- {suggestion}\n" | |
formatted_results += "\n" | |
# Add summary | |
if "summary" in analysis_results: | |
formatted_results += "## Summary\n\n" | |
formatted_results += analysis_results["summary"] | |
return analysis_results, formatted_results | |
except Exception as e: | |
return None, f"Error analyzing resume: {str(e)}" | |
# Function to tailor resume | |
def tailor_resume_func(resume_text, job_description, template_path, creativity_level, verbosity): | |
if not resume_text or not job_description: | |
return None, "Please provide both a resume and job description." | |
try: | |
tailored_resume = tailor_resume( | |
resume_text, | |
job_description, | |
template_path, | |
creativity_level, | |
verbosity | |
) | |
return tailored_resume, "Resume tailored successfully!" | |
except Exception as e: | |
return None, f"Error tailoring resume: {str(e)}" | |
# Function to create and download Word document | |
def create_word_doc(tailored_resume, template_path): | |
if not tailored_resume: | |
return None, "No tailored resume to download." | |
try: | |
# Create a temporary file | |
temp_dir = tempfile.mkdtemp() | |
output_path = os.path.join(temp_dir, "tailored_resume.docx") | |
# Create Word document | |
if template_path and os.path.exists(template_path): | |
success = create_tailored_resume_from_template( | |
tailored_resume, | |
template_path, | |
output_path | |
) | |
else: | |
success = create_word_document( | |
tailored_resume, | |
output_path | |
) | |
if success: | |
return output_path, "DOCX file created successfully!" | |
else: | |
return None, "Failed to create DOCX file." | |
except Exception as e: | |
return None, f"Error creating DOCX file: {str(e)}" | |
# Function to update API key | |
def update_api_key(api_key): | |
if not api_key: | |
return "Using API key from environment variables if available." | |
api_configured = set_openai_api_key(api_key) | |
if api_configured: | |
return "β API key configured successfully!" | |
else: | |
return "β Failed to configure API key." | |
# Function to update model | |
def update_model(model): | |
set_openai_model(model) | |
return f"β Model set to {model}" | |
# Main function to create the Gradio interface | |
def create_interface(): | |
print("Creating interface...") | |
# Define the blocks with custom theme | |
with gr.Blocks(title="Resume Helper", theme=gr.themes.Soft( | |
primary_hue="blue", | |
secondary_hue="indigo", | |
font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "system-ui", "sans-serif"], | |
)) as app: | |
gr.Markdown("# π Resume Helper") | |
gr.Markdown("Upload your resume and get AI-powered analysis and tailoring to match job descriptions.") | |
# State variables | |
resume_text = gr.State(None) | |
template_path = gr.State(None) | |
analysis_results = gr.State(None) | |
tailored_resume_text = gr.State(None) | |
with gr.Row(): | |
# Left column - Inputs | |
with gr.Column(scale=1): | |
# Configuration section | |
with gr.Accordion("βοΈ Configuration", open=False): | |
api_key = gr.Textbox( | |
label="OpenAI API Key", | |
placeholder="Enter your OpenAI API key (optional)", | |
type="password" | |
) | |
api_status = gr.Markdown("Using API key from environment variables if available.") | |
api_key.change(update_api_key, inputs=[api_key], outputs=[api_status]) | |
model_options = ["gpt-4o-mini", "gpt-4o"] | |
model_selector = gr.Dropdown( | |
label="Select AI Model", | |
choices=model_options, | |
value="gpt-4o-mini", | |
info="Choose the OpenAI model to use. GPT-4o-mini is faster and cheaper, while GPT-4o provides more detailed analysis." | |
) | |
model_status = gr.Markdown("") | |
model_selector.change(update_model, inputs=[model_selector], outputs=[model_status]) | |
# Resume upload section | |
gr.Markdown("### π€ Upload Your Resume") | |
resume_option = gr.Radio( | |
label="Choose an option:", | |
choices=["Upload my resume", "Use a sample resume"], | |
value="Upload my resume" | |
) | |
# Upload resume file | |
upload_file = gr.File( | |
label="Upload your resume (DOCX, PDF)", | |
file_types=[".docx", ".pdf"], | |
visible=True | |
) | |
# Sample resume selection | |
sample_resume = gr.Dropdown( | |
label="Select a sample resume:", | |
choices=list(SAMPLE_RESUMES.keys()), | |
visible=False | |
) | |
# Show/hide based on selection | |
def update_resume_option(option): | |
return { | |
upload_file: gr.update(visible=option == "Upload my resume"), | |
sample_resume: gr.update(visible=option == "Use a sample resume") | |
} | |
resume_option.change(update_resume_option, inputs=[resume_option], outputs=[upload_file, sample_resume]) | |
# Template selection | |
gr.Markdown("### π Select Resume Template (Optional)") | |
templates = get_available_templates() | |
print(f"Templates: {templates}") | |
use_template = gr.Checkbox(label="Use a resume template", value=False) | |
template_selector = gr.Dropdown( | |
label="Choose a template:", | |
choices=["None"] + templates, | |
value="None", | |
visible=False | |
) | |
print(f"Template selector: ") | |
def update_template_visibility(use_template): | |
return gr.update(visible=use_template) | |
use_template.change(update_template_visibility, inputs=[use_template], outputs=[template_selector]) | |
# Job description | |
gr.Markdown("### π Job Description") | |
job_description = gr.Textbox( | |
label="Job Description", | |
value=get_default_job_description(), | |
lines=10 | |
) | |
# MOVED FROM RIGHT COLUMN: Resume detail level | |
gr.Markdown("### π Resume Detail Level") | |
verbosity = gr.Radio( | |
label="Choose how detailed your tailored resume should be:", | |
choices=["Concise", "Elaborate"], | |
value="Elaborate" | |
) | |
# MOVED FROM RIGHT COLUMN: Creativity level | |
gr.Markdown("### π¨ Creativity Level") | |
creativity_level = gr.Slider( | |
label="Adjust how creative the AI should be when tailoring your resume", | |
minimum=0, | |
maximum=100, | |
value=30, | |
step=10, | |
info="Higher values mean more creative modifications to your resume" | |
) | |
creativity_warning = gr.Markdown(visible=False) | |
def update_creativity_warning(level): | |
if level > 70: | |
return gr.update(visible=True, value="β οΈ High creativity levels may generate content that significantly modifies your original resume. Review carefully before using.") | |
else: | |
return gr.update(visible=False) | |
creativity_level.change(update_creativity_warning, inputs=[creativity_level], outputs=[creativity_warning]) | |
# MOVED FROM RIGHT COLUMN: Action buttons | |
gr.Markdown("### π Actions") | |
with gr.Row(): | |
analyze_btn = gr.Button("π Analyze Resume", variant="primary") | |
tailor_btn = gr.Button("βοΈ Tailor Resume", variant="primary") | |
reset_btn = gr.Button("π Reset All", variant="secondary") | |
# Add loading indicator below the buttons | |
loading_indicator = gr.Markdown(visible=False) | |
# Right column - Results | |
with gr.Column(scale=1): | |
# Results section - Now directly in the right column, not in an accordion | |
with gr.Tabs() as results_tabs: | |
# Analysis tab | |
with gr.TabItem("π Analysis"): | |
analysis_plot = gr.Plot(label="Match Percentage") | |
analysis_output = gr.Markdown() | |
# Tailored Resume tab | |
with gr.TabItem("π Tailored Resume"): | |
tailored_resume = gr.Textbox(label="Tailored Resume", lines=15) | |
# Create download buttons but initially hide them | |
with gr.Row(): | |
download_docx = gr.Button("π Download as DOCX", variant="primary", visible=False) | |
download_txt = gr.Button("π Download as TXT", variant="primary", visible=False) | |
# Create file components but initially hide them | |
docx_file = gr.File(label="Download DOCX", visible=False) | |
txt_file = gr.File(label="Download TXT", visible=False) | |
download_status = gr.Markdown() | |
# Event handlers | |
def process_resume_input(resume_opt, upload_file, sample_name, use_template_opt, template_selection): | |
if resume_opt == "Upload my resume" and upload_file is not None: | |
resume_text, template_path = handle_resume_upload(upload_file) | |
elif resume_opt == "Use a sample resume" and sample_name: | |
resume_text, template_path = handle_sample_resume(sample_name) | |
else: | |
resume_text, template_path = None, None | |
if use_template_opt and template_selection != "None": | |
template_path = template_selection | |
return resume_text, template_path | |
# Handle file upload | |
upload_file.upload( | |
lambda file: process_resume_input("Upload my resume", file, None, use_template.value, template_selector.value), | |
inputs=[upload_file], | |
outputs=[resume_text, template_path] | |
) | |
# Handle sample selection | |
sample_resume.change( | |
lambda sample: process_resume_input("Use a sample resume", None, sample, use_template.value, template_selector.value), | |
inputs=[sample_resume], | |
outputs=[resume_text, template_path] | |
) | |
# Handle template selection | |
template_selector.change( | |
lambda template, resume_txt, current_template: (resume_txt, template if template != "None" else current_template), | |
inputs=[template_selector, resume_text, template_path], | |
outputs=[resume_text, template_path] | |
) | |
# Analyze button handler with loading indicator | |
def analyze_with_loading(resume_txt, job_desc, creativity): | |
if not resume_txt: | |
return ( | |
gr.update(visible=False), | |
None, | |
gr.update(visible=False), | |
"", | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
# Show loading message and disable buttons | |
yield ( | |
gr.update(visible=True, value="β³ Analyzing your resume... This may take a moment."), | |
None, | |
gr.update(visible=False), | |
"", | |
gr.update(interactive=False), | |
gr.update(interactive=False), | |
gr.update(interactive=False) | |
) | |
# Perform the actual analysis | |
results, formatted_output = analyze_resume(resume_txt, job_desc, creativity) | |
# Hide loading, show results, and re-enable buttons | |
if results: | |
match_percentage = results.get("match_percentage", 0) | |
gauge_chart = create_match_gauge(match_percentage) | |
yield ( | |
gr.update(visible=False), | |
results, | |
gauge_chart, | |
formatted_output, | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
else: | |
yield ( | |
gr.update(visible=False), | |
None, | |
gr.update(visible=False), | |
formatted_output, | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
analyze_btn.click( | |
analyze_with_loading, | |
inputs=[resume_text, job_description, creativity_level], | |
outputs=[ | |
loading_indicator, | |
analysis_results, | |
analysis_plot, | |
analysis_output, | |
analyze_btn, | |
tailor_btn, | |
reset_btn | |
], | |
queue=True | |
) | |
# Tailor button handler with loading indicator | |
def tailor_with_loading(resume_txt, job_desc, template, creativity, verbosity_level): | |
if not resume_txt: | |
return ( | |
gr.update(visible=False), | |
None, | |
"", | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
# Show loading message and disable buttons | |
yield ( | |
gr.update(visible=True, value="β³ Tailoring your resume... This may take a moment."), | |
None, | |
"", | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(interactive=False), | |
gr.update(interactive=False), | |
gr.update(interactive=False) | |
) | |
# Perform the actual tailoring | |
tailored, message = tailor_resume_func( | |
resume_txt, | |
job_desc, | |
template, | |
creativity, | |
verbosity_level.lower() | |
) | |
# Hide loading, show results, and re-enable buttons | |
if tailored: | |
# Show download buttons only when tailored resume is created | |
yield ( | |
gr.update(visible=False), | |
tailored, | |
tailored, | |
gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
else: | |
# Hide download buttons if tailoring fails | |
yield ( | |
gr.update(visible=False), | |
None, | |
message, | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(interactive=True), | |
gr.update(interactive=True), | |
gr.update(interactive=True) | |
) | |
tailor_btn.click( | |
tailor_with_loading, | |
inputs=[resume_text, job_description, template_path, creativity_level, verbosity], | |
outputs=[ | |
loading_indicator, | |
tailored_resume_text, | |
tailored_resume, | |
download_docx, | |
download_txt, | |
analyze_btn, | |
tailor_btn, | |
reset_btn | |
], | |
queue=True | |
) | |
# Download handlers | |
def create_docx_handler(tailored_txt, template): | |
if not tailored_txt: | |
return gr.update(visible=False), "No tailored resume to download." | |
file_path, message = create_word_doc(tailored_txt, template) | |
if file_path: | |
return gr.update(visible=True, value=file_path), message | |
else: | |
return gr.update(visible=False), message | |
download_docx.click( | |
create_docx_handler, | |
inputs=[tailored_resume_text, template_path], | |
outputs=[docx_file, download_status] | |
) | |
# Download as TXT | |
def download_txt_handler(tailored_txt): | |
if not tailored_txt: | |
return gr.update(visible=False), "No tailored resume to download." | |
# Create a temporary file | |
temp_dir = tempfile.mkdtemp() | |
output_path = os.path.join(temp_dir, "tailored_resume.txt") | |
with open(output_path, "w") as f: | |
f.write(tailored_txt) | |
return gr.update(visible=True, value=output_path), "TXT file created successfully!" | |
download_txt.click( | |
download_txt_handler, | |
inputs=[tailored_resume_text], | |
outputs=[txt_file, download_status] | |
) | |
# Reset handler | |
def reset_all(): | |
return ( | |
None, None, None, None, | |
gr.update(value=None), | |
gr.update(value="Upload my resume"), | |
gr.update(value=None), | |
gr.update(value=None), | |
gr.update(value="None"), | |
gr.update(value=get_default_job_description()), | |
gr.update(value="Elaborate"), | |
gr.update(value=30), | |
gr.update(visible=False), | |
gr.update(value=""), | |
gr.update(value=""), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False) | |
) | |
reset_btn.click( | |
reset_all, | |
inputs=[], | |
outputs=[ | |
resume_text, template_path, analysis_results, tailored_resume_text, | |
upload_file, resume_option, sample_resume, use_template, template_selector, | |
job_description, verbosity, creativity_level, creativity_warning, | |
analysis_output, tailored_resume, | |
download_docx, download_txt, docx_file, txt_file | |
] | |
) | |
# Footer | |
gr.Markdown("---") | |
gr.Markdown("### π Disclaimer") | |
gr.Markdown(""" | |
This tool uses AI to analyze and tailor resumes. While it strives for accuracy, please review all generated content before using it professionally. | |
Higher creativity levels may generate content that requires more thorough verification. Always ensure that your resume accurately represents your skills and experience. | |
""") | |
print("Interface created successfully") | |
return app | |
# Launch the app | |
if __name__ == "__main__": | |
app = create_interface() | |
app.queue() # Enable the queue for the app | |
app.launch() | |