norhan12 commited on
Commit
129b269
·
verified ·
1 Parent(s): 885934a

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +164 -110
process_interview.py CHANGED
@@ -369,22 +369,22 @@ def analyze_interviewee_voice(audio_path: str, utterances: List[Dict]) -> Dict:
369
 
370
  def generate_voice_interpretation(analysis: Dict) -> str:
371
  if 'error' in analysis:
372
- return "Voice analysis not available due to processing error."
373
  interpretation_lines = [
374
- "Voice and Speech Profile:",
375
- f"- Speaking Rate: {analysis['speaking_rate']} words/sec - Compared to optimal range (2.0-3.0 words/sec)",
376
- f"- Filler Word Usage: {analysis['filler_ratio'] * 100:.1f}% - Frequency of non-content words (e.g., 'um', 'like')",
377
- f"- Repetition Tendency: {analysis['repetition_score']:.3f} - Measure of repeated phrases",
378
- f"- Anxiety Indicator: {analysis['interpretation']['anxiety_level']} (Score: {analysis['composite_scores']['anxiety']:.3f}) - Based on pitch and voice stability",
379
- f"- Confidence Indicator: {analysis['interpretation']['confidence_level']} (Score: {analysis['composite_scores']['confidence']:.3f}) - Derived from vocal consistency",
380
- f"- Fluency Assessment: {analysis['interpretation']['fluency_level']} - Reflects speech flow and coherence",
381
  "",
382
- "HR Insights:",
383
- "- Faster speaking rates may indicate confidence but can suggest nervousness if excessive.",
384
- "- High filler word usage often reduces perceived professionalism and clarity.",
385
- "- Elevated anxiety indicators (pitch variability, jitter) may reflect interview pressure.",
386
- "- Strong confidence scores suggest effective vocal presence and control.",
387
- "- Fluency impacts listener engagement; disfluency may hinder communication effectiveness."
388
  ]
389
  return "\n".join(interpretation_lines)
390
 
@@ -392,17 +392,18 @@ def generate_anxiety_confidence_chart(composite_scores: Dict, chart_path_or_buff
392
  try:
393
  labels = ['Anxiety', 'Confidence']
394
  scores = [composite_scores.get('anxiety', 0), composite_scores.get('confidence', 0)]
395
- fig, ax = plt.subplots(figsize=(4, 2.5))
396
- bars = ax.bar(labels, scores, color=['#FF6B6B', '#4ECDC4'], edgecolor='black')
397
- ax.set_ylabel('Score (Normalized)')
398
- ax.set_title('Vocal Dynamics: Anxiety vs. Confidence')
399
  ax.set_ylim(0, 1.2)
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=10)
 
404
  plt.tight_layout()
405
- plt.savefig(chart_path_or_buffer, format='png', bbox_inches='tight', dpi=150)
406
  plt.close(fig)
407
  except Exception as e:
408
  logger.error(f"Error generating chart: {str(e)}")
@@ -410,21 +411,21 @@ def generate_anxiety_confidence_chart(composite_scores: Dict, chart_path_or_buff
410
  def calculate_acceptance_probability(analysis_data: Dict) -> float:
411
  voice = analysis_data.get('voice_analysis', {})
412
  if 'error' in voice: return 0.0
413
- w_confidence, w_anxiety, w_fluency, w_speaking_rate, w_filler_repetition, w_content_strengths = 0.4, -0.3, 0.2, 0.1, -0.1, 0.2
414
  confidence_score = voice.get('composite_scores', {}).get('confidence', 0.0)
415
  anxiety_score = voice.get('composite_scores', {}).get('anxiety', 0.0)
416
  fluency_level = voice.get('interpretation', {}).get('fluency_level', 'Disfluent')
417
  speaking_rate = voice.get('speaking_rate', 0.0)
418
  filler_ratio = voice.get('filler_ratio', 0.0)
419
  repetition_score = voice.get('repetition_score', 0.0)
420
- fluency_map = {'Fluent': 1.0, 'Moderate': 0.5, 'Disfluent': 0.0}
421
- fluency_val = fluency_map.get(fluency_level, 0.0)
422
  ideal_speaking_rate = 2.5
423
  speaking_rate_deviation = abs(speaking_rate - ideal_speaking_rate)
424
  speaking_rate_score = max(0, 1 - (speaking_rate_deviation / ideal_speaking_rate))
425
  filler_repetition_composite = (filler_ratio + repetition_score) / 2
426
  filler_repetition_score = max(0, 1 - filler_repetition_composite)
427
- content_strength_val = 0.8 if analysis_data.get('text_analysis', {}).get('total_duration', 0) > 0 else 0.0
428
  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)
