""" ATS-Proof CV Generator — Main Application """ import os import gradio as gr import traceback import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) from cv_parser import parse_cv, parse_cv_from_text from keyword_extractor import extract_all_keywords, format_keywords_report from cv_generator import generate_cv_full, generate_cv_batch from ats_checker import run_ats_check, format_ats_report from feedback_loop import refine_loop from docx_generator import save_pdf_to_file def run_pipeline(pdf_file, cv_text_input, job_description, ats_threshold, max_iterations, model_choice): """Main pipeline. Yields 5-tuples for progressive output.""" try: hf_token = os.environ.get("HF_TOKEN", "") if not hf_token: yield ("❌ HF_TOKEN not set. Add it in Space Settings → Secrets.", "", "", "", None) return if not job_description or not job_description.strip(): yield ("❌ Please provide a Job Description.", "", "", "", None) return # Phase 1: Parse CV logger.info("Phase 1: Parsing CV...") yield ("⏳ Phase 1/6: Parsing CV...", "", "", "", None) if pdf_file: parsed_cv = parse_cv(pdf_file) elif cv_text_input and cv_text_input.strip(): parsed_cv = parse_cv_from_text(cv_text_input) else: yield ("❌ Please upload a PDF or paste your CV text.", "", "", "", None) return raw_text = parsed_cv.get("raw_text", "") if len(raw_text.strip()) < 50: yield ("❌ Not enough text in CV. Try pasting it as text.", "", "", "", None) return cv_info = ( f"✅ CV Parsed\n" f" Name: {parsed_cv.get('name', 'N/A')}\n" f" Contact: {parsed_cv.get('contact', 'N/A')}\n" f" Sections: {', '.join(parsed_cv.get('sections', {}).keys())}\n" f" Length: {len(raw_text)} chars" ) logger.info(f"CV parsed: {parsed_cv.get('name', '?')}, {len(raw_text)} chars") yield (cv_info, "", "", "", None) # Phase 2: Extract Keywords logger.info("Phase 2: Extracting keywords...") yield (cv_info, "", "⏳ Extracting keywords...", "", None) try: keywords_data = extract_all_keywords(job_description=job_description, resume_text=raw_text) keywords_report = format_keywords_report(keywords_data) logger.info(f"Keywords: {len(keywords_data.get('required_keywords', []))} required, {len(keywords_data.get('missing_keywords', []))} missing") except Exception as e: logger.error(f"Keyword extraction failed: {e}") keywords_data = {"ner_skills": {}, "keybert_keywords": [], "tfidf_terms": [], "all_keywords_flat": [], "required_keywords": [], "resume_keywords": [], "missing_keywords": []} keywords_report = f"⚠️ Keyword extraction error: {str(e)}" yield (cv_info, "", keywords_report, "", None) # Phase 3: Generate CV logger.info("Phase 3: Generating CV...") model_map = { "Qwen 2.5 72B (Best Quality)": "Qwen/Qwen2.5-72B-Instruct", "Qwen 2.5 7B (Fast)": "Qwen/Qwen2.5-7B-Instruct", "Llama 3.1 8B": "meta-llama/Llama-3.1-8B-Instruct", "Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3", } model_id = model_map.get(model_choice, "Qwen/Qwen2.5-72B-Instruct") logger.info(f"Using model: {model_id}") yield (cv_info, "⏳ Calling LLM...", keywords_report, "", None) initial_cv = "" try: for partial_cv in generate_cv_full(parsed_cv, keywords_data, job_description, hf_token, model_id): initial_cv = partial_cv yield (cv_info, partial_cv, keywords_report, "⏳ Generating CV...", None) except Exception as e: logger.error(f"CV generation error: {e}\n{traceback.format_exc()}") yield (cv_info, f"❌ Generation failed: {str(e)}\n\nTry 'Qwen 2.5 7B (Fast)'.", keywords_report, "", None) return if not initial_cv or len(initial_cv.strip()) < 50: yield (cv_info, "❌ LLM returned empty response. Try a different model.", keywords_report, "", None) return logger.info(f"CV generated: {len(initial_cv)} chars") # Phase 4: ATS Check logger.info("Phase 4: ATS check...") yield (cv_info, initial_cv, keywords_report, "⏳ Running ATS check...", None) initial_report = run_ats_check(initial_cv, job_description, keywords_data) initial_score = initial_report["total_score"] ats_text = format_ats_report(initial_report) logger.info(f"ATS score: {initial_score}") yield (cv_info, initial_cv, keywords_report, ats_text, None) # Phase 5: Refine Loop threshold = int(ats_threshold) max_iter = int(max_iterations) if initial_score < threshold and max_iter > 0: logger.info(f"Phase 5: Refining (score {initial_score} < {threshold})...") yield (cv_info, initial_cv, keywords_report, f"{ats_text}\n\n🔄 Score {initial_score} < {threshold}. Refining...", None) last_cv = initial_cv try: for status_msg, current_cv in refine_loop(initial_cv, parsed_cv, keywords_data, job_description, hf_token, model_id, threshold, max_iter): last_cv = current_cv yield (cv_info, current_cv, keywords_report, status_msg, None) except Exception as e: logger.error(f"Refine error: {e}") final_cv = last_cv final_report = run_ats_check(final_cv, job_description, keywords_data) final_ats_text = format_ats_report(final_report) logger.info(f"Final ATS score: {final_report['total_score']}") else: final_cv = initial_cv final_ats_text = ats_text # Phase 6: Generate PDF (1-page) logger.info("Phase 6: Generating 1-page PDF...") yield (cv_info, final_cv, keywords_report, final_ats_text, None) try: pdf_path = save_pdf_to_file(final_cv) logger.info(f"PDF saved: {pdf_path}") except Exception as e: pdf_path = None logger.error(f"PDF error: {e}\n{traceback.format_exc()}") yield (cv_info, final_cv, keywords_report, final_ats_text, pdf_path) logger.info("Pipeline complete!") except Exception as e: error_msg = f"❌ Unexpected error: {str(e)}\n\n{traceback.format_exc()}" logger.error(error_msg) yield (error_msg, "", "", "", None) def quick_ats_score(cv_text, job_description): if not cv_text or not job_description: return "Please provide both CV text and Job Description." try: keywords_data = extract_all_keywords(job_description, cv_text) report = run_ats_check(cv_text, job_description, keywords_data) return format_ats_report(report) except Exception as e: return f"Error: {str(e)}" DESCRIPTION = """ # 🎯 ATS-Proof CV Generator **Closed-loop pipeline** that generates ATS-optimized, **1-page PDF** resumes with a self-refining feedback loop. ### How it works: 1. 📄 **Parse** your CV (PDF upload or paste text) 2. 🔍 **Extract** keywords from the Job Description 3. ✍️ **Generate** a tailored CV — drops irrelevant content, keeps what matters 4. 📊 **Score** against the JD (5-metric hybrid ATS checker) 5. 🔄 **Refine** iteratively until the score hits the target 6. 📥 **Download** as a clean **1-page PDF** > ⚠️ **Requires HF_TOKEN** — Add in Space Settings → Secrets """ EXAMPLE_JD = """Senior Data Scientist — TechCorp Inc. We are looking for an experienced Data Scientist to join our AI team. Requirements: - 5+ years of experience in data science or machine learning - Strong proficiency in Python, SQL, and statistical analysis - Experience with deep learning frameworks (TensorFlow, PyTorch) - Knowledge of NLP, computer vision, or recommendation systems - Experience with cloud platforms (AWS, GCP, or Azure) - Strong communication skills and ability to present to stakeholders - Experience with A/B testing and experimentation - Familiarity with MLOps tools (MLflow, Kubeflow, Docker) - Master's or PhD in Computer Science, Statistics, or related field Responsibilities: - Lead end-to-end ML projects from problem definition to deployment - Build and optimize predictive models for business applications - Collaborate with engineering teams to productionize ML models - Mentor junior data scientists - Design and analyze A/B experiments - Present findings to C-level executives Nice to have: - Experience with LLMs and generative AI - Publications in top ML conferences - Experience with Spark or distributed computing """ with gr.Blocks(title="ATS-Proof CV Generator") as demo: gr.Markdown(DESCRIPTION) with gr.Tabs(): with gr.Tab("🚀 Generate ATS CV"): with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📄 Your Resume") pdf_input = gr.File(label="Upload PDF Resume", file_types=[".pdf"], type="filepath") cv_text_input = gr.Textbox(label="Or Paste CV Text", placeholder="Paste your resume text here...", lines=8) gr.Markdown("### 💼 Job Description") jd_input = gr.Textbox(label="Target Job Description", placeholder="Paste the full job description...", lines=10, value=EXAMPLE_JD) gr.Markdown("### ⚙️ Settings") with gr.Row(): threshold_slider = gr.Slider(minimum=60, maximum=95, value=85, step=5, label="ATS Score Target") iteration_slider = gr.Slider(minimum=1, maximum=4, value=3, step=1, label="Max Refinement Iterations") model_dropdown = gr.Dropdown( choices=["Qwen 2.5 72B (Best Quality)", "Qwen 2.5 7B (Fast)", "Llama 3.1 8B", "Mistral 7B"], value="Qwen 2.5 72B (Best Quality)", label="LLM Model", ) generate_btn = gr.Button("🎯 Generate ATS-Proof CV", variant="primary", size="lg") with gr.Column(scale=2): cv_info_output = gr.Textbox(label="ℹ️ Status / CV Info", lines=6) cv_output = gr.Textbox(label="📝 ATS-Optimized CV", lines=25, buttons=["copy"]) keywords_output = gr.Textbox(label="🔍 Keyword Analysis", lines=15) ats_output = gr.Textbox(label="📊 ATS Score Report", lines=15) pdf_output = gr.File(label="📥 Download 1-Page PDF") generate_btn.click( fn=run_pipeline, inputs=[pdf_input, cv_text_input, jd_input, threshold_slider, iteration_slider, model_dropdown], outputs=[cv_info_output, cv_output, keywords_output, ats_output, pdf_output], ) with gr.Tab("📊 Quick ATS Score"): gr.Markdown("### Quick ATS Score Check\nNo LLM needed — instant keyword + TF-IDF scoring.") with gr.Row(): with gr.Column(): score_cv_input = gr.Textbox(label="Your CV Text", placeholder="Paste your CV here...", lines=15) with gr.Column(): score_jd_input = gr.Textbox(label="Job Description", placeholder="Paste the job description...", lines=15) score_btn = gr.Button("📊 Check ATS Score", variant="primary") score_output = gr.Textbox(label="ATS Score Report", lines=20) score_btn.click(fn=quick_ats_score, inputs=[score_cv_input, score_jd_input], outputs=score_output) demo.queue(max_size=10).launch(ssr_mode=False)