Spaces:
Sleeping
Sleeping
| import re | |
| from datetime import datetime | |
| # ββ Resume Scoring ββββββββββββββββββββββββββββββββββββ | |
| def score_resume_vs_job(resume_text: str, job: dict) -> int: | |
| """ | |
| Score how well a resume matches a job. | |
| Returns integer 0β100. | |
| Pure keyword + heuristic scoring (no LLM cost). | |
| """ | |
| if not resume_text or not job: | |
| return 0 | |
| resume_lower = resume_text.lower() | |
| score = 0 | |
| max_score = 0 | |
| # 1. Job title words in resume (up to 30 pts) | |
| title_words = _tokenize(job.get("title", "")) | |
| title_hits = sum(1 for w in title_words if w in resume_lower) | |
| title_score = int((title_hits / max(len(title_words), 1)) * 30) | |
| score += title_score | |
| max_score += 30 | |
| # 2. Tags / tech stack match (up to 40 pts) | |
| tags = [t.lower() for t in job.get("tags", [])] | |
| tag_hits = sum(1 for t in tags if t in resume_lower) | |
| tag_score = int((tag_hits / max(len(tags), 1)) * 40) | |
| score += tag_score | |
| max_score += 40 | |
| # 3. Description keywords in resume (up to 20 pts) | |
| desc_words = _tokenize(job.get("description", "")) | |
| important = _extract_important_words(desc_words) | |
| desc_hits = sum(1 for w in important if w in resume_lower) | |
| desc_score = int((desc_hits / max(len(important), 1)) * 20) | |
| score += desc_score | |
| max_score += 20 | |
| # 4. Resume length bonus (up to 10 pts) | |
| word_count = len(resume_text.split()) | |
| if word_count > 300: | |
| score += 10 | |
| elif word_count > 150: | |
| score += 5 | |
| max_score += 10 | |
| # Normalize to 100 | |
| final = int((score / max_score) * 100) if max_score > 0 else 0 | |
| return min(final, 100) | |
| def rank_jobs(resume_text: str, jobs: list) -> list: | |
| """ | |
| Return jobs sorted by match score (highest first). | |
| Adds 'match_score' key to each job dict. | |
| """ | |
| scored = [] | |
| for job in jobs: | |
| job_copy = dict(job) | |
| job_copy["match_score"] = score_resume_vs_job(resume_text, job) | |
| scored.append(job_copy) | |
| scored.sort(key=lambda x: x["match_score"], reverse=True) | |
| return scored | |
| # ββ Text Helpers ββββββββββββββββββββββββββββββββββββββ | |
| def _tokenize(text: str) -> list: | |
| """Split text into lowercase word tokens, remove noise.""" | |
| words = re.findall(r'\b[a-zA-Z]{3,}\b', text.lower()) | |
| stopwords = { | |
| "the", "and", "for", "with", "this", "that", "are", "you", | |
| "will", "have", "our", "your", "from", "not", "but", "can", | |
| "all", "work", "team", "role", "join", "looking", "able", | |
| "must", "help", "well", "also", "more", "who", "what", | |
| "how", "when", "they", "their", "about", "into" | |
| } | |
| return [w for w in words if w not in stopwords] | |
| def _extract_important_words(words: list) -> list: | |
| """Return the most meaningful/unique words for matching.""" | |
| # Prioritize longer words (usually technical terms) | |
| return list(set([w for w in words if len(w) > 4]))[:40] | |
| def format_match_score(score: int) -> tuple: | |
| """Return (emoji, label, color) for a score.""" | |
| if score >= 80: | |
| return "π’", "Excellent Match", "green" | |
| elif score >= 60: | |
| return "π‘", "Good Match", "orange" | |
| elif score >= 40: | |
| return "π ", "Fair Match", "orange" | |
| else: | |
| return "π΄", "Low Match", "red" | |
| # ββ Date Helpers ββββββββββββββββββββββββββββββββββββββ | |
| def format_date(iso_string: str) -> str: | |
| """Convert ISO timestamp to human-readable.""" | |
| if not iso_string: | |
| return "Unknown" | |
| try: | |
| dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00")) | |
| return dt.strftime("%b %d, %Y at %I:%M %p") | |
| except Exception: | |
| return iso_string[:10] | |
| def days_ago(iso_string: str) -> str: | |
| """Return '3 days ago' style string.""" | |
| if not iso_string: | |
| return "" | |
| try: | |
| dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00")) | |
| delta = datetime.now(dt.tzinfo) - dt | |
| days = delta.days | |
| if days == 0: | |
| return "Today" | |
| elif days == 1: | |
| return "Yesterday" | |
| else: | |
| return f"{days} days ago" | |
| except Exception: | |
| return "" | |
| # ββ Resume Helpers ββββββββββββββββββββββββββββββββββββ | |
| def extract_name_from_resume(resume_text: str) -> str: | |
| """Best-effort extract name from first line of resume.""" | |
| if not resume_text: | |
| return "Candidate" | |
| first_line = resume_text.strip().split("\n")[0].strip() | |
| if len(first_line) < 40: | |
| return first_line | |
| return "Candidate" | |
| def truncate(text: str, max_chars: int = 300) -> str: | |
| """Truncate text with ellipsis.""" | |
| if not text: | |
| return "" | |
| if len(text) <= max_chars: | |
| return text | |
| return text[:max_chars].rstrip() + "..." | |
| def sanitize_for_json(text: str) -> str: | |
| """Remove characters that break JSON storage.""" | |
| return text.replace("\x00", "").strip() |