429
  max_possible_score = (w_confidence + abs(w_anxiety) + w_fluency + w_speaking_rate + abs(w_filler_repetition) + w_content_strengths)
430
  if max_possible_score == 0: return 50.0
@@ -436,38 +437,39 @@ def generate_report(analysis_data: Dict) -> str:
436
  try:
437
  voice = analysis_data.get('voice_analysis', {})
438
  voice_interpretation = generate_voice_interpretation(voice)
439
- interviewee_responses = [f"Speaker {u['speaker']} ({u['role']}): {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
440
  acceptance_prob = analysis_data.get('acceptance_probability', None)
441
  acceptance_line = ""
442
  if acceptance_prob is not None:
443
- acceptance_line = f"\n**Hiring Potential Score: {acceptance_prob:.2f}%**\n"
444
- if acceptance_prob >= 80: acceptance_line += "Assessment: Exceptional candidate, strongly recommended for advancement."
445
- elif acceptance_prob >= 50: acceptance_line += "Assessment: Promising candidate with moderate strengths; consider for further evaluation."
446
- else: acceptance_line += "Assessment: Limited alignment with role expectations; significant development needed."
 
447
  prompt = f"""
448
- You are an expert HR consultant, EvalBot, tasked with producing a professional, concise, and actionable interview analysis report. Structure the report with clear headings, subheadings, and bullet points (use '- ' for bullets). Adopt a formal, HR-professional tone, focusing on candidate evaluation, fit for role, and development insights.
449
  {acceptance_line}
450
  **1. Executive Summary**
451
- - Provide a concise overview of the interview, highlighting key metrics and overall candidate performance.
452
- - Interview duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
453
- - Total speaker turns: {analysis_data['text_analysis']['speaker_turns']}
454
  - Participants: {', '.join(analysis_data['speakers'])}
455
- **2. Communication and Vocal Analysis**
456
- - Evaluate the candidate's vocal delivery, including speaking rate, fluency, and confidence indicators.
457
- - Provide HR-relevant insights into how these metrics impact perceived professionalism and role suitability.
458
  {voice_interpretation}
459
- **3. Content Analysis and Competency Assessment**
460
- - Analyze key themes in the candidate's responses to assess alignment with job competencies (e.g., problem-solving, communication, leadership).
461
- - Identify strengths and areas for improvement, supported by specific examples.
462
- - Sample responses for context:
463
  {chr(10).join(interviewee_responses)}
464
- **4. Fit and Potential Evaluation**
465
- - Assess the candidate's overall fit for a typical professional role based on communication, content, and vocal dynamics.
466
- - Consider cultural fit, adaptability, and readiness for the role.
467
- **5. Actionable HR Recommendations**
468
- - Provide specific, prioritized recommendations for the candidate’s development.
469
- - Focus areas: Effective Communication, Content Clarity and Depth, Professional Presence.
470
- - Suggest next steps for hiring managers (e.g., advance to next round, additional assessments, training focus).
471
  """
472
  response = gemini_model.generate_content(prompt)
473
  return response.text
@@ -478,63 +480,89 @@ def generate_report(analysis_data: Dict) -> str:
478
  def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
479
  try:
480
  doc = SimpleDocTemplate(output_path, pagesize=letter,
481
- rightMargin=0.75*inch, leftMargin=0.75*inch,
482
- topMargin=1*inch, bottomMargin=1*inch)
483
  styles = getSampleStyleSheet()
484
- h1 = ParagraphStyle(name='Heading1', fontSize=22, leading=26, spaceAfter=20, alignment=1, textColor=colors.HexColor('#1A3C5E'))
485
- h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=18, spaceBefore=14, spaceAfter=8, textColor=colors.HexColor('#2E5A87'))
486
- body_text = ParagraphStyle(name='BodyText', parent=styles['Normal'], fontSize=10, leading=14, spaceAfter=8, fontName='Helvetica')
487
- bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=20, bulletIndent=10, fontName='Helvetica')
 
