Update process_interview.py
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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
|
| 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=
|
| 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 =
|
| 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()}'
|
| 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']:.
|
| 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
|
| 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}%
|
| 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
|
| 577 |
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
| 578 |
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 579 |
-
('FONTNAME', (0,0), (-1
|
| 580 |
('FONTSIZE', (0,0), (-1,-1), 8),
|
| 581 |
-
('BOTTOMPADDING', (0,0), (-1
|
| 582 |
-
('TOPPADDING', (0,0), (
|
| 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 |
-
|
| 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("
|
| 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(
|
| 632 |
-
clean_line = line.lstrip('
|
| 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)}")
|