CR7CAD's picture
Update app.py
89f5ee9 verified
raw
history blame
9.03 kB
import os
import tempfile
import re
import streamlit as st
import docx
import textract
from sentence_transformers import SentenceTransformer, util
#####################################
# Function: Extract Text from File
#####################################
def extract_text_from_file(file_obj):
"""
Extract text from .doc and .docx files.
Returns the extracted text or an error message if extraction fails.
"""
filename = file_obj.name
ext = os.path.splitext(filename)[1].lower()
text = ""
if ext == ".docx":
try:
document = docx.Document(file_obj)
text = "\n".join([para.text for para in document.paragraphs])
except Exception as e:
text = f"Error processing DOCX file: {e}"
elif ext == ".doc":
try:
# textract requires a file name; save the file temporarily.
with tempfile.NamedTemporaryFile(delete=False, suffix=".doc") as tmp:
tmp.write(file_obj.read())
tmp.flush()
tmp_filename = tmp.name
text = textract.process(tmp_filename).decode("utf-8")
except Exception as e:
text = f"Error processing DOC file: {e}"
finally:
try:
os.remove(tmp_filename)
except Exception:
pass
else:
text = "Unsupported file type."
return text
#####################################
# Function: Extract Basic Resume Information
#####################################
def extract_basic_resume_info(text):
"""
Parse the extracted text to extract/summarize:
- Name
- Age
- Job Experience (capturing the block under the "experience" section)
- Skills
- Education
Returns a dictionary with the extracted elements.
"""
info = {
"Name": None,
"Age": None,
"Job Experience": None,
"Skills": None,
"Education": None,
}
# Extract Name (e.g., "CONG, An Dong" from the first line)
name_match = re.search(r"^([A-Z]+)[,\s]+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)", text, re.MULTILINE)
if name_match:
info["Name"] = f"{name_match.group(1)} {name_match.group(2)}"
else:
# Fallback heuristic: assume a line with two or three capitalized words might be the candidate's name.
potential_names = re.findall(r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2}\b", text)
if potential_names:
info["Name"] = potential_names[0]
# Extract Age (e.g., "Age: 28")
age_match = re.search(r"[Aa]ge[:\-]\s*(\d{1,3})", text)
if age_match:
info["Age"] = age_match.group(1)
# Extract Job Experience using the "experience" section.
# Capture everything after the word "experience" until a new section or the end.
experience_match = re.search(
r"experience\s*(.*?)(?:\n\s*\n|additional information|skills|education|$)",
text,
re.IGNORECASE | re.DOTALL,
)
if experience_match:
job_experience = experience_match.group(1).strip()
info["Job Experience"] = " ".join(job_experience.split())
else:
# Fallback if not a labeled section.
exp_match = re.search(
r"(\d+)\s+(years|yrs)\s+(?:of\s+)?experience", text, re.IGNORECASE
)
if exp_match:
info["Job Experience"] = f"{exp_match.group(1)} {exp_match.group(2)}"
# Extract Skills (e.g., "Skills: Python, Java, SQL")
skills_match = re.search(r"(Skills|Technical Skills)[:\-]\s*(.+)", text, re.IGNORECASE)
if skills_match:
skills_str = skills_match.group(2).strip()
info["Skills"] = skills_str.rstrip(".")
# Extract Education (e.g., "Education: ...")
edu_match = re.search(
r"education\s*(.*?)(?:\n\s*\n|experience|$)", text, re.IGNORECASE | re.DOTALL
)
if edu_match:
education_block = edu_match.group(1).strip()
info["Education"] = " ".join(education_block.split())
else:
# Fallback: search for common degree identifiers.
edu_match = re.search(r"(Bachelor|Master|B\.Sc|M\.Sc|Ph\.D)[^\n]+", text)
if edu_match:
info["Education"] = edu_match.group(0)
return info
#####################################
# Function: Summarize Basic Info into a Paragraph
#####################################
def summarize_basic_info(info):
"""
Combine the extracted resume elements into a concise summary paragraph.
"""
parts = []
if info.get("Name"):
parts.append(f"Candidate {info['Name']}")
else:
parts.append("The candidate")
if info.get("Age"):
parts.append(f"aged {info['Age']}")
if info.get("Job Experience"):
parts.append(f"with job experience: {info['Job Experience']}")
if info.get("Skills"):
parts.append(f"skilled in {info['Skills']}")
if info.get("Education"):
parts.append(f"and educated in {info['Education']}")
summary_paragraph = ", ".join(parts) + "."
return summary_paragraph
#####################################
# Function: Compare Candidate Summary to Company Prompt
#####################################
def compute_suitability(candidate_summary, company_prompt, model):
"""
Compute the cosine similarity between candidate summary and company prompt embeddings.
Returns a score in the range [0, 1].
"""
candidate_embed = model.encode(candidate_summary, convert_to_tensor=True)
company_embed = model.encode(company_prompt, convert_to_tensor=True)
cosine_sim = util.cos_sim(candidate_embed, company_embed)
score = float(cosine_sim.item())
return score
#####################################
# Main Resume Processing Logic
#####################################
def process_resume(file_obj):
resume_text = extract_text_from_file(file_obj)
basic_info = extract_basic_resume_info(resume_text)
summary_paragraph = summarize_basic_info(basic_info)
return summary_paragraph
#####################################
# Load the Sentence-BERT Model
#####################################
@st.cache_resource(show_spinner=False)
def load_model():
return SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
model = load_model()
#####################################
# Streamlit Interface
#####################################
st.title("Resume Analyzer and Company Suitability Checker")
st.markdown(
"""
Upload your resume file in **.doc** or **.docx** format. The app extracts key details such as name, age, job experience, skills,
and education, and summarizes them into a single paragraph. Then, it compares the candidate summary with a company profile
(using a pre-defined prompt for Google LLC) to produce a suitability score.
"""
)
# File uploader for resume
uploaded_file = st.file_uploader("Upload Resume", type=["doc", "docx"])
# Button to process the resume and store the summary in session state.
if st.button("Process Resume"):
if uploaded_file is None:
st.error("Please upload a resume file first.")
else:
with st.spinner("Processing resume..."):
candidate_summary = process_resume(uploaded_file)
st.session_state["candidate_summary"] = candidate_summary
st.subheader("Candidate Summary")
st.markdown(candidate_summary)
# Pre-define the company prompt for Google LLC.
default_company_prompt = (
"Google LLC, a global leader in technology and innovation, specializes in internet services, cloud computing, "
"artificial intelligence, and software development. As part of Alphabet Inc., Google seeks candidates with strong "
"problem-solving skills, adaptability, and collaboration abilities. Technical roles require proficiency in programming "
"languages such as Python, Java, C++, Go, or JavaScript, with expertise in data structures, algorithms, and system design. "
"Additionally, skills in AI, cybersecurity, UX/UI design, and digital marketing are highly valued. Google fosters a culture "
"of innovation, expecting candidates to demonstrate creativity, analytical thinking, and a passion for cutting-edge technology."
)
# Company prompt text area.
company_prompt = st.text_area(
"Enter company details:",
value=default_company_prompt,
height=150,
)
# Button to compute the suitability score.
if st.button("Compute Suitability Score"):
if "candidate_summary" not in st.session_state:
st.error("Please process the resume first!")
else:
candidate_summary = st.session_state["candidate_summary"]
if candidate_summary.strip() == "":
st.error("Candidate summary is empty; please check your resume file.")
elif company_prompt.strip() == "":
st.error("Please enter the company information.")
else:
with st.spinner("Computing suitability score..."):
score = compute_suitability(candidate_summary, company_prompt, model)
st.success(f"Suitability Score: {score:.2f} (range 0 to 1)")