Spaces:
Configuration error
Configuration error
Upload 4 files
Browse files- __init__.py +0 -0
- ai_summarizer.py +82 -0
- hf_api.py +27 -0
- resume_parser.py +78 -0
__init__.py
ADDED
|
File without changes
|
ai_summarizer.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional
|
| 2 |
+
from .hf_api import hf_summarize
|
| 3 |
+
|
| 4 |
+
def summarize_resume(resume_data: Dict, *, hf_model: str, hf_token: Optional[str]) -> str:
|
| 5 |
+
"""
|
| 6 |
+
Generate a summary of the resume using a Hugging Face text summarization model
|
| 7 |
+
via the Inference API.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
resume_data: Parsed resume dictionary containing 'raw_text' and optional fields.
|
| 11 |
+
hf_model: Model repo id, e.g. 'facebook/bart-large-cnn' or 'sshleifer/distilbart-cnn-12-6'.
|
| 12 |
+
hf_token: Optional HF API token. Required for rate limits or private models.
|
| 13 |
+
Returns:
|
| 14 |
+
str: Generated summary text.
|
| 15 |
+
"""
|
| 16 |
+
name = resume_data.get('name', 'The candidate')
|
| 17 |
+
skills: List[str] = resume_data.get('skills', []) or []
|
| 18 |
+
raw_text: str = resume_data.get('raw_text', '')
|
| 19 |
+
|
| 20 |
+
if not raw_text or len(raw_text.strip()) == 0:
|
| 21 |
+
return generate_template_summary(resume_data)
|
| 22 |
+
|
| 23 |
+
# The Inference API has input length limits; truncate long text conservatively.
|
| 24 |
+
# We'll keep about ~2000 characters to stay safe across most summarization models.
|
| 25 |
+
truncated = raw_text.strip()
|
| 26 |
+
if len(truncated) > 2000:
|
| 27 |
+
truncated = truncated[:2000]
|
| 28 |
+
|
| 29 |
+
prompt = truncated
|
| 30 |
+
try:
|
| 31 |
+
summary = hf_summarize(prompt, model=hf_model, api_token=hf_token)
|
| 32 |
+
except Exception:
|
| 33 |
+
# Fallback to template summary on any API failure
|
| 34 |
+
return generate_template_summary(resume_data)
|
| 35 |
+
|
| 36 |
+
# Optionally append skills
|
| 37 |
+
if skills:
|
| 38 |
+
summary = summary.strip() + f"\n\nKey Skills: {', '.join(skills[:10])}"
|
| 39 |
+
return summary
|
| 40 |
+
|
| 41 |
+
def generate_extractive_summary(*args, **kwargs) -> str:
|
| 42 |
+
"""Deprecated placeholder. We now use the Hugging Face Inference API."""
|
| 43 |
+
raise NotImplementedError("Use summarize_resume with hf_model and hf_token instead.")
|
| 44 |
+
|
| 45 |
+
def generate_template_summary(resume_data: Dict) -> str:
|
| 46 |
+
"""Generate a template-based summary when there's not enough text for extractive summarization."""
|
| 47 |
+
name = resume_data.get('name', 'The candidate')
|
| 48 |
+
skills = resume_data.get('skills', [])
|
| 49 |
+
|
| 50 |
+
summary = f"{name} "
|
| 51 |
+
|
| 52 |
+
if skills:
|
| 53 |
+
summary += f"is proficient in {', '.join(skills[:5])}."
|
| 54 |
+
else:
|
| 55 |
+
summary += "has submitted a resume for review."
|
| 56 |
+
|
| 57 |
+
return summary
|
| 58 |
+
|
| 59 |
+
# For testing
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
test_resume = {
|
| 62 |
+
'name': 'John Doe',
|
| 63 |
+
'email': 'john.doe@example.com',
|
| 64 |
+
'phone': '(123) 456-7890',
|
| 65 |
+
'skills': ['Python', 'Machine Learning', 'Data Analysis'],
|
| 66 |
+
'raw_text': """John Doe
|
| 67 |
+
Senior Data Scientist
|
| 68 |
+
|
| 69 |
+
Experienced data scientist with 5+ years of experience in machine learning and data analysis.
|
| 70 |
+
Proficient in Python, R, and SQL. Strong background in statistical modeling and data visualization.
|
| 71 |
+
|
| 72 |
+
Experience:
|
| 73 |
+
- Data Scientist at Tech Corp (2020-Present)
|
| 74 |
+
- Junior Data Analyst at Data Systems (2018-2020)
|
| 75 |
+
|
| 76 |
+
Education:
|
| 77 |
+
- MS in Computer Science, University of Technology
|
| 78 |
+
- BS in Mathematics, State University
|
| 79 |
+
"""
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
print(summarize_resume(test_resume))
|
hf_api.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from huggingface_hub import InferenceClient
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def hf_summarize(text: str, *, model: str, api_token: Optional[str] = None, max_new_tokens: int = 200) -> str:
|
| 6 |
+
"""
|
| 7 |
+
Call the Hugging Face Inference API for summarization.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
text: Input text to summarize.
|
| 11 |
+
model: Hugging Face model repo id (e.g., 'facebook/bart-large-cnn').
|
| 12 |
+
api_token: Optional HF API token for higher rate limits / private models.
|
| 13 |
+
max_new_tokens: Maximum tokens to generate in the summary.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
The summarized text.
|
| 17 |
+
"""
|
| 18 |
+
client = InferenceClient(model=model, token=api_token)
|
| 19 |
+
# The library returns either a string or a dict depending on the task helper used.
|
| 20 |
+
# Using the summarization task helper yields a string.
|
| 21 |
+
result = client.summarization(text, max_new_tokens=max_new_tokens)
|
| 22 |
+
if isinstance(result, str):
|
| 23 |
+
return result.strip()
|
| 24 |
+
# Fallback if a different payload format is returned
|
| 25 |
+
if isinstance(result, dict) and 'summary_text' in result:
|
| 26 |
+
return str(result['summary_text']).strip()
|
| 27 |
+
return str(result).strip()
|
resume_parser.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import PyPDF2
|
| 4 |
+
from docx import Document
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
def extract_text_from_pdf(file_path: str) -> str:
|
| 8 |
+
"""Extract text from PDF file."""
|
| 9 |
+
with open(file_path, 'rb') as file:
|
| 10 |
+
reader = PyPDF2.PdfReader(file)
|
| 11 |
+
text = ""
|
| 12 |
+
for page in reader.pages:
|
| 13 |
+
text += page.extract_text() + "\n"
|
| 14 |
+
return text
|
| 15 |
+
|
| 16 |
+
def extract_text_from_docx(file_path: str) -> str:
|
| 17 |
+
"""Extract text from DOCX file."""
|
| 18 |
+
doc = Document(file_path)
|
| 19 |
+
return "\n".join([paragraph.text for paragraph in doc.paragraphs])
|
| 20 |
+
|
| 21 |
+
def extract_contact_info(text: str) -> Dict[str, str]:
|
| 22 |
+
"""Extract contact information from text."""
|
| 23 |
+
# Email pattern
|
| 24 |
+
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
|
| 25 |
+
# Phone pattern (supports various formats)
|
| 26 |
+
phone_pattern = r'(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
|
| 27 |
+
|
| 28 |
+
email = re.search(email_pattern, text)
|
| 29 |
+
phone = re.search(phone_pattern, text)
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
'email': email.group(0) if email else '',
|
| 33 |
+
'phone': phone.group(0) if phone else ''
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
def extract_skills(text: str) -> List[str]:
|
| 37 |
+
"""Extract skills by keyword scan (lightweight, no external NLP)."""
|
| 38 |
+
skills_list = [
|
| 39 |
+
'python', 'java', 'javascript', 'c++', 'c#', 'sql', 'html', 'css',
|
| 40 |
+
'machine learning', 'deep learning', 'data science', 'data analysis',
|
| 41 |
+
'project management', 'team leadership', 'agile', 'scrum', 'devops',
|
| 42 |
+
'cloud', 'aws', 'azure', 'gcp', 'docker', 'kubernetes', 'git', 'rest api',
|
| 43 |
+
'graphql', 'mongodb', 'postgresql', 'mysql', 'tensorflow', 'pytorch',
|
| 44 |
+
'pandas', 'numpy', 'spark', 'hadoop', 'nlp'
|
| 45 |
+
]
|
| 46 |
+
text_lower = text.lower()
|
| 47 |
+
found = set()
|
| 48 |
+
for skill in skills_list:
|
| 49 |
+
if skill in text_lower:
|
| 50 |
+
found.add(skill)
|
| 51 |
+
return sorted(found)
|
| 52 |
+
|
| 53 |
+
def parse_resume(file_path: str) -> Dict:
|
| 54 |
+
"""Main function to parse resume and extract information."""
|
| 55 |
+
# Determine file type and extract text
|
| 56 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 57 |
+
|
| 58 |
+
if file_ext == '.pdf':
|
| 59 |
+
text = extract_text_from_pdf(file_path)
|
| 60 |
+
elif file_ext == '.docx':
|
| 61 |
+
text = extract_text_from_docx(file_path)
|
| 62 |
+
else:
|
| 63 |
+
raise ValueError("Unsupported file format. Please upload a PDF or DOCX file.")
|
| 64 |
+
|
| 65 |
+
# Extract information
|
| 66 |
+
contact_info = extract_contact_info(text)
|
| 67 |
+
skills = extract_skills(text)
|
| 68 |
+
|
| 69 |
+
# Extract name (simple approach - first line is often the name)
|
| 70 |
+
name = text.split('\n')[0].strip()
|
| 71 |
+
|
| 72 |
+
return {
|
| 73 |
+
'name': name,
|
| 74 |
+
'email': contact_info['email'],
|
| 75 |
+
'phone': contact_info['phone'],
|
| 76 |
+
'skills': skills,
|
| 77 |
+
'raw_text': text
|
| 78 |
+
}
|