norhan12 commited on
Commit
293b493
·
verified ·
1 Parent(s): 97f53a0

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +193 -185
process_interview.py CHANGED
@@ -26,7 +26,6 @@ from reportlab.lib import colors
26
  import matplotlib.pyplot as plt
27
  import matplotlib
28
  matplotlib.use('Agg')
29
- from reportlab.platypus import Image
30
  import io
31
  from transformers import AutoTokenizer, AutoModel
32
  import spacy
@@ -37,18 +36,17 @@ from concurrent.futures import ThreadPoolExecutor
37
  # Setup logging
38
  logging.basicConfig(level=logging.INFO)
39
  logger = logging.getLogger(__name__)
40
- logging.getLogger("nemo_logging").setLevel(logging.INFO)
41
- logging.getLogger("nemo").setLevel(logging.INFO)
42
 
43
  # Configuration
44
- AUDIO_DIR = "./uploads"
45
  OUTPUT_DIR = "./processed_audio"
46
  os.makedirs(OUTPUT_DIR, exist_ok=True)
47
 
48
- # API Keys
49
- PINECONE_KEY = os.getenv("PINECONE_KEY")
50
- ASSEMBLYAI_KEY = os.getenv("ASSEMBLYAI_KEY")
51
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
52
 
53
  def download_audio_from_url(url: str) -> str:
54
  """Downloads an audio file from a URL to a temporary local path."""
@@ -92,11 +90,10 @@ logger.info(f"Using device: {device}")
92
 
93
  def load_speaker_model():
94
  try:
95
- import torch
96
  torch.set_num_threads(5)
97
  model = EncDecSpeakerLabelModel.from_pretrained(
98
  "nvidia/speakerverification_en_titanet_large",
99
- map_location=torch.device('cpu')
100
  )
101
  model.eval()
102
  return model
@@ -190,7 +187,7 @@ def transcribe(audio_path: str) -> Dict:
190
  logger.error(f"Transcription failed: {str(e)}")
191
  raise
192
 
193
- def process_utterance(utterance, full_audio, wav_file):
194
  try:
195
  start = utterance['start']
196
  end = utterance['end']
@@ -220,7 +217,7 @@ def process_utterance(utterance, full_audio, wav_file):
220
  'embedding': embedding_list
221
  }
222
  except Exception as e:
223
- logger.error(f"Utterance processing failed: {str(e)}", exc_info=True)
224
  return {
225
  **utterance,
226
  'speaker': 'Unknown',
@@ -267,7 +264,7 @@ def train_role_classifier(utterances: List[Dict]):
267
  sum(1 for token in doc if token.pos_ == 'NOUN')
268
  ])
269
  features.append(feat)
270
- labels.append(0 if i % 2 == 0 else 1)
271
  scaler = StandardScaler()
272
  X = scaler.fit_transform(features)
