norhan12 commited on
Commit
90ba65f
·
verified ·
1 Parent(s): 1c3a078

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +36 -29
process_interview.py CHANGED
@@ -328,12 +328,19 @@ def analyze_interviewee_voice(audio_path: str, utterances: List[Dict]) -> Dict:
328
  y, sr = librosa.load(audio_path, sr=16000)
329
  interviewee_utterances = [u for u in utterances if u['role'] == 'Interviewee']
330
  if not interviewee_utterances:
 
331
  return {'error': 'No interviewee utterances found'}
332
  segments = []
333
  for u in interviewee_utterances:
334
  start = int(u['start'] * sr / 1000)
335
  end = int(u['end'] * sr / 1000)
336
- segments.append(y[start:end])
 
 
 
 
 
 
337
  total_duration = sum(u['prosodic_features']['duration'] for u in interviewee_utterances)
338
  total_words = sum(len(u['text'].split()) for u in interviewee_utterances)
339
  speaking_rate = total_words / total_duration if total_duration > 0 else 0
@@ -355,11 +362,11 @@ def analyze_interviewee_voice(audio_path: str, utterances: List[Dict]) -> Dict:
355
  jitter = np.mean(np.abs(np.diff(pitches))) / pitch_mean if len(pitches) > 1 and pitch_mean > 0 else 0
356
  intensities = []
357
  for segment in segments:
358
- rms = np.mean(librosa.feature.rms(y=segment)[0])
359
- intensities.extend(rms)
360
  intensity_mean = np.mean(intensities) if intensities else 0
361
  intensity_std = np.std(intensities) if intensities else 0
362
- shimmer = np.mean(np.abs(np.diff(intensities))) / intensity_mean if intensity_mean > 0 else 0
363
  anxiety_score = 0.6 * (pitch_std / pitch_mean) + 0.4 * (jitter + shimmer) if pitch_mean > 0 else 0
364
  confidence_score = 0.7 * (1 / (1 + intensity_std)) + 0.3 * (1 / (1 + filler_ratio))
365
  hesitation_score = filler_ratio + repetition_score
@@ -376,12 +383,12 @@ def analyze_interviewee_voice(audio_path: str, utterances: List[Dict]) -> Dict:
376
  'interpretation': {'anxiety_level': anxiety_level, 'confidence_level': confidence_level, 'fluency_level': fluency_level}
377
  }
378
  except Exception as e:
379
- logger.error(f"Voice analysis failed: {str(e)}")
380
- return {'error': str(e)}
381
 
382
  def generate_voice_interpretation(analysis: Dict) -> str:
383
  if 'error' in analysis:
384
- return "Voice analysis unavailable due to processing limitations."
385
  interpretation_lines = [
386
  f"- Speaking Rate: {analysis['speaking_rate']} words/sec (Benchmark: 2.0-3.0 wps; affects clarity)",
387
  f"- Filler Words: {analysis['filler_ratio'] * 100:.1f}% (High usage reduces credibility)",
@@ -413,7 +420,7 @@ def generate_anxiety_confidence_chart(composite_scores: Dict, chart_buffer):
413
  ha='center', color='black', fontweight='bold', fontsize=10)
414
  ax.grid(True, axis='y', linestyle='--', alpha=0.7)
415
  plt.tight_layout()
416
- plt.savefig(chart_buffer, format='png', bbox_inches='tight', dpi=300)
417
  plt.close(fig)
418
  except Exception as e:
419
  logger.error(f"Error generating chart: {str(e)}")
@@ -516,10 +523,10 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
516
  story.append(Paragraph("Candidate Interview Analysis", h1))
517
  story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
518
  story.append(Spacer(1, 0.3*inch))
519
- acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
520
  story.append(Paragraph("Hiring Suitability Snapshot", h2))
521
  prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
522
- story.append(Paragraph(f"Suitability Score: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
523
  ParagraphStyle(name='Prob', fontSize=10, spaceAfter=8, alignment=1, fontName='Helvetica-Bold')))
524
  if acceptance_prob >= 80:
525
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
@@ -533,9 +540,9 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
533
  participants = sorted([p for p in set(u['speaker'] for u in analysis_data['transcript']) if p != 'Unknown'])
534
  table_data = [
535
  ['Metric', 'Value'],
536
- ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
537
  ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
538
- ['Participants', ', '.join(participants)],
539
  ]
540
  table = Table(table_data, colWidths=[2.2*inch, 3.8*inch])
541
  table.setStyle(TableStyle([
@@ -547,7 +554,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
547
  ('FONTSIZE', (0,0), (-1,-1), 8),
548
  ('BOTTOMPADDING', (0,0), (-1,0), 6),
549
  ('TOPPADDING', (0,0), (-1,0), 6),
550
- ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
551
  ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
552
  ]))
553
  story.append(table)
@@ -558,41 +565,41 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
558
  # Detailed Analysis
559
  story.append(Paragraph("Detailed Candidate Evaluation", h1))
560
 
561
- # Communication and Vocal Dynamics
562
  story.append(Paragraph("1. Communication & Vocal Dynamics", h2))
