File size: 9,028 Bytes
cf8a522
92f45fe
7716c5c
8e1d297
92f45fe
 
cccaa8e
8e1d297
 
4c77f62
8e1d297
 
92f45fe
7716c5c
 
92f45fe
 
 
 
7716c5c
 
9753cc9
92f45fe
 
9753cc9
92f45fe
 
 
6637415
92f45fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e1d297
 
7716c5c
 
 
 
6637415
7716c5c
 
6637415
50528fd
 
 
 
7716c5c
 
 
 
50528fd
 
 
7716c5c
 
89f5ee9
 
7716c5c
89f5ee9
7716c5c
89f5ee9
7716c5c
 
 
 
d836318
6637415
7716c5c
 
6637415
 
cccaa8e
3661e7e
 
 
 
 
6637415
 
 
7716c5c
cccaa8e
3661e7e
 
 
6637415
 
50528fd
 
 
 
 
 
6637415
 
3661e7e
 
 
50528fd
6637415
 
 
cccaa8e
6637415
 
 
7716c5c
 
 
d836318
 
 
 
 
6637415
d836318
 
50528fd
d836318
50528fd
 
 
 
d836318
 
50528fd
 
6637415
50528fd
 
 
 
 
6637415
50528fd
 
d836318
 
cccaa8e
 
 
 
 
 
 
 
 
 
 
 
 
 
7716c5c
 
8e1d297
6088e9d
8e1d297
7716c5c
d836318
cccaa8e
 
 
 
 
 
 
 
 
 
8e1d297
 
586dcd2
8e1d297
cccaa8e
3661e7e
 
 
 
cccaa8e
3661e7e
 
8e1d297
3661e7e
7716c5c
8e1d297
3661e7e
d836318
8e1d297
3661e7e
8e1d297
d836318
3661e7e
 
cccaa8e
3661e7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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)")