273
  clf = RandomForestClassifier(
@@ -371,46 +368,44 @@ def generate_voice_interpretation(analysis: Dict) -> str:
371
  if 'error' in analysis:
372
  return "Voice analysis unavailable due to processing limitations."
373
  interpretation_lines = [
374
- "Vocal Performance Profile:",
375
- f"- Speaking Rate: {analysis['speaking_rate']} words/sec - Benchmark: 2.0-3.0 wps for clear delivery",
376
- f"- Filler Word Frequency: {analysis['filler_ratio'] * 100:.1f}% - Measures non-content words",
377
- f"- Repetition Index: {analysis['repetition_score']:.3f} - Frequency of repeated phrases",
378
- f"- Anxiety Indicator: {analysis['interpretation']['anxiety_level']} (Score: {analysis['composite_scores']['anxiety']:.3f}) - Pitch and vocal stability",
379
- f"- Confidence Indicator: {analysis['interpretation']['confidence_level']} (Score: {analysis['composite_scores']['confidence']:.3f}) - Vocal strength",
380
- f"- Fluency Rating: {analysis['interpretation']['fluency_level']} - Speech flow and coherence",
381
  "",
382
  "HR Insights:",
383
- "- Rapid speech (>3.0 wps) may signal enthusiasm but risks clarity.",
384
- "- High filler word use reduces perceived professionalism.",
385
- "- Elevated anxiety suggests pressure; training can build resilience.",
386
- "- Strong confidence aligns with leadership presence.",
387
- "- Fluent speech enhances engagement, critical for team roles."
388
  ]
389
  return "\n".join(interpretation_lines)
390
 
391
- def generate_anxiety_confidence_chart(composite_scores: Dict, chart_path_or_buffer):
392
  try:
393
  labels = ['Anxiety', 'Confidence']
394
  scores = [composite_scores.get('anxiety', 0), composite_scores.get('confidence', 0)]
395
  fig, ax = plt.subplots(figsize=(5, 3.5))
396
  bars = ax.bar(labels, scores, color=['#FF5252', '#26A69A'], edgecolor='black', width=0.45)
397
- ax.set_ylabel('Score (Normalized)', fontsize=12)
398
  ax.set_title('Vocal Dynamics: Anxiety vs. Confidence', fontsize=14, pad=15)
399
  ax.set_ylim(0, 1.3)
400
  for bar in bars:
401
  height = bar.get_height()
402
  ax.text(bar.get_x() + bar.get_width()/2, height + 0.05, f"{height:.2f}",
403
- ha='center', color='black', fontweight='bold', fontsize=11)
404
  ax.grid(True, axis='y', linestyle='--', alpha=0.7)
405
  plt.tight_layout()
406
- plt.savefig(chart_path_or_buffer, format='png', bbox_inches='tight', dpi=300)
407
  plt.close(fig)
408
  except Exception as e:
409
  logger.error(f"Error generating chart: {str(e)}")
410
 
411
  def calculate_acceptance_probability(analysis_data: Dict) -> float:
412
  voice = analysis_data.get('voice_analysis', {})
413
- if 'error' in voice: return 0.0
414
  w_confidence, w_anxiety, w_fluency, w_speaking_rate, w_filler_repetition, w_content_strengths = 0.35, -0.25, 0.2, 0.15, -0.15, 0.25
415
  confidence_score = voice.get('composite_scores', {}).get('confidence', 0.0)
416
  anxiety_score = voice.get('composite_scores', {}).get('anxiety', 0.0)
@@ -428,8 +423,7 @@ def calculate_acceptance_probability(analysis_data: Dict) -> float:
428
  content_strength_val = 0.85 if analysis_data.get('text_analysis', {}).get('total_duration', 0) > 60 else 0.4
429
  raw_score = (confidence_score * w_confidence + (1 - anxiety_score) * abs(w_anxiety) + fluency_val * w_fluency + speaking_rate_score * w_speaking_rate + filler_repetition_score * abs(w_filler_repetition) + content_strength_val * w_content_strengths)
430
  max_possible_score = (w_confidence + abs(w_anxiety) + w_fluency + w_speaking_rate + abs(w_filler_repetition) + w_content_strengths)
431
- if max_possible_score == 0: return 50.0
432
- normalized_score = raw_score / max_possible_score
433
  acceptance_probability = max(0.0, min(1.0, normalized_score))
434
  return float(f"{acceptance_probability * 100:.2f}")
435
 
@@ -437,39 +431,39 @@ def generate_report(analysis_data: Dict) -> str:
437
  try:
438
  voice = analysis_data.get('voice_analysis', {})
439
  voice_interpretation = generate_voice_interpretation(voice)
440
- interviewee_responses = [f"Speaker {u['speaker']} ({u['role']}): {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:6]
441
- acceptance_prob = analysis_data.get('acceptance_probability', None)
442
- acceptance_line = ""
443
- if acceptance_prob is not None:
444
- acceptance_line = f"\n**Hiring Suitability Score: {acceptance_prob:.2f}%**\n"
445
- if acceptance_prob >= 80: acceptance_line += "HR Verdict: Outstanding candidate, highly recommended for immediate advancement."
446
- elif acceptance_prob >= 60: acceptance_line += "HR Verdict: Strong candidate, suitable for further evaluation with targeted development."
447
- elif acceptance_prob >= 40: acceptance_line += "HR Verdict: Moderate potential, requires additional assessment and skill-building."
448
- else: acceptance_line += "HR Verdict: Limited fit, significant improvement needed for role alignment."
 
 
449
  prompt = f"""
450
- You are EvalBot, a senior HR consultant with 20+ years of experience, delivering a polished, concise, and engaging interview analysis report. Use a professional tone, clear headings, and bullet points ('- ') for readability. Avoid redundancy and ensure distinct sections for strengths, growth areas, and recommendations.
451
  {acceptance_line}
452
  **1. Executive Summary**
453
- - Provide a concise overview of performance, key metrics, and hiring potential.
454
- - Interview length: {analysis_data['text_analysis']['total_duration']:.2f} seconds
455
- - Speaker turns: {analysis_data['text_analysis']['speaker_turns']}
456
- - Participants: {', '.join(analysis_data['speakers'])}
457
  **2. Communication and Vocal Dynamics**
458
- - Evaluate vocal delivery (rate, fluency, confidence) and professional impact.
459
- - Offer HR insights on workplace alignment.
460
  {voice_interpretation}
461
- **3. Competency and Content Evaluation**
462
- - Assess competencies: leadership, problem-solving, communication, adaptability.
463
- - List strengths and growth areas separately, with specific examples.
464
  - Sample responses:
465
  {chr(10).join(interviewee_responses)}
466
- **4. Role Fit and Growth Potential**
467
- - Analyze cultural fit, role readiness, and long-term potential.
468
- - Highlight enthusiasm and scalability.
469
- **5. Strategic HR Recommendations**
470
- - Provide distinct, prioritized strategies for candidate growth.
471
- - Target: Communication, Response Depth, Professional Presence.
472
- - List clear next steps for hiring managers (e.g., advance, train, assess).
473
  """
474
  response = gemini_model.generate_content(prompt)
475
  return response.text
@@ -480,14 +474,14 @@ def generate_report(analysis_data: Dict) -> str:
480
  def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
481
  try:
482
  doc = SimpleDocTemplate(output_path, pagesize=letter,
483
- rightMargin=0.7*inch, leftMargin=0.7*inch,
484
- topMargin=0.9*inch, bottomMargin=0.9*inch)
485
  styles = getSampleStyleSheet()
486
- h1 = ParagraphStyle(name='Heading1', fontSize=22, leading=26, spaceAfter=20, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
487
- h2 = ParagraphStyle(name='Heading2', fontSize=15, leading=18, spaceBefore=14, spaceAfter=8, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
488
- h3 = ParagraphStyle(name='Heading3', fontSize=11, leading=14, spaceBefore=10, spaceAfter=6, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
489
- body_text = ParagraphStyle(name='BodyText', fontSize=10, leading=13, spaceAfter=8, fontName='Helvetica', textColor=colors.HexColor('#333333'))
490
- bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=20, bulletIndent=10, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=10)
491
 
492
  story = []
493
 
@@ -495,55 +489,54 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
495
  canvas.saveState()
496
  canvas.setFont('Helvetica', 8)
497
  canvas.setFillColor(colors.HexColor('#666666'))
498
- canvas.drawString(doc.leftMargin, 0.4 * inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
499
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
500
- canvas.setLineWidth(1)
501
- canvas.line(doc.leftMargin, doc.height + 0.85*inch, doc.width + doc.leftMargin, doc.height + 0.85*inch)
502
- canvas.setFont('Helvetica-Bold', 10)
503
- canvas.drawString(doc.leftMargin, doc.height + 0.9*inch, "Candidate Interview Analysis")
504
- canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.9*inch, time.strftime('%B %d, %Y'))
505
  canvas.restoreState()
506
 
507
  # Title Page
508
  story.append(Paragraph("Candidate Interview Analysis", h1))
509
- story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=10, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
510
- story.append(Spacer(1, 0.5 * inch))
511
- acceptance_prob = analysis_data.get('acceptance_probability')
512
- if acceptance_prob is not None:
513
- story.append(Paragraph("Hiring Suitability Snapshot", h2))
514
- prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
515
- story.append(Paragraph(f"Suitability Score: <font size=16 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
516
- ParagraphStyle(name='Prob', fontSize=12, spaceAfter=12, alignment=1, fontName='Helvetica-Bold')))
517
- if acceptance_prob >= 80:
518
- story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, highly recommended for immediate advancement.", body_text))
519
- elif acceptance_prob >= 60:
520
- story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation with targeted development.", body_text))
521
- elif acceptance_prob >= 40:
522
- story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, requires additional assessment and skill-building.", body_text))
523
- else:
524
- story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement needed for role alignment.", body_text))
525
- story.append(Spacer(1, 0.3 * inch))
526
- table_data = [
527
- ['Metric', 'Value'],
528
- ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
529
- ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
530
- ['Participants', ', '.join(sorted(analysis_data['speakers']))]
531
- ]
532
- table = Table(table_data, colWidths=[2.2*inch, 3.8*inch])
533
- table.setStyle(TableStyle([
534
- ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
535
- ('TEXTCOLOR', (0,0), (-1,0), colors.white),
536
- ('ALIGN', (0,0), (-1,-1), 'LEFT'),
537
- ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
538
- ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
539
- ('FONTSIZE', (0,0), (-1,-1), 9),
540
- ('BOTTOMPADDING', (0,0), (-1,0), 10),
541
- ('TOPPADDING', (0,0), (-1,0), 10),
542
- ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
543
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB'))
544
- ]))
545
- story.append(table)
546
- story.append(Spacer(1, 0.4 * inch))
547
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
548
  story.append(PageBreak())
549
 
@@ -558,11 +551,11 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
558
  ['Metric', 'Value', 'HR Insight'],
559
  ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
560
  ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
561
- ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}; stress response"],
562
- ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}; vocal strength"],
563
- ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement']
564
  ]
565
- table = Table(table_data, colWidths=[1.7*inch, 1.2*inch, 3.1*inch])
566
  table.setStyle(TableStyle([
567
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
568
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
@@ -570,124 +563,138 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
570
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
571
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
572
  ('FONTSIZE', (0,0), (-1,-1), 9),
573
- ('BOTTOMPADDING', (0,0), (-1,0), 10),
574
- ('TOPPADDING', (0,0), (-1,0), 10),
575
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
576
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB'))
577
  ]))
578
  story.append(table)
579
- story.append(Spacer(1, 0.2 * inch))
580
  chart_buffer = io.BytesIO()
581
  generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
582
  chart_buffer.seek(0)
583
- img = Image(chart_buffer, width=4.8*inch, height=3.2*inch)
584
  img.hAlign = 'CENTER'
585
  story.append(img)
586
  else:
587
  story.append(Paragraph("Vocal analysis unavailable.", body_text))
588
- story.append(Spacer(1, 0.3 * inch))
589
 
590
  # Parse Gemini Report
591
  sections = {
592
  "Executive Summary": [],
593
- "Communication and Vocal Dynamics": [],
594
- "Competency and Content Evaluation": {"Strengths": [], "Growth Areas": []},
595
- "Role Fit and Growth Potential": [],
596
- "Strategic HR Recommendations": {"Development Priorities": [], "Next Steps": []}
597
  }
598
- report_parts = re.split(r'(\s*\*\*\s*\d\.\s*.*?\s*\*\*)', gemini_report_text)
599
  current_section = None
600
- for part in report_parts:
601
- if not part.strip(): continue
602
- is_heading = False
603
- for title in sections.keys():
604
- if title.lower() in part.lower():
605
- current_section = title
606
- is_heading = True
607
- break
608
- if not is_heading and current_section:
609
- if current_section == "Competency and Content Evaluation":
610
- if 'strength' in part.lower() or any(k in part.lower() for k in ['leadership', 'problem-solving', 'communication', 'adaptability']):
611
- sections[current_section]["Strengths"].append(part.strip())
612
- elif 'improve' in part.lower() or 'grow' in part.lower() or 'challenge' in part.lower():
613
- sections[current_section]["Growth Areas"].append(part.strip())
614
- elif current_section == "Strategic HR Recommendations":
615
- if any(k in part.lower() for k in ['communication', 'depth', 'presence', 'improve']):
616
- sections[current_section]["Development Priorities"].append(part.strip())
617
- elif any(k in part.lower() for k in ['advance', 'train', 'assess', 'next step']):
618
- sections[current_section]["Next Steps"].append(part.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  else:
620
- sections[current_section].append(part.strip())
621
 
622
  # Executive Summary
623
  story.append(Paragraph("2. Executive Summary", h2))
624
  if sections['Executive Summary']:
625
  for line in sections['Executive Summary']:
626
- if line.startswith(('-', '•', '*')):
627
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
628
- else:
629
- story.append(Paragraph(line, body_text))
630
  else:
631
- story.append(Paragraph("Summary unavailable.", body_text))
632
- story.append(Spacer(1, 0.3 * inch))
633
 
634
  # Competency and Content
635
- story.append(Paragraph("3. Competency & Content", h2))
636
  story.append(Paragraph("Strengths", h3))
637
- if sections['Competency and Content Evaluation']['Strengths']:
638
- for line in sections['Competency and Content Evaluation']['Strengths']:
639
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
640
  else:
641
  story.append(Paragraph("No strengths identified.", body_text))
642
- story.append(Spacer(1, 0.2 * inch))
643
  story.append(Paragraph("Growth Areas", h3))
644
- if sections['Competency and Content Evaluation']['Growth Areas']:
645
- for line in sections['Competency and Content Evaluation']['Growth Areas']:
646
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
647
  else:
648
- story.append(Paragraph("No growth areas identified.", body_text))
649
- story.append(Spacer(1, 0.3 * inch))
650
 
651
  # Role Fit
652
  story.append(Paragraph("4. Role Fit & Potential", h2))
653
- if sections['Role Fit and Growth Potential']:
654
- for line in sections['Role Fit and Growth Potential']:
655
- if line.startswith(('-', '•', '*')):
656
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
657
- else:
658
- story.append(Paragraph(line, body_text))
659
  else:
660
- story.append(Paragraph("Fit and potential analysis unavailable.", body_text))
661
- story.append(Spacer(1, 0.3 * inch))
662
 
663
- # Strategic Recommendations
664
- story.append(Paragraph("5. Strategic Recommendations", h2))
665
  story.append(Paragraph("Development Priorities", h3))
666
- if sections['Strategic HR Recommendations']['Development Priorities']:
667
- for line in sections['Strategic HR Recommendations']['Development Priorities']:
668
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
669
  else:
670
  story.append(Paragraph("No development priorities specified.", body_text))
671
- story.append(Spacer(1, 0.2 * inch))
672
- story.append(Paragraph("Next Steps for Managers", h3))
673
- if sections['Strategic HR Recommendations']['Next Steps']:
674
- for line in sections['Strategic HR Recommendations']['Next Steps']:
675
- story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
676
  else:
677
  story.append(Paragraph("No next steps provided.", body_text))
678
- story.append(Spacer(1, 0.3 * inch))
679
- story.append(Paragraph("This report provides a data-driven evaluation to guide hiring and development decisions.", body_text))
680
 
681
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
682
  return True
683
  except Exception as e:
684
- logger.error(f"PDF creation failed: {str(e)}", exc_info=True)
685
  return False
686
 
687
  def convert_to_serializable(obj):
688
  if isinstance(obj, np.generic): return obj.item()
689
  if isinstance(obj, dict): return {k: convert_to_serializable(v) for k, v in obj.items()}
690
- if isinstance(obj, list): return [convert_to_serializable(i) for i in obj]
691
  if isinstance(obj, np.ndarray): return obj.tolist()
692
  return obj
693
 
@@ -730,18 +737,19 @@ def process_interview(audio_path_or_url: str):
730
  base_name = str(uuid.uuid4())
731
  pdf_path = os.path.join(OUTPUT_DIR, f"{base_name}_report.pdf")
732
  json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
733
- create_pdf_report(analysis_data, pdf_path, gemini_report_text=gemini_report_text)
 
734
  with open(json_path, 'w') as f:
735
  serializable_data = convert_to_serializable(analysis_data)
736
  json.dump(serializable_data, f, indent=2)
737
  logger.info(f"Processing completed for {audio_path_or_url}")
738
  return {'pdf_path': pdf_path, 'json_path': json_path}
739
  except Exception as e:
740
- logger.error(f"Processing failed for {audio_path_or_url}: {str(e)}", exc_info=True)
741
  raise
742
  finally:
743
  if wav_file and os.path.exists(wav_file):
744
  os.remove(wav_file)
745
  if is_downloaded and local_audio_path and os.path.exists(local_audio_path):
746
  os.remove(local_audio_path)
747
- logger.info(f"Cleaned up temporary downloaded file: {local_audio_path}")
 
26
  import matplotlib.pyplot as plt
27
  import matplotlib
28
  matplotlib.use('Agg')
 
29
  import io
30
  from transformers import AutoTokenizer, AutoModel
31
  import spacy
 
36
  # Setup logging
37
  logging.basicConfig(level=logging.INFO)
38
  logger = logging.getLogger(__name__)
39
+ logging.getLogger("nemo_logger").setLevel(logging.WARNING)
 
40
 
41
  # Configuration
42
+ AUDIO_DIR = "./Uploads"
43
  OUTPUT_DIR = "./processed_audio"
44
  os.makedirs(OUTPUT_DIR, exist_ok=True)
45
 
46
+ # API Keys (replace with actual keys or environment variables)
47
+ PINECONE_KEY = os.getenv("PINECONE_KEY", "your-pinecone-key")
48
+ ASSEMBLYAI_KEY = os.getenv("ASSEMBLYAI_KEY", "your-assemblyai-key")
49
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "your-gemini-key")
50
 
51
  def download_audio_from_url(url: str) -> str:
52
  """Downloads an audio file from a URL to a temporary local path."""
 
90
 
91
  def load_speaker_model():
92
  try:
 
93
  torch.set_num_threads(5)
94
  model = EncDecSpeakerLabelModel.from_pretrained(
95
  "nvidia/speakerverification_en_titanet_large",
96
+ map_location=device
97
  )
98
  model.eval()
99
  return model
 
187
  logger.error(f"Transcription failed: {str(e)}")
188
  raise
189
 
190
+ def process_utterance(utterance: Dict, full_audio: AudioSegment, wav_file: str) -> Dict:
191
  try:
192
  start = utterance['start']
193
  end = utterance['end']
 
217
  'embedding': embedding_list
218
  }
