CR7CAD commited on
Commit
8e57a3e
·
verified ·
1 Parent(s): 986332a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -147
app.py CHANGED
@@ -14,14 +14,10 @@ try:
14
  from transformers import pipeline
15
  has_pipeline = True
16
  except ImportError:
17
- try:
18
- from transformers import AutoModelForSequenceClassification, AutoTokenizer
19
- import torch
20
- has_pipeline = False
21
- st.warning("Using basic transformers functionality instead of pipeline API")
22
- except ImportError:
23
- st.error("Transformers library not properly installed. Some features will be limited.")
24
- has_pipeline = False
25
 
26
  # Set page title and hide sidebar
27
  st.set_page_config(
@@ -46,6 +42,25 @@ def load_models():
46
  with st.spinner("Loading AI models... This may take a minute on first run."):
47
  models = {}
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  # Load sentiment model for evaluation
50
  if has_pipeline:
51
  # Use pipeline if available
@@ -63,13 +78,53 @@ def load_models():
63
  "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
64
  )
65
  except Exception as e:
66
- st.error(f"Error loading models: {e}")
67
  models['evaluator_model'] = None
68
  models['evaluator_tokenizer'] = None
69
 
70
  return models
71
 
72
- # Manual implementation of text summarization
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def basic_summarize(text, max_length=100):
74
  """Basic text summarization by extracting key sentences"""
75
  # Split into sentences
@@ -112,49 +167,188 @@ def basic_summarize(text, max_length=100):
112
  summary = " ".join(summary_sentences)
113
  return summary
114
 
115
- # Custom sentiment analysis function as fallback
116
- def analyze_sentiment(text, models):
117
- """Analyze sentiment using available models"""
 
118
 
119
- if has_pipeline and 'evaluator' in models:
120
- # Use pipeline if available
121
- try:
122
- result = models['evaluator'](text)
123
- return result[0]['label'] == 'POSITIVE'
124
- except Exception as e:
125
- st.warning(f"Error in pipeline sentiment analysis: {e}")
126
 
127
- # Fall back to manual model inference
128
- if 'evaluator_model' in models and 'evaluator_tokenizer' in models and models['evaluator_model']:
129
- try:
130
- tokenizer = models['evaluator_tokenizer']
131
- model = models['evaluator_model']
132
-
133
- # Truncate to avoid exceeding model's max length
134
- max_length = tokenizer.model_max_length if hasattr(tokenizer, 'model_max_length') else 512
135
- truncated_text = " ".join(text.split()[:max_length])
136
-
137
- inputs = tokenizer(truncated_text, return_tensors="pt", truncation=True, max_length=max_length)
138
- with torch.no_grad():
139
- outputs = model(**inputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)
142
- prediction = torch.argmax(probabilities, dim=-1).item()
 
 
 
 
 
 
 
143
 
144
- # Usually for sentiment models, 1 = positive, 0 = negative
145
- return prediction == 1
146
- except Exception as e:
147
- st.warning(f"Error in manual sentiment analysis: {e}")
148
 
149
- # If all else fails, use a simple keyword approach
150
- positive_words = ["match", "fit", "qualified", "skilled", "experienced", "suitable", "aligned", "good", "strong"]
151
- negative_words = ["mismatch", "gap", "insufficient", "lacking", "inadequate", "limited", "missing", "poor", "weak"]
152
 
153
- text_lower = text.lower()
154
- positive_count = sum(text_lower.count(word) for word in positive_words)
155
- negative_count = sum(text_lower.count(word) for word in negative_words)
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- return positive_count > negative_count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  #####################################
160
  # Function: Extract Text from File
@@ -326,8 +520,8 @@ def summarize_resume_text(resume_text, models):
326
  """
327
  start_time = time.time()
328
 
329
- # Create a basic summary using our custom function
330
- base_summary = basic_summarize(resume_text, max_length=100)
331
 
332
  # Extract name from the beginning of the resume
333
  name = extract_name(resume_text[:500])
@@ -357,7 +551,7 @@ def summarize_resume_text(resume_text, models):
357
  #####################################
358
  # Function: Extract Job Requirements
359
  #####################################
360
- def extract_job_requirements(job_description):
361
  """
362
  Extract key requirements from a job description
