jaothan's picture
Upload 18 files
324e9cf verified
import gradio as gr
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.core.os_manager import ChromeType
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from openai import OpenAI
import time
from docx import Document
from dotenv import load_dotenv
import tempfile
# Load environment variables
load_dotenv(override=True)
# LinkedIn logo as an SVG string
LINKEDIN_LOGO = """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="50" height="50">
<path fill="#0A66C2" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"/>
</svg>
"""
class LinkedInJDScraper:
def __init__(self, email, password):
"""Initialize the LinkedIn scraper with credentials"""
try:
options = webdriver.ChromeOptions()
options.add_argument('--disable-blink-features=AutomationControlled')
options.add_argument('--start-maximized')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--remote-debugging-port=9222')
# Use system ChromeDriver
service = Service(executable_path='/usr/bin/chromedriver')
self.driver = webdriver.Chrome(service=service, options=options)
self.wait = WebDriverWait(self.driver, 10)
self.email = email
self.password = password
except Exception as e:
raise Exception(f"Failed to initialize Chrome driver: {str(e)}")
def login(self):
"""Login to LinkedIn"""
try:
self.driver.get("https://www.linkedin.com/login")
time.sleep(3) # Give page time to load
# Clear any existing data
self.driver.delete_all_cookies()
# Add additional headers
self.driver.execute_cdp_cmd('Network.setUserAgentOverride', {
"userAgent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
})
email_field = self.wait.until(
EC.presence_of_element_located((By.ID, "username"))
)
email_field.clear()
email_field.send_keys(self.email)
password_field = self.driver.find_element(By.ID, "password")
password_field.clear()
password_field.send_keys(self.password)
login_button = self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
self.driver.execute_script("arguments[0].click();", login_button)
time.sleep(5) # Give more time for login to complete
# Verify login success
if "feed" in self.driver.current_url or "mynetwork" in self.driver.current_url:
return True
else:
raise Exception("Login verification failed")
except Exception as e:
return f"Login failed: {str(e)}"
def get_description(self, job_url):
"""Scrape job description from LinkedIn"""
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
self.driver.get(job_url)
time.sleep(5) # Give more time for the page to load
# Check if we're still logged in
if "login" in self.driver.current_url.lower():
self.login()
self.driver.get(job_url)
time.sleep(5)
# Wait for any one of these selectors to be present
description_selectors = [
"div.jobs-description",
"div.jobs-description-content",
"div.jobs-box__html-content",
"div.show-more-less-html__markup"
]
description_element = None
for selector in description_selectors:
try:
description_element = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
if description_element:
break
except:
continue
if not description_element:
raise NoSuchElementException("Could not find job description element")
# Try to expand the description if there's a "show more" button
try:
show_more_button = self.driver.find_element(By.CSS_SELECTOR,
"button.show-more-less-html__button")
if show_more_button.is_displayed():
self.driver.execute_script("arguments[0].click();", show_more_button)
time.sleep(2)
except:
pass
# Get the text content
description = description_element.text
if description:
# Try to clean up the description
description = description.strip()
# Look for common section markers
markers = ["about the job", "job description", "about this role"]
for marker in markers:
start_index = description.lower().find(marker)
if start_index != -1:
description = description[start_index:]
break
return description
retry_count += 1
time.sleep(3) # Increased wait between retries
except Exception as e:
print(f"Attempt {retry_count + 1} failed: {str(e)}")
retry_count += 1
time.sleep(3)
return "Failed to retrieve job description after multiple attempts"
def read_resume(file_obj):
"""Read resume content from uploaded file"""
try:
# Get the file path directly from the Gradio file object
file_path = file_obj.name
# Read the document directly from the path
doc = Document(file_path)
text = []
for paragraph in doc.paragraphs:
if paragraph.text.strip():
text.append(paragraph.text.strip())
return "\n".join(text)
except Exception as e:
return f"Error reading resume: {str(e)}"
def generate_cover_letter(linkedin_email, linkedin_password, job_url, resume_file):
"""Generate cover letter based on job description and resume"""
# Input validation
if not all([linkedin_email, linkedin_password, job_url]):
return "Please fill in all required fields (LinkedIn email, password, and job URL)"
if not resume_file:
return "Please upload a resume file"
try:
# Initialize scraper and get job description
scraper = LinkedInJDScraper(linkedin_email, linkedin_password)
login_result = scraper.login()
if isinstance(login_result, str) and "failed" in login_result.lower():
scraper.close()
return login_result
# Get job description
job_description = scraper.get_description(job_url)
scraper.close()
if "Failed to retrieve" in job_description:
return job_description
# Read resume
resume_content = read_resume(resume_file)
if "Error reading resume" in resume_content:
return resume_content
# Prepare prompts
system_prompt = """
You are a career assistant specialized in crafting professional and personalized cover letters.
Your goal is to create compelling, tailored cover letters that align with the job description.
Each cover letter should emphasize the user's qualifications, skills, and experiences while maintaining a professional tone and structure.
"""
user_prompt = f"""
Create a professional cover letter based on:
Resume content:
{resume_content}
Job Description:
{job_description}
Make the cover letter specific to the role and highlight relevant experience.
"""
# Generate cover letter using OpenAI
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
return "OpenAI API key not found. Please check your .env file."
client = OpenAI(api_key=api_key)
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
return response.choices[0].message.content
except Exception as e:
return f"Error generating cover letter: {str(e)}"
finally:
# Ensure browser is closed
if 'scraper' in locals():
scraper.close()
def create_gradio_interface():
"""Create and configure the Gradio interface"""
with gr.Blocks(title="LinkedIn Cover Letter Generator") as app:
# Header with logo and title
with gr.Row():
with gr.Column(scale=1):
gr.HTML(LINKEDIN_LOGO)
with gr.Column(scale=4):
gr.Markdown("# LinkedIn Cover Letter Generator")
with gr.Row():
with gr.Column():
linkedin_email = gr.Textbox(
label="LinkedIn Email",
placeholder="Enter your LinkedIn email"
)
linkedin_password = gr.Textbox(
label="LinkedIn Password",
type="password",
placeholder="Enter your LinkedIn password"
)
job_url = gr.Textbox(
label="LinkedIn Job URL",
placeholder="Paste the LinkedIn job posting URL"
)
resume_file = gr.File(
label="Upload Resume (DOCX)",
file_types=[".docx"]
)
generate_button = gr.Button("Generate Cover Letter", variant="primary")
with gr.Column():
output = gr.Markdown(label="Generated Cover Letter")
generate_button.click(
fn=generate_cover_letter,
inputs=[linkedin_email, linkedin_password, job_url, resume_file],
outputs=output
)
return app
# Main execution
if __name__ == "__main__":
# Create and launch the Gradio app
app = create_gradio_interface()
app.launch(share=True)