219
  except Exception as e:
220
+ logger.error(f"Utterance processing failed: {str(e)}")
221
  return {
222
  **utterance,
223
  'speaker': 'Unknown',
 
264
  sum(1 for token in doc if token.pos_ == 'NOUN')
265
  ])
266
  features.append(feat)
267
+ labels.append(0 if i % 2 == 0 else 1) # Simplified for demo; replace with actual labels
268
  scaler = StandardScaler()
269
  X = scaler.fit_transform(features)
270
  clf = RandomForestClassifier(
 
368
  if 'error' in analysis:
369
  return "Voice analysis unavailable due to processing limitations."
370
  interpretation_lines = [
371
+ f"- Speaking Rate: {analysis['speaking_rate']} words/sec (Benchmark: 2.0-3.0 wps; affects clarity)",
372
+ f"- Filler Words: {analysis['filler_ratio'] * 100:.1f}% (High usage reduces credibility)",
373
+ f"- Anxiety: {analysis['interpretation']['anxiety_level']} (Score: {analysis['composite_scores']['anxiety']:.3f}; stress response)",
374
+ f"- Confidence: {analysis['interpretation']['confidence_level']} (Score: {analysis['composite_scores']['confidence']:.3f}; vocal strength)",
375
+ f"- Fluency: {analysis['interpretation']['fluency_level']} (Drives engagement)",
 
 
376
  "",
377
  "HR Insights:",
378
+ "- Rapid speech (>3.0 wps) may reduce clarity; slower pacing enhances professionalism.",
379
+ "- High filler word usage undermines perceived confidence.",
380
+ "- Elevated anxiety suggests pressure; training can improve resilience.",
381
+ "- Strong confidence supports leadership presence.",
382
+ "- Fluent speech enhances engagement in team settings."
383
  ]
384
  return "\n".join(interpretation_lines)
385
 
386
+ def generate_anxiety_confidence_chart(composite_scores: Dict, chart_buffer):
387
  try:
388
  labels = ['Anxiety', 'Confidence']
389
  scores = [composite_scores.get('anxiety', 0), composite_scores.get('confidence', 0)]
390
  fig, ax = plt.subplots(figsize=(5, 3.5))
391
  bars = ax.bar(labels, scores, color=['#FF5252', '#26A69A'], edgecolor='black', width=0.45)
392
+ ax.set_ylabel('Score', fontsize=12)
393
  ax.set_title('Vocal Dynamics: Anxiety vs. Confidence', fontsize=14, pad=15)
394
  ax.set_ylim(0, 1.3)
395
  for bar in bars:
396
  height = bar.get_height()
397
  ax.text(bar.get_x() + bar.get_width()/2, height + 0.05, f"{height:.2f}",
398
+ ha='center', color='black', fontweight='bold', fontsize=10)
399
  ax.grid(True, axis='y', linestyle='--', alpha=0.7)
400
  plt.tight_layout()
401
+ plt.savefig(chart_buffer, format='png', bbox_inches='tight', dpi=300)
402
  plt.close(fig)
403
  except Exception as e:
404
  logger.error(f"Error generating chart: {str(e)}")
405
 
406
  def calculate_acceptance_probability(analysis_data: Dict) -> float:
407
  voice = analysis_data.get('voice_analysis', {})
408
+ if 'error' in voice: return 50.0
409
  w_confidence, w_anxiety, w_fluency, w_speaking_rate, w_filler_repetition, w_content_strengths = 0.35, -0.25, 0.2, 0.15, -0.15, 0.25
410
  confidence_score = voice.get('composite_scores', {}).get('confidence', 0.0)
411
  anxiety_score = voice.get('composite_scores', {}).get('anxiety', 0.0)
 
423
  content_strength_val = 0.85 if analysis_data.get('text_analysis', {}).get('total_duration', 0) > 60 else 0.4
424
  raw_score = (confidence_score * w_confidence + (1 - anxiety_score) * abs(w_anxiety) + fluency_val * w_fluency + speaking_rate_score * w_speaking_rate + filler_repetition_score * abs(w_filler_repetition) + content_strength_val * w_content_strengths)
425
  max_possible_score = (w_confidence + abs(w_anxiety) + w_fluency + w_speaking_rate + abs(w_filler_repetition) + w_content_strengths)
426
+ normalized_score = raw_score / max_possible_score if max_possible_score > 0 else 0.5
 
427
  acceptance_probability = max(0.0, min(1.0, normalized_score))
428
  return float(f"{acceptance_probability * 100:.2f}")
429
 
 
431
  try:
432
  voice = analysis_data.get('voice_analysis', {})
433
  voice_interpretation = generate_voice_interpretation(voice)
434
+ interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
435
+ acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
436
+ acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
437
+ if acceptance_prob >= 80:
438
+ acceptance_line += "HR Verdict: Outstanding candidate, recommended for immediate advancement."
439
+ elif acceptance_prob >= 60:
440
+ acceptance_line += "HR Verdict: Strong candidate, suitable for further evaluation."
441
+ elif acceptance_prob >= 40:
442
+ acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
443
+ else:
444
+ acceptance_line += "HR Verdict: Limited fit, significant improvement required."
445
  prompt = f"""
446
+ You are EvalBot, a senior HR consultant delivering a concise, professional interview analysis report. Use clear headings, bullet points ('-'), and avoid redundancy. Focus on candidate suitability, strengths, and actionable recommendations.
447
  {acceptance_line}
448
  **1. Executive Summary**
449
+ - Summarize performance, key metrics, and hiring potential.
450
+ - Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
451
+ - Speaker Turns: {analysis_data['text_analysis']['speaker_turns']}
452
+ - Participants: {', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))}
453
  **2. Communication and Vocal Dynamics**
454
+ - Evaluate vocal delivery (rate, fluency, confidence).
455
+ - Provide HR insights on workplace alignment.
456
  {voice_interpretation}
457
+ **3. Competency and Content**
458
+ - Assess leadership, problem-solving, communication, adaptability.
459
+ - List strengths and growth areas separately with examples.
460
  - Sample responses:
461
  {chr(10).join(interviewee_responses)}
462
+ **4. Role Fit and Potential**
463
+ - Analyze cultural fit, role readiness, and growth potential.
464
+ **5. Recommendations**
465
+ - Provide prioritized strategies for growth (communication, technical skills, presence).
466
+ - Suggest next steps for hiring managers (advance, train, assess).
 
 
467
  """
468
  response = gemini_model.generate_content(prompt)
469
  return response.text
 
474
  def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
475
  try:
476
  doc = SimpleDocTemplate(output_path, pagesize=letter,
477
+ rightMargin=0.75*inch, leftMargin=0.75*inch,
478
+ topMargin=1*inch, bottomMargin=1*inch)
479
  styles = getSampleStyleSheet()
480
+ h1 = ParagraphStyle(name='Heading1', fontSize=20, leading=24, spaceAfter=18, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
481
+ h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=16, spaceBefore=12, spaceAfter=8, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
482
+ h3 = ParagraphStyle(name='Heading3', fontSize=10, leading=12, spaceBefore=8, spaceAfter=6, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
483
+ body_text = ParagraphStyle(name='BodyText', fontSize=9, leading=12, spaceAfter=6, fontName='Helvetica', textColor=colors.HexColor('#333333'))
484
+ bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=8, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=9)
485
 
486
  story = []
487
 
 
489
  canvas.saveState()
490
  canvas.setFont('Helvetica', 8)
491
  canvas.setFillColor(colors.HexColor('#666666'))
492
+ canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
493
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
494
+ canvas.setLineWidth(0.8)
495
+ canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
496
+ canvas.setFont('Helvetica-Bold', 9)
497
+ canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
498
+ canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
499
  canvas.restoreState()
500
 
501
  # Title Page
502
  story.append(Paragraph("Candidate Interview Analysis", h1))
503
+ story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=9, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
504
+ story.append(Spacer(1, 0.4*inch))
505
+ acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
506
+ story.append(Paragraph("Hiring Suitability Snapshot", h2))
507
+ prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
508
+ story.append(Paragraph(f"Suitability Score: <font size=15 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
509
+ ParagraphStyle(name='Prob', fontSize=11, spaceAfter=10, alignment=1, fontName='Helvetica-Bold')))
510
+ if acceptance_prob >= 80:
511
+ story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
512
+ elif acceptance_prob >= 60:
513
+ story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation.", body_text))
514
+ elif acceptance_prob >= 40:
515
+ story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
516
+ else:
517
+ story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
518
+ story.append(Spacer(1, 0.3*inch))
519
+ table_data = [
520
+ ['Metric', 'Value'],
521
+ ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
522
+ ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
523
+ ['Participants', ', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))],
524
+ ]
525
+ table = Table(table_data, colWidths=[2.3*inch, 3.7*inch])
526
+ table.setStyle(TableStyle([
527
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
528
+ ('TEXTCOLOR', (0,0), (-1,0), colors.white),
529
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
530
+ ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
531
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
532
+ ('FONTSIZE', (0,0), (-1,-1), 9),
533
+ ('BOTTOMPADDING', (0,0), (-1,0), 8),
534
+ ('TOPPADDING', (0,0), (-1,0), 8),
535
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
536
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
537
+ ]))
538
+ story.append(table)
539
+ story.append(Spacer(1, 0.4*inch))
 
540
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
541
  story.append(PageBreak())
542
 
 
551
  ['Metric', 'Value', 'HR Insight'],
552
  ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
553
  ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
554
+ ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
555
+ ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
556
+ ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
557
  ]
558
+ table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.2*inch])
559
  table.setStyle(TableStyle([
560
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
561
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
 
563
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
564
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
565
  ('FONTSIZE', (0,0), (-1,-1), 9),
566
+ ('BOTTOMPADDING', (0,0), (-1,0), 8),
567
+ ('TOPPADDING', (0,0), (-1,0), 8),
568
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
569
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
570
  ]))