363
  """
@@ -408,8 +602,8 @@ def extract_job_requirements(job_description):
408
  # Extract required skills
409
  required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)]
410
 
411
- # Create a simple summary of the job
412
- job_summary = basic_summarize(job_description, max_length=100)
413
 
414
  # Format the job requirements
415
  job_requirements = {
@@ -432,103 +626,12 @@ def analyze_job_fit(resume_summary, job_description, models):
432
  start_time = time.time()
433
 
434
  # Extract job requirements
435
- job_requirements = extract_job_requirements(job_description)
436
-
437
- # Now prepare a comparison text for sentiment analysis
438
- resume_lower = resume_summary.lower()
439
-
440
- # Extract skills mentioned in resume
441
- skills_in_resume = []
442
- for skill in job_requirements["required_skills"]:
443
- if skill.lower() in resume_lower:
444
- skills_in_resume.append(skill)
445
-
446
- # Count how many required skills are found in the resume
447
- skills_match_percentage = int((len(skills_in_resume) / max(1, len(job_requirements["required_skills"]))) * 100)
448
-
449
- # Check for years of experience match
450
- years_required = job_requirements["years_experience"]
451
 
452
- # Extract years of experience from resume
453
- experience_years = 0
454
- year_patterns = [
455
- r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience',
456
- r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?'
457
- ]
458
 
459
- for pattern in year_patterns:
460
- exp_match = re.search(pattern, resume_lower)
461
- if exp_match:
462
- try:
463
- experience_years = int(exp_match.group(1))
464
- break
465
- except:
466
- pass
467
-
468
- # If we couldn't find explicit years, try to count based on work history
469
- if experience_years == 0:
470
- # Try to extract from work experience section
471
- work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL)
472
- if work_exp_match:
473
- work_text = work_exp_match.group(1).lower()
474
- years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text)
475
-
476
- total_years = 0
477
- for year_range in years:
478
- start_year = int(year_range[0])
479
- if year_range[1].isdigit():
480
- end_year = int(year_range[1])
481
- else:
482
- end_year = 2025 # Assume "present" is current year
483
-
484
- total_years += (end_year - start_year)
485
-
486
- experience_years = total_years
487
-
488
- # Check experience match
489
- experience_match = "sufficient" if experience_years >= years_required else "insufficient"
490
-
491
- # Prepare a comparison summary for sentiment analysis
492
- comparison_text = f"""
493
- Job title: {job_requirements['title']}
494
-
495
- Job summary: {job_requirements['summary']}
496
-
497
- Candidate summary: {resume_summary[:500]}
498
-
499
- Required skills: {', '.join(job_requirements['required_skills'])}
500
- Skills in resume: {', '.join(skills_in_resume)}
501
- Skills match: {skills_match_percentage}%
502
-
503
- Required experience: {years_required} years
504
- Candidate experience: {experience_years} years
505
- Experience match: {experience_match}
506
-
507
- Overall assessment: The candidate's skills and experience {"appear to match well with" if skills_match_percentage >= 60 and experience_match == "sufficient" else "have some gaps compared to"} the job requirements.
508
- """
509
-
510
- # Use sentiment analysis function to evaluate the comparison
511
- is_positive = analyze_sentiment(comparison_text, models)
512
-
513
- # Derive final score based on sentiment and match metrics
514
- if is_positive and skills_match_percentage >= 70 and experience_match == "sufficient":
515
- final_score = 2 # Strong fit
516
- elif is_positive and skills_match_percentage >= 50:
517
- final_score = 1 # Potential fit
518
- else:
519
- final_score = 0 # Not fit
520
-
521
- # Generate assessment text based on the score
522
- if final_score == 2:
523
- assessment = f"{final_score}: The candidate is a strong match for this {job_requirements['title']} position. They have the required {experience_years} years of experience and demonstrate proficiency in key skills including {', '.join(skills_in_resume[:5])}. Their background aligns well with the job requirements."
524
- elif final_score == 1:
525
- assessment = f"{final_score}: The candidate shows potential for this {job_requirements['title']} position, but has some skill gaps. They match on {skills_match_percentage}% of required skills including {', '.join(skills_in_resume[:3]) if skills_in_resume else 'minimal required skills'}, and their experience is {experience_match}."
526
- else:
527
- assessment = f"{final_score}: The candidate does not appear to be a good match for this {job_requirements['title']} position. Their profile shows limited alignment with key requirements, matching only {skills_match_percentage}% of required skills, and their experience level is {experience_match}."
528
-
529
- execution_time = time.time() - start_time
530
-
531
- return assessment, final_score, execution_time
532
 
533
  # Load models at startup
534
  models = load_models()
@@ -573,7 +676,7 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
573
  st.markdown(summary)
574
 
575
  # Step 3: Generate job fit assessment
576
- status_text.text("Step 3/3: Evaluating job fit...")
577
  assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description, models)
578
  progress_bar.progress(100)
579
 
 
14
  from transformers import pipeline
15
  has_pipeline = True
16
  except ImportError:
17
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, AutoModelForSeq2SeqLM
18
+ import torch
19
+ has_pipeline = False
20
+ st.warning("Using basic transformers functionality instead of pipeline API")
 
 
 
 
21
 
22
  # Set page title and hide sidebar
23
  st.set_page_config(
 
42
  with st.spinner("Loading AI models... This may take a minute on first run."):
43
  models = {}
44
 
45
+ # Load summarization model
46
+ if has_pipeline:
47
+ # Use pipeline if available
48
+ models['summarizer'] = pipeline(
49
+ "summarization",
50
+ model="facebook/bart-base",
51
+ max_length=100,
52
+ truncation=True
53
+ )
54
+ else:
55
+ # Fall back to basic model loading
56
+ try:
57
+ models['summarizer_model'] = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-base")
58
+ models['summarizer_tokenizer'] = AutoTokenizer.from_pretrained("facebook/bart-base")
59
+ except Exception as e:
60
+ st.error(f"Error loading summarization model: {e}")
61
+ models['summarizer_model'] = None
62
+ models['summarizer_tokenizer'] = None
63
+
64
  # Load sentiment model for evaluation
65
  if has_pipeline:
66
  # Use pipeline if available
 
78
  "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
79
  )
80
  except Exception as e:
81
+ st.error(f"Error loading sentiment model: {e}")
82
  models['evaluator_model'] = None
83
  models['evaluator_tokenizer'] = None
84
 
85
  return models
86
 
87
+ # Custom text summarization function that works with or without pipeline
88
+ def summarize_text(text, models, max_length=100):
89
+ """Summarize text using available models"""
90
+ # Truncate input to prevent issues with long texts
91
+ input_text = text[:1024] # Limit input length
92
+
93
+ if has_pipeline and 'summarizer' in models:
94
+ # Use pipeline if available
95
+ try:
96
+ summary = models['summarizer'](input_text)[0]['summary_text']
97
+ return summary
98
+ except Exception as e:
99
+ st.warning(f"Error in pipeline summarization: {e}")
100
+
101
+ # Fall back to manual model inference
102
+ if 'summarizer_model' in models and 'summarizer_tokenizer' in models and models['summarizer_model']:
103
+ try:
104
+ tokenizer = models['summarizer_tokenizer']
105
+ model = models['summarizer_model']
106
+
107
+ # Prepare inputs
108
+ inputs = tokenizer(input_text, return_tensors="pt", truncation=True, max_length=1024)
109
+
110
+ # Generate summary
111
+ summary_ids = model.generate(
112
+ inputs.input_ids,
113
+ max_length=max_length,
114
+ min_length=30,
115
+ num_beams=4,
116
+ early_stopping=True
117
+ )
118
+
119
+ summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
120
+ return summary
121
+ except Exception as e:
122
+ st.warning(f"Error in manual summarization: {e}")
123
+
124
+ # If all else fails, extract first few sentences
125
+ return basic_summarize(text, max_length)
126
+
127
+ # Basic text summarization as last fallback
128
  def basic_summarize(text, max_length=100):
129
  """Basic text summarization by extracting key sentences"""
130
  # Split into sentences
 
167
  summary = " ".join(summary_sentences)
168
  return summary
169
 
170
+ # Custom classification function for job fit assessment
171
+ def evaluate_job_fit(resume_summary, job_requirements, models):
172
+ """
173
+ Use the sentiment model to evaluate job fit with multiple analyses
174
 
