norhan12 commited on
Commit
6f2861b
·
verified ·
1 Parent(s): 482bddd

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +224 -75
process_interview.py CHANGED
@@ -592,13 +592,11 @@ def calculate_acceptance_probability(analysis_data: Dict) -> float:
592
 
593
 
594
 
595
-
596
-
597
  def generate_report(analysis_data: Dict) -> str:
598
  try:
599
  voice = analysis_data.get('voice_analysis', {})
600
  voice_interpretation = generate_voice_interpretation(voice)
601
- interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
602
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
603
  acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
604
  if acceptance_prob >= 80:
@@ -609,71 +607,181 @@ def generate_report(analysis_data: Dict) -> str:
609
  acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
610
  else:
611
  acceptance_line += "HR Verdict: Limited fit, significant improvement required."
 
 
612
  prompt = f"""
613
- You are EvalBot, a senior HR consultant delivering a concise, professional interview analysis report. Use clear headings, bullet points ('-'), and avoid redundancy. Ensure text is clean and free of special characters that could break formatting.
 
 
 
614
  {acceptance_line}
615
  **1. Executive Summary**
616
- - Summarize performance, key metrics, and hiring potential.
617
- - Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
618
- - Speaker Turns: {analysis_data['text_analysis']['speaker_turns']}
619
- - Participants: {', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))}
620
  **2. Communication and Vocal Dynamics**
621
- - Evaluate vocal delivery (rate, fluency, confidence).
622
- - Provide HR insights on workplace alignment.
623
- {voice_interpretation}
624
  **3. Competency and Content**
625
- - Assess leadership, problem-solving, communication, adaptability.
626
- - List strengths and growth areas separately with examples.
627
- - Sample responses:
628
  {chr(10).join(interviewee_responses)}
629
  **4. Role Fit and Potential**
630
- - Analyze cultural fit, role readiness, and growth potential.
631
  **5. Recommendations**
632
- - Provide prioritized strategies for growth (communication, technical skills, presence).
633
- - Suggest next steps for hiring managers (advance, train, assess).
 
 
 
 
634
  """
635
  response = gemini_model.generate_content(prompt)
636
- return re.sub(r'[^\x00-\x7F]+', '', response.text) # Sanitize non-ASCII characters
 
 
637
  except Exception as e:
638
  logger.error(f"Report generation failed: {str(e)}")
639
  return f"Error generating report: {str(e)}"
640
 
641
  def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str) -> bool:
642
  try:
643
- doc = SimpleDocTemplate(output_path, pagesize=letter,
644
- rightMargin=0.75*inch, leftMargin=0.75*inch,
645
- topMargin=1*inch, bottomMargin=1*inch)
 
 
 
 
 
646
  styles = getSampleStyleSheet()