488
 
489
  story = []
490
 
491
  def header_footer(canvas, doc):
492
  canvas.saveState()
493
  canvas.setFont('Helvetica', 9)
494
- canvas.setFillColor(colors.grey)
495
  canvas.drawString(doc.leftMargin, 0.5 * inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
496
  canvas.setStrokeColor(colors.HexColor('#2E5A87'))
497
- canvas.setLineWidth(1)
498
- canvas.line(doc.leftMargin, doc.height + 0.85*inch, doc.width + doc.leftMargin, doc.height + 0.85*inch)
499
- canvas.setFont('Helvetica-Bold', 10)
500
- canvas.drawString(doc.leftMargin, doc.height + 0.9*inch, "Candidate Interview Analysis Report")
 
 
501
  canvas.restoreState()
502
 
503
  # Title Page
504
- story.append(Paragraph("Candidate Interview Analysis Report", h1))
505
- story.append(Paragraph(f"Generated on: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=10, textColor=colors.grey)))
506
- story.append(Spacer(1, 0.5 * inch))
507
  acceptance_prob = analysis_data.get('acceptance_probability')
508
  if acceptance_prob is not None:
509
- story.append(Paragraph("Hiring Potential Snapshot", h2))
510
- prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 70 else (colors.HexColor('#F57C00') if acceptance_prob >= 40 else colors.HexColor('#D32F2F'))
511
- story.append(Paragraph(f"Hiring Potential Score: <font size=16 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
512
- ParagraphStyle(name='Prob', fontSize=12, spaceAfter=12, alignment=1)))
513
  if acceptance_prob >= 80:
514
- story.append(Paragraph("<b>HR Assessment:</b> Exceptional candidate, strongly recommended for advancement to the next stage.", body_text))
515
- elif acceptance_prob >= 50:
516
- story.append(Paragraph("<b>HR Assessment:</b> Promising candidate with moderate strengths; consider for further evaluation.", body_text))
 
 
517
  else:
518
- story.append(Paragraph("<b>HR Assessment:</b> Limited alignment with role expectations; significant development needed.", body_text))
519
- story.append(Spacer(1, 0.3 * inch))
520
- story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Interview Analysis System", body_text))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  story.append(PageBreak())
522
 
523
  # Detailed Analysis
524
- story.append(Paragraph("Detailed Candidate Evaluation", h1))
525
 
526
- story.append(Paragraph("1. Communication and Vocal Profile", h2))
527
  voice_analysis = analysis_data.get('voice_analysis', {})
528
  if voice_analysis and 'error' not in voice_analysis:
529
  table_data = [
530
  ['Metric', 'Value', 'HR Insight'],
531
- ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Optimal: 2.0-3.0 wps; impacts clarity and confidence'],
532
- ['Filler Word Usage', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage may reduce perceived professionalism'],
533
- ['Anxiety Indicator', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}; reflects pressure response"],
534
- ['Confidence Indicator', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}; indicates vocal authority"],
535
- ['Fluency Assessment', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Affects engagement and message delivery']
536
  ]
537
- table = Table(table_data, colWidths=[1.8*inch, 1.2*inch, 3.5*inch])
538
  table.setStyle(TableStyle([
539
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#2E5A87')),
540
  ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
@@ -548,22 +576,22 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
548
  ('GRID', (0,0), (-1,-1), 1, colors.HexColor('#DDE4EB'))
549
  ]))
550
  story.append(table)
551
- story.append(Spacer(1, 0.25 * inch))
552
  chart_buffer = io.BytesIO()
553
  generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
554
  chart_buffer.seek(0)
555
- img = Image(chart_buffer, width=4.5*inch, height=2.8*inch)
556
  img.hAlign = 'CENTER'
557
  story.append(img)
558
  else:
559
- story.append(Paragraph("Voice analysis unavailable due to processing limitations.", body_text))
560
- story.append(Spacer(1, 0.3 * inch))
561
 
562
  # Parse Gemini Report
563
  sections = {}
564
- section_titles = ["Executive Summary", "Communication and Vocal Analysis",
565
- "Content Analysis and Competency Assessment",
566
- "Fit and Potential Evaluation", "Actionable HR Recommendations"]
567
  for title in section_titles:
568
  sections[title] = []
569
  report_parts = re.split(r'(\s*\*\*\s*\d\.\s*.*?\s*\*\*)', gemini_report_text)
@@ -588,43 +616,69 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
588
  else:
589
  story.append(Paragraph(line, body_text))
590
  else:
591
- story.append(Paragraph("Summary not available from analysis.", body_text))
592
- story.append(Spacer(1, 0.3 * inch))
593
-
594
- # Content and Competency
595
- story.append(Paragraph("3. Content and Competency Assessment", h2))
596
- if sections['Content Analysis and Competency Assessment']:
597
- for line in sections['Content Analysis and Competency Assessment']:
598
- if line.startswith(('-', '•', '*')):
 
 
599
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
600
- else:
601
- story.append(Paragraph(line, body_text))
 
 
 
 
 
 
 
 
 
 
602
  else:
603
- story.append(Paragraph("Content and competency analysis not provided.", body_text))
604
  story.append(PageBreak())
605
 
606
- # Fit and Potential
607
- story.append(Paragraph("4. Fit and Potential Evaluation", h2))
608
- if sections['Fit and Potential Evaluation']:
609
- for line in sections['Fit and Potential Evaluation']:
610
  if line.startswith(('-', '•', '*')):
611
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
612
  else:
613
  story.append(Paragraph(line, body_text))
614
  else:
615
- story.append(Paragraph("Fit and potential evaluation not available.", body_text))
616
- story.append(Spacer(1, 0.3 * inch))
617
 
618
  # HR Recommendations
619
- story.append(Paragraph("5. Actionable HR Recommendations", h2))
620
- if sections['Actionable HR Recommendations']:
621
- for line in sections['Actionable HR Recommendations']:
622
- if line.startswith(('-', '•', '*')):
 
 
623
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
624
- else:
625
- story.append(Paragraph(line, body_text))
 
 
 
 
 
 
 
 
 
 
626
  else:
627
- story.append(Paragraph("HR recommendations not provided.", body_text))
 
 
628
 
629
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
630
  return True
 
369
 
370
  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, professional delivery",
376
+ f"- Filler Word Frequency: {analysis['filler_ratio'] * 100:.1f}% - Measures non-content words (e.g., 'um', 'like')",
377
+ f"- Repetition Index: {analysis['repetition_score']:.3f} - Frequency of repeated phrases or ideas",
378
+ f"- Anxiety Indicator: {analysis['interpretation']['anxiety_level']} (Score: {analysis['composite_scores']['anxiety']:.3f}) - Derived from pitch variation and vocal stability",
379
+ f"- Confidence Indicator: {analysis['interpretation']['confidence_level']} (Score: {analysis['composite_scores']['confidence']:.3f}) - Reflects vocal strength and consistency",
380
+ f"- Fluency Rating: {analysis['interpretation']['fluency_level']} - Assesses speech flow and coherence",
381
  "",
382
+ "HR Performance Insights:",
383
+ "- Rapid speech (>3.0 wps) may signal enthusiasm but risks clarity; slower, deliberate pacing enhances professionalism.",
384
+ "- Elevated filler word use reduces perceived polish and can distract from key messages.",
385
+ "- High anxiety scores suggest interview pressure; training can build resilience.",
386
+ "- Strong confidence indicators align with leadership presence and effective communication.",
387
+ "- Fluent speech enhances engagement, critical for client-facing or team roles."
388
  ]
389
  return "\n".join(interpretation_lines)
390
 
 
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))
396
+ bars = ax.bar(labels, scores, color=['#FF6B6B', '#4ECDC4'], edgecolor='black', width=0.6)
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.2)
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=200)
407
  plt.close(fig)
