DreamStream-1 commited on
Commit
50d5b44
1 Parent(s): 2080e4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -139
app.py CHANGED
@@ -1,28 +1,17 @@
1
  import os
2
  import re
 
3
  from datetime import datetime
4
  import PyPDF2
5
  import torch
6
  from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModelForSeq2SeqLM
7
- from sentence_transformers import SentenceTransformer, util
8
  import gradio as gr
 
9
 
10
- # --- Model Loading with Caching --- #
11
- class ModelCache:
12
- _tokenizers = {}
13
- _models = {}
14
-
15
- @classmethod
16
- def get_model(cls, model_name):
17
- if model_name not in cls._models:
18
- cls._models[model_name] = AutoModelForSeq2SeqLM.from_pretrained(model_name)
19
- return cls._models[model_name]
20
-
21
- @classmethod
22
- def get_tokenizer(cls, model_name):
23
- if model_name not in cls._tokenizers:
24
- cls._tokenizers[model_name] = AutoTokenizer.from_pretrained(model_name)
25
- return cls._tokenizers[model_name]
26
 
27
  # --- PDF/Text Extraction Functions --- #
28
  def extract_text_from_file(file_path):
@@ -35,98 +24,73 @@ def extract_text_from_file(file_path):
35
  raise ValueError("Unsupported file type. Only PDF and TXT files are accepted.")
36
 
37
  def extract_text_from_pdf(pdf_file_path):
38
- """Extracts text from a PDF file with logging for page extraction."""
39
- text = []
40
  with open(pdf_file_path, 'rb') as pdf_file:
41
  pdf_reader = PyPDF2.PdfReader(pdf_file)
42
- for i, page in enumerate(pdf_reader.pages):
43
- page_text = page.extract_text()
44
- if page_text:
45
- text.append(page_text)
46
- else:
47
- print(f"Warning: Page {i} could not be extracted.")
48
- return ''.join(text)
49
 
50
  def extract_text_from_txt(txt_file_path):
51
  """Extracts text from a .txt file."""
52
  with open(txt_file_path, 'r', encoding='utf-8') as txt_file:
53
  return txt_file.read()
54
 
55
- # --- Skill Extraction with Hugging Face --- #
56
- def extract_skills_huggingface(text):
57
- """Extracts skills from the text using a Hugging Face model."""
58
- model_name = "google/flan-t5-base"
59
- tokenizer = ModelCache.get_tokenizer(model_name)
60
- model = ModelCache.get_model(model_name)
61
-
62
- input_text = f"Extract skills from the following text: {text}"
63
- inputs = tokenizer(input_text, return_tensors="pt", padding=True, truncation=True)
64
- outputs = model.generate(**inputs)
65
- skills = tokenizer.decode(outputs[0], skip_special_tokens=True).split(', ') # Expecting a comma-separated list
66
- return skills
67
-
68
- # --- Job Description Processing Function --- #
69
- def process_job_description(text):
70
- """Extracts skills or relevant keywords from the job description."""
71
- return extract_skills_huggingface(text)
72
-
73
- # --- Qualification and Experience Extraction --- #
74
- def extract_qualifications(text):
75
- """Extracts qualifications from text (e.g., degrees, certifications)."""
76
- qualifications = re.findall(r'\b(bachelor|master|phd|certified|degree|diploma|qualification|certification)\b', text, re.IGNORECASE)
77
- return qualifications if qualifications else ['No specific qualifications found']
78
-
79
- def extract_experience(text):
80
- """Extracts years of experience from the text."""
81
- experience_years = re.findall(r'(\d+)\s*(years|year) of experience', text, re.IGNORECASE)
82
- job_titles = re.findall(r'\b(software engineer|developer|manager|analyst)\b', text, re.IGNORECASE)
83
- experience_years = [int(year[0]) for year in experience_years]
84
- return experience_years, job_titles
85
-
86
- # --- Semantic Similarity Calculation --- #
87
- def calculate_semantic_similarity(text1, text2):
88
- """Calculates semantic similarity using a sentence transformer model and returns the score as a percentage."""
89
- model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
90
- embeddings1 = model.encode(text1, convert_to_tensor=True)
91
- embeddings2 = model.encode(text2, convert_to_tensor=True)
92
- similarity_score = util.pytorch_cos_sim(embeddings1, embeddings2).item()
93
-
94
- # Convert similarity score to percentage
95
- similarity_percentage = similarity_score * 100
96
- return similarity_percentage
97
-
98
- # --- Thresholds --- #
99
- def categorize_similarity(score):
100
- """Categorizes the similarity score into thresholds for better insights."""
101
- if score >= 80:
102
- return "High Match"
103
- elif score >= 50:
104
- return "Moderate Match"
105
- else:
106
- return "Low Match"
107
 