647
- h1 = ParagraphStyle(name='Heading1', fontSize=20, leading=24, spaceAfter=18, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
648
- h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=16, spaceBefore=12, spaceAfter=8, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
649
- h3 = ParagraphStyle(name='Heading3', fontSize=10, leading=12, spaceBefore=8, spaceAfter=6, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
650
- body_text = ParagraphStyle(name='BodyText', fontSize=9, leading=12, spaceAfter=6, fontName='Helvetica', textColor=colors.HexColor('#333333'))
651
- bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=8, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=9)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
  story = []
654
 
655
  def header_footer(canvas, doc):
656
  canvas.saveState()
 
657
  canvas.setFont('Helvetica', 8)
658
  canvas.setFillColor(colors.HexColor('#666666'))
659
  canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
 
 
660
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
661
  canvas.setLineWidth(0.8)
662
  canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
663
  canvas.setFont('Helvetica-Bold', 9)
664
  canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
665
- canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
666
  canvas.restoreState()
667
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  # Title Page
669
- story.append(Paragraph("Candidate Interview Analysis", h1))
670
- story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=9, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
671
- story.append(Spacer(1, 0.4*inch))
672
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
673
- story.append(Paragraph("Hiring Suitability Snapshot", h2))
674
- prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
675
- story.append(Paragraph(f"Suitability Score: <font size=15 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
676
- ParagraphStyle(name='Prob', fontSize=11, spaceAfter=10, alignment=1, fontName='Helvetica-Bold')))
 
 
 
 
 
 
 
 
 
 
677
  if acceptance_prob >= 80:
678
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
679
  elif acceptance_prob >= 60:
@@ -682,12 +790,15 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
682
  story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
683
  else:
684
  story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
685
- story.append(Spacer(1, 0.3*inch))
 
 
 
686
  table_data = [
687
- ['Metric', 'Value'],
688
- ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
689
- ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
690
- ['Participants', ', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))],
691
  ]
692
  table = Table(table_data, colWidths=[2.3*inch, 3.7*inch])
693
  table.setStyle(TableStyle([
@@ -700,10 +811,10 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
700
  ('BOTTOMPADDING', (0,0), (-1,0), 8),
701
  ('TOPPADDING', (0,0), (-1,0), 8),
702
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
703
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
704
  ]))
705
  story.append(table)
706
- story.append(Spacer(1, 0.4*inch))
707
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
708
  story.append(PageBreak())
709
 
@@ -715,12 +826,12 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
715
  voice_analysis = analysis_data.get('voice_analysis', {})
716
  if voice_analysis and 'error' not in voice_analysis:
717
  table_data = [
718
- ['Metric', 'Value', 'HR Insight'],
719
- ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
720
- ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
721
- ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
722
- ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
723
- ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
724
  ]
725
  table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.2*inch])
726
  table.setStyle(TableStyle([
@@ -730,10 +841,10 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
730
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
731
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
732
  ('FONTSIZE', (0,0), (-1,-1), 9),
733
- ('BOTTOMPADDING', (0,0), (-1,0), 8),
734
- ('TOPPADDING', (0,0), (-1,0), 8),
735
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
736
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
737
  ]))
738
  story.append(table)
739
  story.append(Spacer(1, 0.2*inch))
@@ -744,16 +855,16 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
744
  img.hAlign = 'CENTER'
745
  story.append(img)
746
  else:
747
- story.append(Paragraph("Vocal analysis unavailable.", body_text))
748
  story.append(Spacer(1, 0.2*inch))
749
 
750
  # Parse Gemini Report
751
  sections = {
752
  "Executive Summary": [],
753
  "Communication": [],
754
- "Competency": {"Strengths": [], "Growth Areas": []},
755
- "Recommendations": {"Development": [], "Next Steps": []},
756
  "Role Fit": [],
 
757
  }
758
  current_section = None
759
  current_subsection = None
@@ -761,11 +872,9 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
761
  for line in lines:
762
  line = line.strip()
763
  if not line: continue
764
- # Simplified regex to avoid parenthesis issues
765
- if line.startswith('**') and line.endswith('**'):
766
- section_title = line.strip('**').strip()
767
- if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
768
- section_title = section_title[2:].strip()
769
  if 'Executive Summary' in section_title:
770
  current_section = 'Executive Summary'
771
  current_subsection = None
@@ -781,27 +890,51 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
781
  elif 'Recommendations' in section_title:
782
  current_section = 'Recommendations'
783
  current_subsection = None
784
- elif line.startswith(('-', '*', '•')) and current_section:
785
- clean_line = line.lstrip('-*• ').strip()
786
  if not clean_line: continue
787
- clean_line = re.sub(r'[()]', '', clean_line) # Remove parentheses
788
  if current_section == 'Competency':
789
- if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength']):
790
  current_subsection = 'Strengths'
791
- elif any(k in clean_line.lower() for k in ['improv', 'grow', 'depth']):
792
- current_subsection = 'Growth Areas'
793
  if current_subsection:
794
  sections[current_section][current_subsection].append(clean_line)