408
  except Exception as e:
409
  logger.error(f"Error generating chart: {str(e)}")
 
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)
417
  fluency_level = voice.get('interpretation', {}).get('fluency_level', 'Disfluent')
418
  speaking_rate = voice.get('speaking_rate', 0.0)
419
  filler_ratio = voice.get('filler_ratio', 0.0)
420
  repetition_score = voice.get('repetition_score', 0.0)
421
+ fluency_map = {'Fluent': 1.0, 'Moderate': 0.6, 'Disfluent': 0.2}
422
+ fluency_val = fluency_map.get(fluency_level, 0.2)
423
  ideal_speaking_rate = 2.5
424
  speaking_rate_deviation = abs(speaking_rate - ideal_speaking_rate)
425
  speaking_rate_score = max(0, 1 - (speaking_rate_deviation / ideal_speaking_rate))
426
  filler_repetition_composite = (filler_ratio + repetition_score) / 2
427
  filler_repetition_score = max(0, 1 - filler_repetition_composite)
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
 
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 visually engaging interview analysis report. Use a professional tone, clear headings, and bullet points ('- ') for readability. Focus on candidate suitability, strengths, and actionable growth strategies.
451
  {acceptance_line}
452
  **1. Executive Summary**
453
+ - Deliver a crisp overview of the candidate's performance, emphasizing 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
+ - Assess the candidate's vocal delivery (rate, fluency, confidence) and its impact on professional presence.
459
+ - Provide HR insights on how these traits align with workplace expectations.
460
  {voice_interpretation}