108
- # --- Communication Generation with Enhanced Response --- #
109
- def communication_generator(resume_skills, job_description_skills, skills_similarity, qualifications_similarity, experience_similarity, max_length=200):
110
- """Generates a more detailed communication response based on similarity scores."""
111
  model_name = "google/flan-t5-base"
112
- tokenizer = ModelCache.get_tokenizer(model_name)
113
- model = ModelCache.get_model(model_name)
114
 
115
- # Assess candidate fit based on similarity scores
116
- fit_status = "strong fit" if skills_similarity >= 80 and qualifications_similarity >= 80 and experience_similarity >= 80 else \
117
- "moderate fit" if skills_similarity >= 50 else "weak fit"
118
 
119
- # Create a detailed communication message based on match levels
120
  message = (
121
- f"After a detailed analysis of the candidate's resume, we found the following insights:\n\n"
122
- f"- **Skills Match**: {skills_similarity:.2f}% ({categorize_similarity(skills_similarity)})\n"
123
- f"- **Qualifications Match**: {qualifications_similarity:.2f}% ({categorize_similarity(qualifications_similarity)})\n"
124
- f"- **Experience Match**: {experience_similarity:.2f}% ({categorize_similarity(experience_similarity)})\n\n"
125
- f"The overall assessment indicates that the candidate is a {fit_status} for the role. "
126
- f"Skills such as {', '.join(resume_skills)} align {categorize_similarity(skills_similarity).lower()} with the job's requirements of {', '.join(job_description_skills)}. "
127
- f"In terms of qualifications and experience, the candidate shows a {categorize_similarity(qualifications_similarity).lower()} match with the role's needs. "
128
- f"Based on these findings, we believe the candidate could potentially excel in the role, "
129
- f"but additional evaluation or interviews are recommended for further clarification."
130
  )
131
 
132
  inputs = tokenizer(message, return_tensors="pt", padding=True, truncation=True)