795
  elif current_section == 'Recommendations':
796
- if any(k in clean_line.lower() for k in ['commun', 'tech', 'depth', 'pres']):
797
  current_subsection = 'Development'
798
- elif any(k in clean_line.lower() for k in ['adv', 'train', 'assess', 'next', 'mentor']):
799
  current_subsection = 'Next Steps'
800
  if current_subsection:
801
  sections[current_section][current_subsection].append(clean_line)
802
  else:
803
  sections[current_section].append(clean_line)
804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
  # Executive Summary
806
  story.append(Paragraph("2. Executive Summary", h2))
807
  if sections['Executive Summary']:
@@ -815,17 +948,27 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
815
  story.append(Paragraph("3. Competency & Evaluation", h2))
816
  story.append(Paragraph("Strengths", h3))
817
  if sections['Competency']['Strengths']:
818
- for line in sections['Competency']['Strengths']:
819
- story.append(Paragraph(line, bullet_style))
 
 
 
 
 
820
  else:
821
  story.append(Paragraph("No strengths identified.", body_text))
822
  story.append(Spacer(1, 0.1*inch))
823
- story.append(Paragraph("Growth Areas", h3))
824
- if sections['Competency']['Growth Areas']:
825
- for line in sections['Competency']['Growth Areas']:
826
- story.append(Paragraph(line, bullet_style))
 
 
 
 
 
827
  else:
828
- story.append(Paragraph("No growth areas identified; maintain current strengths.", body_text))
829
  story.append(Spacer(1, 0.2*inch))
830
 
831
  # Role Fit
@@ -841,8 +984,13 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
841
  story.append(Paragraph("5. Recommendations", h2))
842
  story.append(Paragraph("Development Priorities", h3))
843
  if sections['Recommendations']['Development']:
844
- for line in sections['Recommendations']['Development']:
845
- story.append(Paragraph(line, bullet_style))
 
 
 
 
 
846
  else:
847
  story.append(Paragraph("No development priorities specified.", body_text))
848
  story.append(Spacer(1, 0.1*inch))
@@ -862,6 +1010,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
862
  return False
863
 
864
 
 
865
  def convert_to_serializable(obj):
866
  if isinstance(obj, np.generic):
867
  return obj.item()
 
592
 
593
 
594
 
 
 
595
  def generate_report(analysis_data: Dict) -> str:
596
  try:
597
  voice = analysis_data.get('voice_analysis', {})
598
  voice_interpretation = generate_voice_interpretation(voice)
599
+ interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u.get('role') == 'Interviewee'][:5]
600
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
601
  acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
602
  if acceptance_prob >= 80:
 
607
  acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
608
  else:
609
  acceptance_line += "HR Verdict: Limited fit, significant improvement required."
610
+
611
+ # Enhanced prompt with detailed recommendations
612
  prompt = f"""
613
+ You are EvalBot, a senior HR consultant delivering a comprehensive, professional interview analysis report.
614
+ Use clear headings, bullet points ('-'), and avoid redundancy.
615
+ Ensure text is clean, professional, and free of special characters that could break formatting.
616
+ The interview involves two roles: Interviewer and Interviewee, assigned alternately to utterances.
617
  {acceptance_line}
618
  **1. Executive Summary**
619
+ - Summarize the candidate's overall performance, key strengths, and hiring potential in 2-3 bullets.
620
+ - Include metrics: Duration ({analysis_data['text_analysis']['total_duration']:.2f} seconds), Speaker Turns ({analysis_data['text_analysis']['speaker_turns']}).
621
+ - Mention roles: Interviewer and Interviewee.
 
622
  **2. Communication and Vocal Dynamics**
623
+ - Evaluate the Interviewee's vocal delivery (speaking rate, fluency, confidence, anxiety) in 2-3 bullets.
624
+ - Provide one HR insight on workplace communication alignment.
625
+ - Voice Analysis: {voice_interpretation}
626
  **3. Competency and Content**
627
+ - List 2-3 key strengths (e.g., leadership, problem-solving, communication) with specific examples from responses.
628
+ - List 1-2 weaknesses (growth areas) with actionable feedback to address them.
629
+ - Sample Interviewee responses (for context):
630
  {chr(10).join(interviewee_responses)}
631
  **4. Role Fit and Potential**
632
+ - Analyze the Interviewee's cultural fit, role readiness, and long-term growth potential in 2-3 bullets.
633
  **5. Recommendations**
634
+ - Provide 3-4 specific suggestions for improvement focusing on:
635
+ - Communication skills (e.g., clarity, fluency, reducing filler words).
636
+ - Content delivery (e.g., structuring responses, emphasizing key points).
637
+ - Professional presentation (e.g., confidence, tone, engagement).
638
+ - Include practical strategies or examples for each suggestion.
639
+ - Suggest 2-3 specific next steps for hiring managers (e.g., advance to technical round, provide training, conduct behavioral assessment).
640
  """