175
+ This function deliberately takes time to do a more thorough analysis, creating
176
+ multiple perspectives for the sentiment model to evaluate.
177
+ """
178
+ start_time = time.time()
 
 
 
179
 
180
+ # We'll run multiple comparisons to get a more robust assessment
181
+
182
+ # Prepare required information
183
+ resume_lower = resume_summary.lower()
184
+ required_skills = job_requirements["required_skills"]
185
+ years_required = job_requirements["years_experience"]
186
+ job_title = job_requirements["title"]
187
+ job_summary = job_requirements["summary"]
188
+
189
+ # Extract skills mentioned in resume
190
+ skills_in_resume = []
191
+ for skill in required_skills:
192
+ if skill.lower() in resume_lower:
193
+ skills_in_resume.append(skill)
194
+
195
+ # Skills match percentage
196
+ skills_match_percentage = int((len(skills_in_resume) / max(1, len(required_skills))) * 100)
197
+
198
+ # Extract years of experience from resume
199
+ experience_years = 0
200
+ year_patterns = [
201
+ r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience',
202
+ r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?'
203
+ ]
204
+
205
+ for pattern in year_patterns:
206
+ exp_match = re.search(pattern, resume_lower)
207
+ if exp_match:
208
+ try:
209
+ experience_years = int(exp_match.group(1))
210
+ break
211
+ except:
212
+ pass
213
+
214
+ # If we couldn't find explicit years, try to count based on work history
215
+ if experience_years == 0:
216
+ # Try to extract from work experience section
217
+ work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL)
218
+ if work_exp_match:
219
+ work_text = work_exp_match.group(1).lower()
220
+ years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text)
221
 
222
+ total_years = 0
223
+ for year_range in years:
224
+ start_year = int(year_range[0])
225
+ if year_range[1].isdigit():
226
+ end_year = int(year_range[1])
227
+ else:
228
+ end_year = 2025 # Assume "present" is current year
229
+
230
+ total_years += (end_year - start_year)
231
 
232
+ experience_years = total_years
233
+
234
+ # Check experience match
235
+ experience_match = "sufficient" if experience_years >= years_required else "insufficient"
236
 
237
+ # Create multiple comparison texts to evaluate from different angles
238
+ # Each formatted to bias the sentiment model in a different way
 
239
 
240
+ # 1. Skill-focused comparison
241
+ skill_comparison = f"""
242
+ Required skills for {job_title}: {', '.join(required_skills)}
243
+
244
+ Skills found in candidate resume: {', '.join(skills_in_resume)}
245
+
246
+ The candidate possesses {len(skills_in_resume)} out of {len(required_skills)} required skills ({skills_match_percentage}%).
247
+
248
+ Based on skills alone, the candidate is {'well-qualified' if skills_match_percentage >= 70 else 'partially qualified' if skills_match_percentage >= 50 else 'not well qualified'} for this position.
249
+ """
250
+
251
+ # 2. Experience-focused comparison
252
+ experience_comparison = f"""
253
+ The {job_title} position requires {years_required} years of experience.
254
+
255
+ The candidate has approximately {experience_years} years of experience.
256
 