563
  voice_analysis = analysis_data.get('voice_analysis', {})
564
- if voice_analysis and 'error' not in voice_analysis:
565
  table_data = [
566
  ['Metric', 'Value', 'HR Insight'],
567
  ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
568
- ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
569
  ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
570
  ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
571
  ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
572
  ]
573
  table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
574
  table.setStyle(TableStyle([
575
- ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
576
- ('TEXTCOLOR', (0,0), (-1,0), colors.white),
577
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
578
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
579
- ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
580
  ('FONTSIZE', (0,0), (-1,-1), 8),
581
- ('BOTTOMPADDING', (0,0), (-1,0), 6),
582
- ('TOPPADDING', (0,0), (-1,0), 6),
583
- ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
584
- ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
585
  ]))
586
  story.append(table)
587
  story.append(Spacer(1, 0.15*inch))
588
  chart_buffer = io.BytesIO()
589
- generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
590
  chart_buffer.seek(0)
591
  img = Image(chart_buffer, width=4.2*inch, height=2.8*inch)
592
  img.hAlign = 'CENTER'
593
  story.append(img)
594
  else:
595
- story.append(Paragraph("Vocal analysis unavailable due to processing limitations.", body_text))
596
  story.append(Spacer(1, 0.15*inch))
597
 
598
  # Parse Gemini Report
@@ -628,12 +635,12 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
628
  elif 'Recommendations' in section_title:
629
  current_section = 'Recommendations'
630
  current_subsection = None
631
- elif line.startswith(('-', '*', '•')) and current_section:
632
- clean_line = line.lstrip('-*• ').strip()
633
  if not clean_line: continue
634
  clean_line = re.sub(r'[()]', '', clean_line)
635
  if current_section == 'Competency':
636
- if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength']):
637
  current_subsection = 'Strengths'
638
  elif any(k in clean_line.lower() for k in ['improv', 'grow', 'depth']):
639
  current_subsection = 'Growth Areas'
@@ -793,6 +800,6 @@ def process_interview(audio_url: str) -> Dict:
793
  if is_downloaded and local_audio_path and os.path.exists(local_audio_path):
794
  try:
795
  os.remove(local_audio_path)
796
- logger.info(f"Cleaned up temporary file: {local_audio_path}")
797
  except Exception as e:
798
  logger.error(f"Failed to clean up local audio file {local_audio_path}: {str(e)}")
 
328
  y, sr = librosa.load(audio_path, sr=16000)
329
  interviewee_utterances = [u for u in utterances if u['role'] == 'Interviewee']
330
  if not interviewee_utterances:
331
+ logger.warning("No interviewee utterances found")
332
  return {'error': 'No interviewee utterances found'}
333
  segments = []
334
  for u in interviewee_utterances:
335
  start = int(u['start'] * sr / 1000)
336
  end = int(u['end'] * sr / 1000)
337
+ if end > start and len(y[start:end]) > 0:
338
+ segments.append(y[start:end])
339
+ else:
340
+ logger.warning(f"Invalid segment for utterance: start={start}, end={end}")
341
+ if not segments:
342
+ logger.warning("No valid audio segments for voice analysis")
343
+ return {'error': 'No valid audio segments found'}
344
  total_duration = sum(u['prosodic_features']['duration'] for u in interviewee_utterances)
345
  total_words = sum(len(u['text'].split()) for u in interviewee_utterances)
346
  speaking_rate = total_words / total_duration if total_duration > 0 else 0
 
362
  jitter = np.mean(np.abs(np.diff(pitches))) / pitch_mean if len(pitches) > 1 and pitch_mean > 0 else 0
363
  intensities = []
364
  for segment in segments:
365
+ rms = np.mean(librosa.feature.rms(y=segment)[0]) if len(segment) > 0 else 0.0
366
+ intensities.append(float(rms)) # Fix: Use append instead of extend
367
  intensity_mean = np.mean(intensities) if intensities else 0
368
  intensity_std = np.std(intensities) if intensities else 0
369
+ shimmer = np.mean(np.abs(np.diff(intensities))) / intensity_mean if len(intensities) > 1 and intensity_mean > 0 else 0
370
  anxiety_score = 0.6 * (pitch_std / pitch_mean) + 0.4 * (jitter + shimmer) if pitch_mean > 0 else 0
371
  confidence_score = 0.7 * (1 / (1 + intensity_std)) + 0.3 * (1 / (1 + filler_ratio))
372
  hesitation_score = filler_ratio + repetition_score
 
383
  'interpretation': {'anxiety_level': anxiety_level, 'confidence_level': confidence_level, 'fluency_level': fluency_level}
384
  }
385
  except Exception as e:
386
+ logger.error(f"Voice analysis failed: {str(e)}", exc_info=True)
387
+ return {'error': f'Voice analysis incomplete due to audio processing issues: {str(e)}'}
388
 
389
  def generate_voice_interpretation(analysis: Dict) -> str:
390
  if 'error' in analysis:
391
+ return f"Voice analysis unavailable: {analysis['error']}"
392
  interpretation_lines = [
393
  f"- Speaking Rate: {analysis['speaking_rate']} words/sec (Benchmark: 2.0-3.0 wps; affects clarity)",
394
  f"- Filler Words: {analysis['filler_ratio'] * 100:.1f}% (High usage reduces credibility)",
 
420
  ha='center', color='black', fontweight='bold', fontsize=10)
421
  ax.grid(True, axis='y', linestyle='--', alpha=0.7)
422
  plt.tight_layout()
423
+ plt.savefig(chart_buffer, format='png', bbox_inches='tight', dpi=100)
424
  plt.close(fig)
425
  except Exception as e:
426
  logger.error(f"Error generating chart: {str(e)}")
 
523
  story.append(Paragraph("Candidate Interview Analysis", h1))
524
  story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
525
  story.append(Spacer(1, 0.3*inch))
526
+ acceptance_prob = float(np.mean([np.mean([np.mean([analysis_data['acceptance_probability'], 0.0])])])) # Ensure float
527
  story.append(Paragraph("Hiring Suitability Snapshot", h2))
528
  prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
529
+ story.append(Paragraph(f"Suitability Score: <font size=14 color='{prob_color.hexval()}'>{acceptance_prob:.2f}%</font>",
530
  ParagraphStyle(name='Prob', fontSize=10, spaceAfter=8, alignment=1, fontName='Helvetica-Bold')))
531
  if acceptance_prob >= 80:
532
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
 
540
  participants = sorted([p for p in set(u['speaker'] for u in analysis_data['transcript']) if p != 'Unknown'])
541
  table_data = [
542
  ['Metric', 'Value'],
543
+ ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.1f} seconds"],
544
  ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
545
+ ['Participants', f"{', '.join(participants)}"],
546
  ]
547
  table = Table(table_data, colWidths=[2.2*inch, 3.8*inch])
548
  table.setStyle(TableStyle([
 
554
  ('FONTSIZE', (0,0), (-1,-1), 8),
555
  ('BOTTOMPADDING', (0,0), (-1,0), 6),
556
  ('TOPPADDING', (0,0), (-1,0), 6),
557
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA'),),
558
  ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
559
  ]))
560
  story.append(table)
 
565
  # Detailed Analysis
566
  story.append(Paragraph("Detailed Candidate Evaluation", h1))
567
 
568
+ # Communication and Speech
569
  story.append(Paragraph("1. Communication & Vocal Dynamics", h2))
570
  voice_analysis = analysis_data.get('voice_analysis', {})
571
+ if voice_analysis' and 'error' not in voice_analysis:
572
  table_data = [
573
  ['Metric', 'Value', 'HR Insight'],
574
  ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
575
+ ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%', 'High usage reduces credibility'],
576
  ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
577
  ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
578
  ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
579
  ]
580
  table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
581
  table.setStyle(TableStyle([
582
+ ('BACKGROUND', (0,0), (-1,0)), colors.HexColor('#0050BC')),
583
+ ('TEXTCOLOR', (0,0), (-1,-0)), colors.white),
584
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
585
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
586
+ ('FONTNAME', (0,0), (-1,-0)), 'Helvetica-Bold'),
587
  ('FONTSIZE', (0,0), (-1,-1), 8),
588
+ ('BOTTOMPADDING', (0,0), (-1,-0)), 6),
589
+ ('TOPPADDING', (0,0), (0,-1), 6),
590
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA'))),
591
+ ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB'))),
592
  ]))
593
  story.append(table)
594
  story.append(Spacer(1, 0.15*inch))
595
  chart_buffer = io.BytesIO()
596
+ generate_anxiety_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
597
  chart_buffer.seek(0)
598
  img = Image(chart_buffer, width=4.2*inch, height=2.8*inch)
599
  img.hAlign = 'CENTER'
600
  story.append(img)
601
  else:
602
+ story.append(Paragraph("Voice analysis unavailable.", body_text))
603
  story.append(Spacer(1, 0.15*inch))
604
 
605
  # Parse Gemini Report
 
635
  elif 'Recommendations' in section_title:
636
  current_section = 'Recommendations'
637
  current_subsection = None
638
+ elif line.startswith('-') and current_section:
639
+ clean_line = line.lstrip('-').strip()
640
  if not clean_line: continue
641
  clean_line = re.sub(r'[()]', '', clean_line)
642
  if current_section == 'Competency':
643
+ if any(k in clean_line.lower() for k in ['leader', 'leadership', 'problem', 'commun', 'adapt', 'strength']):
644
  current_subsection = 'Strengths'
645
  elif any(k in clean_line.lower() for k in ['improv', 'grow', 'depth']):
646
  current_subsection = 'Growth Areas'
 
800
  if is_downloaded and local_audio_path and os.path.exists(local_audio_path):
801
  try:
802
  os.remove(local_audio_path)
803
+ logger.info(f"Cleaned up temporary audio file: {local_audio_path}")
804
  except Exception as e:
805
  logger.error(f"Failed to clean up local audio file {local_audio_path}: {str(e)}")