641
  response = gemini_model.generate_content(prompt)
642
+ # Robust sanitization
643
+ clean_text = re.sub(r'[^\x20-\x7E\n]+', '', response.text)
644
+ return clean_text
645
  except Exception as e:
646
  logger.error(f"Report generation failed: {str(e)}")
647
  return f"Error generating report: {str(e)}"
648
 
649
  def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str) -> bool:
650
  try:
651
+ doc = SimpleDocTemplate(
652
+ output_path,
653
+ pagesize=letter,
654
+ rightMargin=0.75*inch,
655
+ leftMargin=0.75*inch,
656
+ topMargin=1*inch,
657
+ bottomMargin=1*inch
658
+ )
659
  styles = getSampleStyleSheet()
660
+
661
+ # Custom styles
662
+ cover_title = ParagraphStyle(
663
+ name='CoverTitle',
664
+ fontSize=24,
665
+ leading=28,
666
+ spaceAfter=20,
667
+ alignment=1,
668
+ textColor=colors.HexColor('#003087'),
669
+ fontName='Helvetica-Bold'
670
+ )
671
+ h1 = ParagraphStyle(
672
+ name='Heading1',
673
+ fontSize=16,
674
+ leading=20,
675
+ spaceAfter=14,
676
+ alignment=1,
677
+ textColor=colors.HexColor('#003087'),
678
+ fontName='Helvetica-Bold'
679
+ )
680
+ h2 = ParagraphStyle(
681
+ name='Heading2',
682
+ fontSize=12,
683
+ leading=15,
684
+ spaceBefore=10,
685
+ spaceAfter=8,
686
+ textColor=colors.HexColor('#0050BC'),
687
+ fontName='Helvetica-Bold'
688
+ )
689
+ h3 = ParagraphStyle(
690
+ name='Heading3',
691
+ fontSize=10,
692
+ leading=12,
693
+ spaceBefore=8,
694
+ spaceAfter=6,
695
+ textColor=colors.HexColor('#3F7CFF'),
696
+ fontName='Helvetica-Bold'
697
+ )
698
+ body_text = ParagraphStyle(
699
+ name='BodyText',
700
+ fontSize=9,
701
+ leading=12,
702
+ spaceAfter=6,
703
+ fontName='Helvetica',
704
+ textColor=colors.HexColor('#333333')
705
+ )
706
+ bullet_style = ParagraphStyle(
707
+ name='Bullet',
708
+ parent=body_text,
709
+ leftIndent=18,
710
+ bulletIndent=8,
711
+ fontName='Helvetica',
712
+ bulletFontName='Helvetica',
713
+ bulletFontSize=9
714
+ )
715
+ table_header = ParagraphStyle(
716
+ name='TableHeader',
717
+ fontSize=9,
718
+ leading=11,
719
+ textColor=colors.white,
720
+ fontName='Helvetica-Bold'
721
+ )
722
+ table_body = ParagraphStyle(
723
+ name='TableBody',
724
+ fontSize=9,
725
+ leading=11,
726
+ fontName='Helvetica'
727
+ )
728
 