@@ -136,10 +100,10 @@ def communication_generator(resume_skills, job_description_skills, skills_simila
136
 
137
  # --- Sentiment Analysis --- #
138
  def sentiment_analysis(text):
139
- """Analyzes the sentiment of the text using a Hugging Face model."""
140
- model_name = "distilbert-base-uncased-finetuned-sst-2-english"
141
- tokenizer = ModelCache.get_tokenizer(model_name)
142
- model = ModelCache.get_model(model_name)
143
 
144
  inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
145
  with torch.no_grad():
@@ -147,63 +111,70 @@ def sentiment_analysis(text):
147
  predicted_sentiment = torch.argmax(outputs.logits).item()
148
  return ["Negative", "Neutral", "Positive"][predicted_sentiment]
149
 
150
- # --- Updated Resume Analysis Function --- #
151
  def analyze_resume(resume_file, job_description_file):
152
- """Analyzes the resume and job description, returning similarity score, skills, qualifications, and experience matching."""
153
- # Extract resume and job description text
154
  try:
155
  resume_text = extract_text_from_file(resume_file.name)
156
  job_description_text = extract_text_from_file(job_description_file.name)
157
  except ValueError as ve:
158
  return str(ve)
159
 
160
- # Extract skills, qualifications, and experience
161
- resume_skills = extract_skills_huggingface(resume_text)
162
  job_description_skills = process_job_description(job_description_text)
163
- resume_qualifications = extract_qualifications(resume_text)
164
- job_description_qualifications = extract_qualifications(job_description_text)
165
- resume_experience, resume_job_titles = extract_experience(resume_text)
166
- job_description_experience, job_description_titles = extract_experience(job_description_text)
167
-
168
- # Calculate semantic similarity for different sections in percentages
169
- skills_similarity = calculate_semantic_similarity(' '.join(resume_skills), ' '.join(job_description_skills))
170
- qualifications_similarity = calculate_semantic_similarity(' '.join(resume_qualifications), ' '.join(job_description_qualifications))
171
- experience_similarity = calculate_semantic_similarity(' '.join([str(e) for e in resume_experience]), ' '.join([str(e) for e in job_description_experience]))
172
-
173
- # Generate a communication response based on the similarity percentages
174
- communication_response = communication_generator(
175
- resume_skills, job_description_skills,
176
- skills_similarity, qualifications_similarity, experience_similarity
177
- )
178
-
179
- # Perform Sentiment Analysis
180
  sentiment = sentiment_analysis(resume_text)
181
 
182
- # Return the results including thresholds and percentage scores
183
  return (
184
- f"Skills Similarity: {skills_similarity:.2f}% ({categorize_similarity(skills_similarity)})",
185
- f"Qualifications Similarity: {qualifications_similarity:.2f}% ({categorize_similarity(qualifications_similarity)})",
186
- f"Experience Similarity: {experience_similarity:.2f}% ({categorize_similarity(experience_similarity)})",
187
  communication_response,
188
- f"Sentiment Analysis: {sentiment}",
189
- f"Resume Skills: {', '.join(resume_skills)}",
190
- f"Job Description Skills: {', '.join(job_description_skills)}",
191
- f"Resume Qualifications: {', '.join(resume_qualifications)}",
192
- f"Job Description Qualifications: {', '.join(job_description_qualifications)}",
193
- f"Resume Experience: {', '.join(map(str, resume_experience))} years, Titles: {', '.join(resume_job_titles)}",
194
- f"Job Description Experience: {', '.join(map(str, job_description_experience))} years, Titles: {', '.join(job_description_titles)}"
195
  )
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  # --- Gradio Interface --- #
198
  iface = gr.Interface(
199
  fn=analyze_resume,
200
- inputs=["file", "file"],
 
 
 
201
  outputs=[
202
- "text", "text", "text", "text", "text", "text", "text", "text", "text", "text", "text"
 
 
 
 
203
  ],
204
- title="Resume Analysis Tool",
205
- description="Analyze a resume against a job description to evaluate skills, qualifications, experience, and sentiment."
206
  )
207
 
208
- if __name__ == "__main__":
209
- iface.launch()
 
1
  import os
2
  import re
3
+ import io
4
  from datetime import datetime
5
  import PyPDF2
6
  import torch
7
  from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModelForSeq2SeqLM
8
+ from groq import Groq
9
  import gradio as gr
10
+ from docxtpl import DocxTemplate
11
 
12
+ # Set your API key for Groq
13
+ os.environ["GROQ_API_KEY"] = "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"
14
+ client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  # --- PDF/Text Extraction Functions --- #
17
  def extract_text_from_file(file_path):
 
24
  raise ValueError("Unsupported file type. Only PDF and TXT files are accepted.")
25
 
26
  def extract_text_from_pdf(pdf_file_path):
27
+ """Extracts text from a PDF file."""
 
28
  with open(pdf_file_path, 'rb') as pdf_file:
29
  pdf_reader = PyPDF2.PdfReader(pdf_file)
30
+ text = ''.join(page.extract_text() for page in pdf_reader.pages if page.extract_text())
31
+ return text
 
 
 
 
 
32
 
33
  def extract_text_from_txt(txt_file_path):
34
  """Extracts text from a .txt file."""
35
  with open(txt_file_path, 'r', encoding='utf-8') as txt_file:
36
  return txt_file.read()
37
 
38
+ # --- Skill Extraction with Llama Model --- #
39
+ def extract_skills_llama(text):
40
+ """Extracts skills from the text using the Llama model via Groq API."""
41
+ try:
42
+ response = client.chat.completions.create(
43
+ messages=[{"role": "user", "content": f"Extract skills from the following text: {text}"}],
44
+ model="llama3-70b-8192",
45
+ )
46
+ skills = response.choices[0].message.content.split(', ') # Expecting a comma-separated list
47
+ return skills
48
+ except Exception as e:
49
+ raise RuntimeError(f"Error during skill extraction: {e}")
50
+
51
+ # --- Job Description Processing --- #
52
+ def process_job_description(job_description_text):
53
+ """Processes the job description text and extracts relevant skills."""
54
+ job_description_text = preprocess_text(job_description_text)
55
+ return extract_skills_llama(job_description_text)
56
+
57
+ # --- Text Preprocessing --- #
58
+ def preprocess_text(text):
59
+ """Preprocesses text for analysis (lowercase, punctuation removal)."""
60
+ text = text.lower()
61
+ text = re.sub(r'[^\w\s]', '', text) # Remove punctuation
62
+ return re.sub(r'\s+', ' ', text).strip() # Remove extra whitespace
63
+
64
+ # --- Resume Similarity Calculation --- #
65
+ def calculate_resume_similarity(resume_text, job_description_text):
66
+ """Calculates similarity score between resume and job description using a sentence transformer model."""
67
+ model_name = "cross-encoder/stsb-roberta-base"
68
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
69
+ model = AutoModelForSequenceClassification.from_pretrained(model_name)
70
+
71
+ inputs = tokenizer(resume_text, job_description_text, return_tensors="pt", padding=True, truncation=True)
72
+ with torch.no_grad():
73
+ outputs = model(**inputs)
74
+ similarity_score = torch.sigmoid(outputs.logits).item() # Get the raw score
75
+ return similarity_score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # --- Communication Generation --- #
78
+ def communication_generator(resume_skills, job_description_skills, similarity_score, max_length=150):
79
+ """Generates a communication response based on the extracted skills from the resume and job description."""
80
  model_name = "google/flan-t5-base"
81
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
82
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
83
 
84
+ # Assess candidate fit based on similarity score
85
+ fit_status = "fit for the job" if similarity_score >= 0.7 else "not a fit for the job"
 
86
 
87
+ # Create a more detailed communication message
88
  message = (
89
+ f"After a thorough review of the candidate's resume, we found a significant alignment "
90
+ f"between their skills and the job description requirements. The candidate possesses the following "
91
+ f"key skills: {', '.join(resume_skills)}. These align well with the job requirements, particularly in areas such as "
92
+ f"{', '.join(job_description_skills)}. The candidate’s diverse expertise suggests they would make a valuable addition to our team. "
93
+ f"We believe the candidate is {fit_status}. If further evaluation is needed, please let us know how we can assist."
 
 
 
 
94
  )
95
 
96
  inputs = tokenizer(message, return_tensors="pt", padding=True, truncation=True)
 
100
 
101
  # --- Sentiment Analysis --- #
102
  def sentiment_analysis(text):
103
+ """Analyzes the sentiment of the text."""
104
+ model_name = "mrm8488/distiluse-base-multilingual-cased-v2-finetuned-stsb_multi_mt-es"
105
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
106
+ model = AutoModelForSequenceClassification.from_pretrained(model_name)
107
 
108
  inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
109
  with torch.no_grad():
 
111
  predicted_sentiment = torch.argmax(outputs.logits).item()
112
  return ["Negative", "Neutral", "Positive"][predicted_sentiment]
113
 
114
+ # --- Resume Analysis Function --- #
115
  def analyze_resume(resume_file, job_description_file):
116
+ """Analyzes the resume and job description, returning similarity score, skills, and communication response."""
117
+ # Extract resume text based on file type
118
  try:
119
  resume_text = extract_text_from_file(resume_file.name)
120
  job_description_text = extract_text_from_file(job_description_file.name)
121
  except ValueError as ve:
122
  return str(ve)
123
 
124
+ # Analyze texts
 
125
  job_description_skills = process_job_description(job_description_text)
126
+ resume_skills = extract_skills_llama(resume_text)
127
+ similarity_score = calculate_resume_similarity(resume_text, job_description_text)
128
+ communication_response = communication_generator(resume_skills, job_description_skills, similarity_score)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  sentiment = sentiment_analysis(resume_text)
130
 
 
131
  return (
132
+ f"Similarity Score: {similarity_score * 100:.2f}%", # Convert to percentage
 
 
133
  communication_response,
134
+ f"Sentiment: {sentiment}",
135
+ ", ".join(resume_skills),
136
+ ", ".join(job_description_skills),
 
 
 
 
137
  )
138
 
139
+ # --- Offer Letter Generation --- #
140
+ def generate_offer_letter(template_file, candidate_name, role, start_date, hours):
141
+ """Generates an offer letter from a template."""
142
+ try:
143
+ start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%B %d, %Y")
144
+ except ValueError:
145
+ return "Invalid date format. Please use YYYY-MM-DD."
146
+
147
+ context = {
148
+ 'candidate_name': candidate_name,
149
+ 'role': role,
150
+ 'start_date': start_date,
151
+ 'hours': hours
152
+ }
153
+
154
+ doc = DocxTemplate(template_file)
155
+ doc.render(context)
156
+
157
+ offer_letter_path = f"{candidate_name.replace(' ', '_')}_offer_letter.docx"
158
+ doc.save(offer_letter_path)
159
+
160
+ return offer_letter_path
161
+
162
  # --- Gradio Interface --- #
163
  iface = gr.Interface(
164
  fn=analyze_resume,
165
+ inputs=[
166
+ gr.File(label="Upload Resume (PDF/TXT)"),
167
+ gr.File(label="Upload Job Description (PDF/TXT)")
168
+ ],
169
  outputs=[
170
+ gr.Textbox(label="Similarity Score"),
171
+ gr.Textbox(label="Communication Response"),
172
+ gr.Textbox(label="Sentiment Analysis"),
173
+ gr.Textbox(label="Extracted Resume Skills"),
174
+ gr.Textbox(label="Extracted Job Description Skills"),
175
  ],
176
+ title="Resume and Job Description Analyzer",
177
+ description="This tool analyzes a resume against a job description to extract skills, calculate similarity, and generate communication responses."
178
  )
179
 
180
+ iface.launch()