461
+ **3. Competency and Content Evaluation**
462
+ - Evaluate responses for core competencies: leadership, problem-solving, communication, adaptability.
463
+ - Highlight strengths and growth areas with specific, concise examples.
464
+ - Sample responses:
465
  {chr(10).join(interviewee_responses)}
466
+ **4. Role Fit and Growth Potential**
467
+ - Analyze alignment with professional roles, focusing on cultural fit, readiness, and scalability.
468
+ - Consider enthusiasm, teamwork, and long-term potential.
469
+ **5. Strategic HR Recommendations**
470
+ - Offer prioritized, actionable strategies to enhance candidate performance.
471
+ - Target: Communication Effectiveness, Response Depth, Professional Impact.
472
+ - Suggest clear next steps for hiring managers (e.g., advance, train, assess).
473
  """
474
  response = gemini_model.generate_content(prompt)
475
  return response.text
 
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.6*inch, leftMargin=0.6*inch,
484
+ topMargin=0.8*inch, bottomMargin=0.8*inch)
485
  styles = getSampleStyleSheet()
486
+ h1 = ParagraphStyle(name='Heading1', fontSize=24, leading=28, spaceAfter=25, alignment=1, textColor=colors.HexColor('#1A3C5E'), fontName='Helvetica-Bold')
487
+ h2 = ParagraphStyle(name='Heading2', fontSize=16, leading=20, spaceBefore=16, spaceAfter=10, textColor=colors.HexColor('#2E5A87'), fontName='Helvetica-Bold')
488
+ h3 = ParagraphStyle(name='Heading3', fontSize=12, leading=16, spaceBefore=12, spaceAfter=8, textColor=colors.HexColor('#4A6FA5'), fontName='Helvetica')
489
+ body_text = ParagraphStyle(name='BodyText', parent=styles['Normal'], fontSize=10, leading=14, spaceAfter=10, fontName='Helvetica')
490
+ bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=25, bulletIndent=12, fontName='Helvetica')
491
 
492
  story = []
493
 
494
  def header_footer(canvas, doc):
495
  canvas.saveState()
496
  canvas.setFont('Helvetica', 9)
497
+ canvas.setFillColor(colors.HexColor('#666666'))
498
  canvas.drawString(doc.leftMargin, 0.5 * inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
499
  canvas.setStrokeColor(colors.HexColor('#2E5A87'))
500
+ canvas.setLineWidth(1.2)
501
+ canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
502
+ canvas.setFont('Helvetica-Bold', 11)
503
+ canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
504
+ canvas.setFillColor(colors.HexColor('#666666'))
505
+ canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
506
  canvas.restoreState()
507
 
508
  # Title Page
509
+ story.append(Paragraph("Candidate Interview Analysis", h1))
510
+ story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=11, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
511
+ story.append(Spacer(1, 0.6 * inch))
512
  acceptance_prob = analysis_data.get('acceptance_probability')
513
  if acceptance_prob is not None:
514
+ story.append(Paragraph("Hiring Suitability Overview", h2))
515
+ prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
516
+ story.append(Paragraph(f"Hiring Suitability Score: <font size=18 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
517
+ ParagraphStyle(name='Prob', fontSize=14, spaceAfter=15, alignment=1, fontName='Helvetica-Bold')))
518
  if acceptance_prob >= 80:
519
+ story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, highly recommended for immediate advancement.", body_text))
520
+ elif acceptance_prob >= 60:
521
+ story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation with targeted development.", body_text))
522
+ elif acceptance_prob >= 40:
523
+ story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, requires additional assessment and skill-building.", body_text))
524
  else:
525
+ story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement needed for role alignment.", body_text))
526
+ story.append(Spacer(1, 0.4 * inch))
527
+ table_data = [
528
+ ['Key Metrics', 'Value'],
529
+ ['Interview Length', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
530
+ ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
531
+ ['Participants', ', '.join(analysis_data['speakers'])]
532
+ ]
533
+ table = Table(table_data, colWidths=[2.5*inch, 4*inch])
534
+ table.setStyle(TableStyle([
535
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#2E5A87')),
536
+ ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
537
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
538
+ ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
539
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
540
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
541
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
542
+ ('TOPPADDING', (0, 0), (-1, 0), 12),
543
+ ('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#F5F7FA')),
544
+ ('GRID', (0,0), (-1,-1), 1, colors.HexColor('#DDE4EB'))
545
+ ]))
546
+ story.append(table)
547
+ story.append(Spacer(1, 0.5 * inch))
548
+ story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis System", body_text))
549
  story.append(PageBreak())
550
 
551
  # Detailed Analysis
552
+ story.append(Paragraph("Detailed Candidate Profile", h1))
553
 
554
+ story.append(Paragraph("1. Communication & Vocal Dynamics", h2))
555
  voice_analysis = analysis_data.get('voice_analysis', {})
556
  if voice_analysis and 'error' not in voice_analysis:
557
  table_data = [
558
  ['Metric', 'Value', 'HR Insight'],
559
+ ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; affects clarity, poise'],
560
+ ['Filler Word Frequency', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'Excess use impacts polish, credibility'],
561
+ ['Anxiety Indicator', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}; shows stress response"],
562
+ ['Confidence Indicator', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}; reflects vocal strength"],
563
+ ['Fluency Rating', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement, message impact']
564
  ]
565
+ table = Table(table_data, colWidths=[1.9*inch, 1.3*inch, 3.3*inch])
566
  table.setStyle(TableStyle([
567
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#2E5A87')),
568
  ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
 
576
  ('GRID', (0,0), (-1,-1), 1, colors.HexColor('#DDE4EB'))
577
  ]))
578
  story.append(table)
579
+ story.append(Spacer(1, 0.3 * 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=5*inch, height=3*inch)
584
  img.hAlign = 'CENTER'
585
  story.append(img)
586
  else:
587
+ story.append(Paragraph("Vocal analysis unavailable due to processing constraints.", body_text))
588
+ story.append(Spacer(1, 0.4 * inch))
589
 
590
  # Parse Gemini Report
591
  sections = {}
592
+ section_titles = ["Executive Summary", "Communication and Vocal Dynamics",
593
+ "Competency and Content Evaluation",
594
+ "Role Fit and Growth Potential", "Strategic HR Recommendations"]
595
  for title in section_titles:
596
  sections[title] = []
597
  report_parts = re.split(r'(\s*\*\*\s*\d\.\s*.*?\s*\*\*)', gemini_report_text)
 
616
  else:
617
  story.append(Paragraph(line, body_text))
618
  else:
619
+ story.append(Paragraph("Executive summary unavailable.", body_text))
620
+ story.append(Spacer(1, 0.4 * inch))
621
+
622
+ # Competency and Content
623
+ story.append(Paragraph("3. Competency & Content Evaluation", h2))
624
+ if sections['Competency and Content Evaluation']:
625
+ story.append(Paragraph("Strengths", h3))
626
+ strengths_found = False
627
+ for line in sections['Competency and Content Evaluation']:
628
+ if 'strength' in line.lower() or any(k in line.lower() for k in ['leadership', 'problem-solving', 'communication', 'adaptability']):
629
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
630
+ strengths_found = True
631
+ if not strengths_found:
632
+ story.append(Paragraph("No specific strengths identified.", body_text))
633
+ story.append(Spacer(1, 0.2 * inch))
634
+ story.append(Paragraph("Growth Areas", h3))
635
+ growth_found = False
636
+ for line in sections['Competency and Content Evaluation']:
637
+ if 'improve' in line.lower() or 'weak' in line.lower() or 'challenge' in line.lower():
638
+ story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
639
+ growth_found = True
640
+ if not growth_found:
641
+ story.append(Paragraph("No specific growth areas identified.", body_text))
642
  else:
643
+ story.append(Paragraph("Competency and content evaluation unavailable.", body_text))
644
  story.append(PageBreak())
645
 
646
+ # Role Fit
647
+ story.append(Paragraph("4. Role Fit & Growth Potential", h2))
648
+ if sections['Role Fit and Growth Potential']:
649
+ for line in sections['Role Fit and Growth Potential']:
650
  if line.startswith(('-', '•', '*')):
651
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
652
  else:
653
  story.append(Paragraph(line, body_text))
654
  else:
655
+ story.append(Paragraph("Role fit and potential analysis unavailable.", body_text))
656
+ story.append(Spacer(1, 0.4 * inch))
657
 
658
  # HR Recommendations
659
+ story.append(Paragraph("5. Strategic HR Recommendations", h2))
660
+ if sections['Strategic HR Recommendations']:
661
+ story.append(Paragraph("Development Priorities", h3))
662
+ dev_found = False
663
+ for line in sections['Strategic HR Recommendations']:
664
+ if any(k in line.lower() for k in ['communication', 'clarity', 'depth', 'presence', 'improve']):
665
  story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
666
+ dev_found = True
667
+ if not dev_found:
668
+ story.append(Paragraph("No development priorities specified.", body_text))
669
+ story.append(Spacer(1, 0.2 * inch))
670
+ story.append(Paragraph("Next Steps for Hiring Managers", h3))
671
+ steps_found = False
672
+ for line in sections['Strategic HR Recommendations']:
673
+ if any(k in line.lower() for k in ['advance', 'train', 'assess', 'next step']):
674
+ story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
675
+ steps_found = True
676
+ if not steps_found:
677
+ story.append(Paragraph("No specific next steps provided.", body_text))
678
  else:
679
+ story.append(Paragraph("Strategic recommendations unavailable.", body_text))
680
+ story.append(Spacer(1, 0.3 * inch))
681
+ story.append(Paragraph("This report delivers a comprehensive, data-driven evaluation to guide hiring decisions and candidate development.", body_text))
682
 
683
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
684
  return True