571
  story.append(table)
572
+ story.append(Spacer(1, 0.2*inch))
573
  chart_buffer = io.BytesIO()
574
  generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
575
  chart_buffer.seek(0)
576
+ img = Image(chart_buffer, width=4.5*inch, height=3*inch)
577
  img.hAlign = 'CENTER'
578
  story.append(img)
579
  else:
580
  story.append(Paragraph("Vocal analysis unavailable.", body_text))
581
+ story.append(Spacer(1, 0.2*inch))
582
 
583
  # Parse Gemini Report
584
  sections = {
585
  "Executive Summary": [],
586
+ "Communication": [],
587
+ "Competency": {"Strengths": [], "Growth Areas": []},
588
+ "Recommendations": {"Development": [], "Next Steps": []},
589
+ "Role Fit": [],
590
  }
 
591
  current_section = None
592
+ current_subsection = None
593
+ lines = gemini_report_text.split('\n')
594
+ for line in lines:
595
+ line = line.strip()
596
+ if not line: continue
597
+ if re.match(r'\s*\*\*\s*\d*\.?\s*.*?)\s*\*\*', line):
598
+ section_match = re.search(r'\s*\*\*\s*\d*\.?\s*(.*?)\s*\*\*', line)
599
+ section_title = section_match.group(1).strip()
600
+ if section_title.startswith('Executive Summary'):
601
+ current_section = 'Executive Summary'
602
+ current_subsection = None
603
+ elif 'Communication' in section_title:
604
+ current_section = 'Communication'
605
+ current_subsection = None
606
+ elif 'Competency' in section_title:
607
+ current_section = 'Competency'
608
+ current_subsection = None
609
+ elif 'Role Fit' in section_title:
610
+ current_section = 'Role Fit'
611
+ current_subsection = None
612
+ elif 'Recommendations' in section_title:
613
+ current_section = 'Recommendations'
614
+ current_subsection = None
615
+ elif line.startswith(('-', '*', '•')) and current_section:
616
+ clean_line = line.lstrip('-*• ').strip()
617
+ if not clean_line: continue
618
+ if current_section == 'Competency':
619
+ if any(k in clean_line.lower() for k in ['leadership', 'problem-solving', 'communication', 'adaptability', 'strength']):
620
+ current_subsection = 'Strengths'
621
+ elif any(k in clean_line.lower() for k in ['improve', 'grow', 'depth', 'challenge']):
622
+ current_subsection = 'Growth Areas'
623
+ if current_subsection:
624
+ sections[current_section][current_subsection].append(clean_line)
625
+ elif current_section == 'Recommendations':
626
+ if any(k in clean_line.lower() for k in ['communication', 'technical', 'depth', 'presence']):
627
+ current_subsection = 'Development'
628
+ elif any(k in clean_line.lower() for k in ['advance', 'train', 'assess', 'next', 'mentor']):
629
+ current_subsection = 'Next Steps'
630
+ if current_subsection:
631
+ sections[current_section][current_subsection].append(clean_line)
632
  else:
633
+ sections[current_section].append(clean_line)
634
 
635
  # Executive Summary
636
  story.append(Paragraph("2. Executive Summary", h2))
637
  if sections['Executive Summary']:
638
  for line in sections['Executive Summary']:
639
+ story.append(Paragraph(line, bullet_style))
 
 
 
640
  else:
641
+ story.append(Paragraph("No summary provided.", body_text))
642
+ story.append(Spacer(1, 0.2*inch))
643
 
644
  # Competency and Content
645
+ story.append(Paragraph("3. Competency & Evaluation", h2))
646
  story.append(Paragraph("Strengths", h3))
647
+ if sections['Competency']['Strengths']:
648
+ for line in sections['Competency']['Strengths']:
649
+ story.append(Paragraph(line, bullet_style))
650
  else:
651
  story.append(Paragraph("No strengths identified.", body_text))
652
+ story.append(Spacer(1, 0.1*inch))
653
  story.append(Paragraph("Growth Areas", h3))
654
+ if sections['Competency']['Growth Areas']:
655
+ for line in sections['Competency']['Growth Areas']:
656
+ story.append(Paragraph(line, bullet_style))
657
  else:
658
+ story.append(Paragraph("No growth areas identified; maintain current strengths.", body_text))
659
+ story.append(Spacer(1, 0.2*inch))
660
 