729
  story = []
730
 
731
  def header_footer(canvas, doc):
732
  canvas.saveState()
733
+ # Footer
734
  canvas.setFont('Helvetica', 8)
735
  canvas.setFillColor(colors.HexColor('#666666'))
736
  canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
737
+ canvas.drawRightString(doc.width + doc.leftMargin, 0.5*inch, time.strftime('%B %d, %Y'))
738
+ # Header
739
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
740
  canvas.setLineWidth(0.8)
741
  canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
742
  canvas.setFont('Helvetica-Bold', 9)
743
  canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
 
744
  canvas.restoreState()
745
 
746
+ # Cover Page
747
+ story.append(Spacer(1, 2*inch))
748
+ logo_path = 'logo.png'
749
+ if os.path.exists(logo_path):
750
+ story.append(Image(logo_path, width=2*inch, height=0.75*inch))
751
+ story.append(Spacer(1, 0.3*inch))
752
+ story.append(Paragraph("Candidate Interview Analysis Report", cover_title))
753
+ story.append(Spacer(1, 0.2*inch))
754
+ story.append(Paragraph(f"Candidate ID: {analysis_data.get('user_id', 'N/A')}", body_text))
755
+ story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", body_text))
756
+ story.append(Spacer(1, 0.5*inch))
757
+ story.append(Paragraph("Confidential", ParagraphStyle(
758
+ name='Confidential',
759
+ fontSize=10,
760
+ alignment=1,
761
+ textColor=colors.HexColor('#D32F2F'),
762
+ fontName='Helvetica-Bold'
763
+ )))
764
+ story.append(PageBreak())
765
+
766
  # Title Page
767
+ story.append(Paragraph("Interview Evaluation Summary", h1))
768
+ story.append(Spacer(1, 0.3*inch))
769
+ # Suitability Score
770
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
771
+ prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (
772
+ colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F')
773
+ )
774
+ story.append(Paragraph(
775
+ f"Suitability Score: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
776
+ ParagraphStyle(
777
+ name='Score',
778
+ fontSize=14,
779
+ spaceAfter=12,
780
+ alignment=1,
781
+ fontName='Helvetica-Bold'
782
+ )
783
+ ))
784
+ # HR Verdict
785
  if acceptance_prob >= 80:
786
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
787
  elif acceptance_prob >= 60:
 
790
  story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
791
  else:
792
  story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
793
+ story.append(Spacer(1, 0.2*inch))
794
+
795
+ # Summary Table with Roles
796
+ roles = sorted(set(u.get('role', 'Unknown') for u in analysis_data.get('transcript', [])))
797
  table_data = [
798
+ [Paragraph('Metric', table_header), Paragraph('Value', table_header)],
799
+ [Paragraph('Interview Duration', table_body), Paragraph(f"{analysis_data['text_analysis'].get('total_duration', 0):.2f} seconds", table_body)],
800
+ [Paragraph('Speaker Turns', table_body), Paragraph(f"{analysis_data['text_analysis'].get('speaker_turns', 0)}", table_body)],
801
+ [Paragraph('Roles', table_body), Paragraph(', '.join(roles), table_body)],
802
  ]
803
  table = Table(table_data, colWidths=[2.3*inch, 3.7*inch])
804
  table.setStyle(TableStyle([
 
811
  ('BOTTOMPADDING', (0,0), (-1,0), 8),
812
  ('TOPPADDING', (0,0), (-1,0), 8),
813
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
814
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EE')),
815
  ]))
816
  story.append(table)
817
+ story.append(Spacer(1, 0.3*inch))
818
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
819
  story.append(PageBreak())
