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 = """ """ 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)