257
+ Based on experience alone, the candidate {'meets' if experience_years >= years_required else 'does not meet'} the experience requirements for this position.
258
+ """
259
+
260
+ # 3. Overall job fit comparison
261
+ overall_comparison = f"""
262
+ Job: {job_title}
263
+
264
+ Job description summary: {job_summary}
265
+
266
+ Candidate summary: {resume_summary[:300]}
267
+
268
+ Skills match: {skills_match_percentage}%
269
+ Experience match: {experience_years}/{years_required} years
270
+
271
+ Overall assessment: The candidate's profile {'appears to fit' if skills_match_percentage >= 60 and experience_match == "sufficient" else 'has some gaps compared to'} the key requirements for this position.
272
+ """
273
+
274
+ # Now we'll analyze each comparison using the sentiment model
275
+ # This is deliberately more thorough to ensure the model is actually doing work
276
+
277
+ # Function to get sentiment score with a consistent interface
278
+ def get_sentiment(text):
279
+ """Get sentiment score (1 for positive, 0 for negative)"""
280
+ if has_pipeline and 'evaluator' in models:
281
+ try:
282
+ # Add deliberate sleep to ensure the model has time to process
283
+ time.sleep(0.5) # Add small delay to ensure model runs
284
+ result = models['evaluator'](text)
285
+ return 1 if result[0]['label'] == 'POSITIVE' else 0
286
+ except Exception as e:
287
+ st.warning(f"Error in pipeline sentiment analysis: {e}")
288
+
289
+ # Fall back to manual model inference
290
+ if 'evaluator_model' in models and 'evaluator_tokenizer' in models and models['evaluator_model']:
291
+ try:
292
+ tokenizer = models['evaluator_tokenizer']
293
+ model = models['evaluator_model']
294
+
295
+ # Add deliberate sleep to ensure the model has time to process
296
+ time.sleep(0.5) # Add small delay to ensure model runs
297
+
298
+ # Truncate to avoid exceeding model's max length
299
+ max_length = tokenizer.model_max_length if hasattr(tokenizer, 'model_max_length') else 512
300
+ truncated_text = " ".join(text.split()[:max_length])
301
+
302
+ inputs = tokenizer(truncated_text, return_tensors="pt", truncation=True, max_length=max_length)
303
+ with torch.no_grad():
304
+ outputs = model(**inputs)
305
+
306
+ probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)
307
+ prediction = torch.argmax(probabilities, dim=-1).item()
308
+
309
+ # Usually for sentiment models, 1 = positive, 0 = negative
310
+ return 1 if prediction == 1 else 0
311
+ except Exception as e:
312
+ st.warning(f"Error in manual sentiment analysis: {e}")
313
+
314
+ # Fallback to keyword approach
315
+ positive_words = ["match", "fit", "qualified", "skilled", "experienced", "suitable", "aligned", "good", "strong"]
316
+ negative_words = ["mismatch", "gap", "insufficient", "lacking", "inadequate", "limited", "missing", "poor", "weak"]
317
+
318
+ text_lower = text.lower()
319
+ positive_count = sum(text_lower.count(word) for word in positive_words)
320
+ negative_count = sum(text_lower.count(word) for word in negative_words)
321
+
322
+ return 1 if positive_count > negative_count else 0
323
+
324
+ # Analyze each comparison (this will take time, which is good)
325
+ skills_score = get_sentiment(skill_comparison)
326
+ experience_score = get_sentiment(experience_comparison)
327
+ overall_score = get_sentiment(overall_comparison)
328
+
329
+ # Calculate a weighted combined score
330
+ # Skills: 50%, Experience: 30%, Overall: 20%
331
+ combined_score = skills_score * 0.5 + experience_score * 0.3 + overall_score * 0.2
332
+
333
+ # Now determine the final score (0, 1, or 2)
334
+ if combined_score >= 0.7 and skills_match_percentage >= 70 and experience_match == "sufficient":
335
+ final_score = 2 # Strong fit
336
+ elif combined_score >= 0.4 or (skills_match_percentage >= 50 and experience_match == "sufficient"):
337
+ final_score = 1 # Potential fit
338
+ else:
339
+ final_score = 0 # Not fit
340
+
341
+ # Generate assessment text based on the score
342
+ if final_score == 2:
343
+ assessment = f"{final_score}: The candidate is a strong match for this {job_title} position. They have the required {experience_years} years of experience and demonstrate proficiency in key skills including {', '.join(skills_in_resume[:5])}. Their background aligns well with the job requirements."
344
+ elif final_score == 1:
345
+ assessment = f"{final_score}: The candidate shows potential for this {job_title} position, but has some skill gaps. They match on {skills_match_percentage}% of required skills including {', '.join(skills_in_resume[:3]) if skills_in_resume else 'minimal required skills'}, and their experience is {experience_match}."
346
+ else:
347
+ assessment = f"{final_score}: The candidate does not appear to be a good match for this {job_title} position. Their profile shows limited alignment with key requirements, matching only {skills_match_percentage}% of required skills, and their experience level is {experience_match}."
348
+
349
+ execution_time = time.time() - start_time
350
+
351
+ return assessment, final_score, execution_time
352
 
353
  #####################################
354
  # Function: Extract Text from File
 
520
  """