820
 
 
826
  voice_analysis = analysis_data.get('voice_analysis', {})
827
  if voice_analysis and 'error' not in voice_analysis:
828
  table_data = [
829
+ [Paragraph('Metric', table_header), Paragraph('Value', table_header), Paragraph('HR Insight', table_header)],
830
+ [Paragraph('Speaking Rate', table_body), Paragraph(f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", table_body), Paragraph('Benchmark: 2.0-3.0 wps; impacts clarity', table_body)],
831
+ [Paragraph('Filler Words', table_body), Paragraph(f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", table_body), Paragraph('High usage may reduce credibility', table_body)],
832
+ [Paragraph('Anxiety', table_body), Paragraph(voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A').title(), table_body), Paragraph(f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}", table_body)],
833
+ [Paragraph('Confidence', table_body), Paragraph(voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A').title(), table_body), Paragraph(f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}", table_body)],
834
+ [Paragraph('Fluency', table_body), Paragraph(voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A').title(), table_body), Paragraph('Drives engagement', table_body)],
835
  ]
836
  table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.2*inch])
837
  table.setStyle(TableStyle([
 
841
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
842
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
843
  ('FONTSIZE', (0,0), (-1,-1), 9),
844
+ ('BOTTOMPADDING', (0,0), (-1,-1), 8),
845
+ ('TOPPADDING', (0,0), (-1,-1), 8),
846
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
847
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EE')),
848
  ]))
849
  story.append(table)
850
  story.append(Spacer(1, 0.2*inch))
 
855
  img.hAlign = 'CENTER'
856
  story.append(img)
857
  else:
858
+ story.append(Paragraph(f"Vocal analysis unavailable: {voice_analysis.get('error', 'No data available')}", body_text))
859
  story.append(Spacer(1, 0.2*inch))
860
 
861
  # Parse Gemini Report
862
  sections = {
863
  "Executive Summary": [],
864
  "Communication": [],
865
+ "Competency": {"Strengths": [], "Weaknesses": []},
 
866
  "Role Fit": [],
867
+ "Recommendations": {"Development": [], "Next Steps": []},
868
  }
869
  current_section = None
870
  current_subsection = None
 
872
  for line in lines:
873
  line = line.strip()
874
  if not line: continue
875
+ heading_match = re.match(r'^\**(\d+\.\s+)?([^\*]+)\**$', line)
876
+ if heading_match:
877
+ section_title = heading_match.group(2).strip()
 
 
878
  if 'Executive Summary' in section_title:
879
  current_section = 'Executive Summary'
880
  current_subsection = None
 
890
  elif 'Recommendations' in section_title:
891
  current_section = 'Recommendations'
892
  current_subsection = None
893
+ elif re.match(r'^[-*•]\s+', line) and current_section:
894
+ clean_line = re.sub(r'^[-*•]\s+', '', line).strip()
895
  if not clean_line: continue
896
+ clean_line = re.sub(r'[()\[\]{}]', '', clean_line)
897
  if current_section == 'Competency':
898
+ if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength', 'effective', 'skill']):
899
  current_subsection = 'Strengths'
900
+ elif any(k in clean_line.lower() for k in ['improv', 'grow', 'weak', 'depth', 'challenge']):
901
+ current_subsection = 'Weaknesses'
902
  if current_subsection:
903
  sections[current_section][current_subsection].append(clean_line)
904
  elif current_section == 'Recommendations':
905
+ if any(k in clean_line.lower() for k in ['commun', 'tech', 'depth', 'pres', 'improve', 'enhance', 'clarity', 'structur', 'tone']):
906
  current_subsection = 'Development'
907
+ elif any(k in clean_line.lower() for k in ['adv', 'train', 'assess', 'next', 'mentor', 'round']):
908
  current_subsection = 'Next Steps'
909
  if current_subsection:
910
  sections[current_section][current_subsection].append(clean_line)
911
  else:
912
  sections[current_section].append(clean_line)
913
 
914
+ # Summary Box
915
+ story.append(Paragraph("Key Highlights", h2))
916
+ summary_data = [
917
+ [Paragraph("Category", table_header), Paragraph("Detail", table_header)],
918
+ [Paragraph("Top Strength", table_body), Paragraph(sections['Competency']['Strengths'][0] if sections['Competency']['Strengths'] else "N/A", table_body)],
919
+ [Paragraph("Key Weakness", table_body), Paragraph(sections['Competency']['Weaknesses'][0] if sections['Competency']['Weaknesses'] else "N/A", table_body)],
920
+ [Paragraph("Top Recommendation", table_body), Paragraph(sections['Recommendations']['Development'][0] if sections['Recommendations']['Development'] else "N/A", table_body)],
921
+ ]
922
+ summary_table = Table(summary_data, colWidths=[2*inch, 4*inch])
923
+ summary_table.setStyle(TableStyle([
924
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
925
+ ('TEXTCOLOR', (0,0), (-1,0), colors.white),
926
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
927
+ ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
928
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
929
+ ('FONTSIZE', (0,0), (-1,-1), 9),
930
+ ('BOTTOMPADDING', (0,0), (-1,-1), 6),
931
+ ('TOPPADDING', (0,0), (-1,-1), 6),
932
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#E8F0FE')),
933
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EE')),
934
+ ]))
935
+ story.append(summary_table)
936
+ story.append(Spacer(1, 0.3*inch))
937
+
938
  # Executive Summary
939
  story.append(Paragraph("2. Executive Summary", h2))
940
  if sections['Executive Summary']:
 
948
  story.append(Paragraph("3. Competency & Evaluation", h2))
949
  story.append(Paragraph("Strengths", h3))
950
  if sections['Competency']['Strengths']:
951
+ strength_table = Table([[Paragraph(line, bullet_style)] for line in sections['Competency']['Strengths']], colWidths=[6*inch])
952
+ strength_table.setStyle(TableStyle([
953
+ ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#E6FFE6')),
954
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
955
+ ('LEFTPADDING', (0,0), (-1,-1), 6),
956
+ ]))
957
+ story.append(strength_table)
958
  else:
959
  story.append(Paragraph("No strengths identified.", body_text))
960
  story.append(Spacer(1, 0.1*inch))
961
+ story.append(Paragraph("Weaknesses", h3))
962
+ if sections['Competency']['Weaknesses']:
963
+ weakness_table = Table([[Paragraph(line, bullet_style)] for line in sections['Competency']['Weaknesses']], colWidths=[6*inch])
964
+ weakness_table.setStyle(TableStyle([
965
+ ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#FFF0F0')),
966
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
967
+ ('LEFTPADDING', (0,0), (-1,-1), 6),
968
+ ]))
969
+ story.append(weakness_table)
970
  else:
971
+ story.append(Paragraph("No weaknesses identified; maintain current strengths.", body_text))
972
  story.append(Spacer(1, 0.2*inch))
973
 
974
  # Role Fit
 
984
  story.append(Paragraph("5. Recommendations", h2))
985
  story.append(Paragraph("Development Priorities", h3))
986
  if sections['Recommendations']['Development']:
987
+ dev_table = Table([[Paragraph(line, bullet_style)] for line in sections['Recommendations']['Development']], colWidths=[6*inch])
988
+ dev_table.setStyle(TableStyle([
989
+ ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#E8F0FE')),
990
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
991
+ ('LEFTPADDING', (0,0), (-1,-1), 6),
992
+ ]))
993
+ story.append(dev_table)
994
  else:
995
  story.append(Paragraph("No development priorities specified.", body_text))
996
  story.append(Spacer(1, 0.1*inch))
 
1010
  return False
1011
 
1012
 
1013
+
1014
  def convert_to_serializable(obj):
1015
  if isinstance(obj, np.generic):
1016
  return obj.item()