661
  # Role Fit
662
  story.append(Paragraph("4. Role Fit & Potential", h2))
663
+ if sections['Role Fit']:
664
+ for line in sections['Role Fit']:
665
+ story.append(Paragraph(line, bullet_style))
 
 
 
666
  else:
667
+ story.append(Paragraph("No fit analysis provided.", body_text))
668
+ story.append(Spacer(1, 0.2*inch))
669
 
670
+ # Recommendations
671
+ story.append(Paragraph("5. Recommendations", h2))
672
  story.append(Paragraph("Development Priorities", h3))
673
+ if sections['Recommendations']['Development']:
674
+ for line in sections['Recommendations']['Development']:
675
+ story.append(Paragraph(line, bullet_style))
676
  else:
677
  story.append(Paragraph("No development priorities specified.", body_text))
678
+ story.append(Spacer(1, 0.1*inch))
679
+ story.append(Paragraph("Next Steps", h3))
680
+ if sections['Recommendations']['Next Steps']:
681
+ for line in sections['Recommendations']['Next Steps']:
682
+ story.append(Paragraph(line, bullet_style))
683
  else:
684
  story.append(Paragraph("No next steps provided.", body_text))
685
+ story.append(Spacer(1, 0.2*inch))
686
+ story.append(Paragraph("This report provides actionable insights to support hiring and candidate development.", body_text))
687
 