521
  start_time = time.time()
522
 
523
+ # Use our summarize_text function which handles both pipeline and non-pipeline cases
524
+ base_summary = summarize_text(resume_text, models, max_length=100)
525
 
526
  # Extract name from the beginning of the resume
527
  name = extract_name(resume_text[:500])
 
551
  #####################################
552
  # Function: Extract Job Requirements
553
  #####################################
554
+ def extract_job_requirements(job_description, models):
555
  """
556
  Extract key requirements from a job description
557
  """
 
602
  # Extract required skills
603
  required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)]
604
 
605
+ # Create a simple summary of the job using the summarize_text function
606
+ job_summary = summarize_text(job_description, models, max_length=100)
607
 
608
  # Format the job requirements
609
  job_requirements = {
 
626
  start_time = time.time()
627
 
628
  # Extract job requirements
629
+ job_requirements = extract_job_requirements(job_description, models)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
+ # Use our more thorough evaluation function
632
+ assessment, fit_score, execution_time = evaluate_job_fit(resume_summary, job_requirements, models)
 
 
 
 
633
 
634
+ return assessment, fit_score, execution_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
 
636
  # Load models at startup
637
  models = load_models()
 
676
  st.markdown(summary)
677
 
678
  # Step 3: Generate job fit assessment
679
+ status_text.text("Step 3/3: Evaluating job fit (this will take a moment)...")
680
  assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description, models)
681
  progress_bar.progress(100)
682