688
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
689
  return True
690
  except Exception as e:
691
+ logger.error(f"PDF generation failed: {str(e)}")
692
  return False
693
 
694
  def convert_to_serializable(obj):
695
  if isinstance(obj, np.generic): return obj.item()
696
  if isinstance(obj, dict): return {k: convert_to_serializable(v) for k, v in obj.items()}
697
+ if isinstance(obj, list): return [convert_to_serializable(item) for item in obj]
698
  if isinstance(obj, np.ndarray): return obj.tolist()
699
  return obj
700
 
 
737
  base_name = str(uuid.uuid4())
738
  pdf_path = os.path.join(OUTPUT_DIR, f"{base_name}_report.pdf")
739
  json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
740
+ if create_pdf_report(analysis_data, pdf_path, gemini_report_text):
741
+ logger.info(f"PDF report generated at: {pdf_path}")
742
  with open(json_path, 'w') as f:
743
  serializable_data = convert_to_serializable(analysis_data)
744
  json.dump(serializable_data, f, indent=2)
745
  logger.info(f"Processing completed for {audio_path_or_url}")
746
  return {'pdf_path': pdf_path, 'json_path': json_path}
747
  except Exception as e:
748
+ logger.error(f"Processing failed for {audio_path_or_url}: {str(e)}")
749
  raise
750
  finally:
751
  if wav_file and os.path.exists(wav_file):
752
  os.remove(wav_file)
753
  if is_downloaded and local_audio_path and os.path.exists(local_audio_path):
754
  os.remove(local_audio_path)
755
+ logger.info(f"Cleaned up temporary audio file: {local_audio_path}")