norhan12 commited on
Commit
f8d3eaf
·
verified ·
1 Parent(s): d03ffa1

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +235 -374
process_interview.py CHANGED
@@ -594,413 +594,274 @@ def calculate_acceptance_probability(analysis_data: Dict) -> float:
594
  return float(f"{acceptance_probability * 100:.2f}") # Return as percentage
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
-
602
- # Extract key metrics for the prompt
603
- total_duration = analysis_data['text_analysis']['total_duration']
604
- speaker_turns = analysis_data['text_analysis']['speaker_turns']
605
- speakers = ', '.join(analysis_data['speakers'])
606
-
607
- # Get acceptance probability with enhanced interpretation
608
- acceptance_prob = analysis_data.get('acceptance_probability', None)
609
- acceptance_line = ""
610
- if acceptance_prob is not None:
611
- acceptance_line = f"\n**Estimated Acceptance Probability: {acceptance_prob:.2f}%**\n"
612
- if acceptance_prob >= 85:
613
- acceptance_line += "This candidate demonstrates exceptional qualifications and interview performance."
614
- elif acceptance_prob >= 70:
615
- acceptance_line += "This is a strong candidate with high potential for the role."
616
- elif acceptance_prob >= 50:
617
- acceptance_line += "This candidate shows promise but would benefit from targeted development."
618
- else:
619
- acceptance_line += "This candidate may not be the ideal fit based on current evaluation."
620
-
621
- # Prepare structured prompt for Gemini
622
  prompt = f"""
623
- As EvalBot, an AI interview analysis system, generate a comprehensive yet concise professional interview report.
624
- Focus on delivering actionable insights in a structured format. Use clear section headings and bullet points.
625
-
626
  {acceptance_line}
627
-
628
  **1. Executive Summary**
629
- Provide a succinct overview highlighting:
630
- - Interview duration: {total_duration:.1f} seconds
631
- - Speaker interactions: {speaker_turns} turns among {speakers}
632
- - Key themes and overall impression
633
-
634
- **2. Voice Analysis Highlights**
 
635
  {voice_interpretation}
636
-
637
- **3. Content Evaluation**
638
- Analyze responses for:
639
- - Technical competence and knowledge depth
640
- - Problem-solving approach
641
- - Communication effectiveness
642
- - Cultural/organizational fit
643
-
644
- **4. Development Opportunities**
645
- Identify 3-5 specific areas for improvement with concrete examples.
646
-
647
- **5. Actionable Recommendations**
648
- Provide tailored suggestions in these categories:
649
- - Immediate onboarding focus (if hired)
650
- - Skill development priorities
651
- - Interview technique refinement
652
- - Professional growth opportunities
653
-
654
- **Formatting Guidelines:**
655
- - Use concise, professional language
656
- - Prioritize quantifiable observations
657
- - Maintain neutral, constructive tone
658
- - Limit report to 1-2 pages equivalent
659
  """
660
-
661
- # Generate the report
662
  response = gemini_model.generate_content(prompt)
663
- return response.text
664
-
665
  except Exception as e:
666
  logger.error(f"Report generation failed: {str(e)}")
667
  return f"Error generating report: {str(e)}"
668
 
669
- def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
670
  try:
671
- doc = SimpleDocTemplate(output_path, pagesize=letter)
 
 
672
  styles = getSampleStyleSheet()
673
-
674
- # Define enhanced custom styles
675
- h1 = ParagraphStyle(
676
- name='Heading1',
677
- parent=styles['h1'],
678
- fontSize=18,
679
- spaceAfter=12,
680
- alignment=TA_CENTER,
681
- textColor=colors.HexColor('#2E5984'),
682
- fontName='Helvetica-Bold'
683
- )
684
 
685
- h2 = ParagraphStyle(
686
- name='Heading2',
687
- parent=styles['h2'],
688
- fontSize=14,
689
- spaceBefore=14,
690
- spaceAfter=8,
691
- textColor=colors.HexColor('#1E3F66'),
692
- fontName='Helvetica-Bold'
693
- )
694
-
695
- h3 = ParagraphStyle(
696
- name='Heading3',
697
- parent=styles['h3'],
698
- fontSize=12,
699
- spaceBefore=10,
700
- spaceAfter=6,
701
- textColor=colors.HexColor('#0066CC'),
702
- fontName='Helvetica-Bold'
703
- )
704
-
705
- body_text = ParagraphStyle(
706
- name='BodyText',
707
- parent=styles['Normal'],
708
- fontSize=10,
709
- leading=14,
710
- spaceAfter=6,
711
- textColor=colors.HexColor('#333333')
712
- )
713
-
714
- bullet_style = ParagraphStyle(
715
- name='Bullet',
716
- parent=styles['Normal'],
717
- fontSize=10,
718
- leading=14,
719
- leftIndent=18,
720
- bulletIndent=9,
721
- textColor=colors.HexColor('#444444'),
722
- bulletFontName='Helvetica-Bold'
723
- )
724
-
725
  story = []
726
 
727
- # --- Title Section ---
728
- title_table = Table([
729
- [Paragraph("<b>EvalBot Interview Analysis Report</b>", h1)]
730
- ], colWidths=[doc.width])
731
-
732
- title_table.setStyle(TableStyle([
733
- ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#F0F8FF')),
734
- ('BOX', (0,0), (-1,-1), 1, colors.HexColor('#B0C4DE')),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
736
- ('ALIGN', (0,0), (-1,-1), 'CENTER'),
737
- ('BOTTOMPADDING', (0,0), (-1,-1), 15),
738
- ]))
739
-
740
- story.append(title_table)
741
- story.append(Spacer(1, 0.2 * inch))
742
-
743
- # Date and basic info
744
- info_table = Table([
745
- ["Candidate ID:", analysis_data.get('candidate_id', 'N/A')],
746
- ["Interview Date:", time.strftime('%Y-%m-%d')],
747
- ["Duration:", f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
748
- ["Speaker Turns:", analysis_data['text_analysis']['speaker_turns']]
749
- ], colWidths=[doc.width/3, doc.width*2/3])
750
-
751
- info_table.setStyle(TableStyle([
752
- ('FONTNAME', (0,0), (-1,-1), 'Helvetica'),
753
- ('FONTSIZE', (0,0), (-1,-1), 10),
754
- ('BOTTOMPADDING', (0,0), (-1,-1), 6),
755
- ('LEFTPADDING', (0,0), (-1,-1), 4),
756
- ('VALIGN', (0,0), (-1,-1), 'TOP'),
757
  ]))
 
 
 
 
 
 
 
758
 
759
- story.append(info_table)
760
- story.append(Spacer(1, 0.3 * inch))
761
-
762
- # --- Acceptance Probability ---
763
- acceptance_prob = analysis_data.get('acceptance_probability', None)
764
- if acceptance_prob is not None:
765
- # Determine color based on probability
766
- if acceptance_prob >= 80:
767
- prob_color = colors.HexColor('#2E8B57') # Green
768
- prob_text = "Excellent Candidate - Strong Recommendation"
769
- elif acceptance_prob >= 60:
770
- prob_color = colors.HexColor('#FFA500') # Orange
771
- prob_text = "Good Candidate - Recommend With Some Development"
772
- else:
773
- prob_color = colors.HexColor('#CD5C5C') # Red
774
- prob_text = "Needs Significant Development"
775
-
776
- prob_table = Table([
777
- [
778
- Paragraph(
779
- f"<b>Estimated Acceptance Probability:</b>",
780
- ParagraphStyle(
781
- name='ProbLabel',
782
- parent=styles['Normal'],
783
- fontSize=12,
784
- textColor=colors.HexColor('#333333')
785
- )
786
- ),
787
- Paragraph(
788
- f"<b>{acceptance_prob:.2f}%</b>",
789
- ParagraphStyle(
790
- name='ProbValue',
791
- parent=styles['Normal'],
792
- fontSize=14,
793
- textColor=prob_color,
794
- fontName='Helvetica-Bold'
795
- )
796
- )
797
- ],
798
- [
799
- "",
800
- Paragraph(
801
- prob_text,
802
- ParagraphStyle(
803
- name='ProbText',
804
- parent=styles['Normal'],
805
- fontSize=10,
806
- textColor=prob_color
807
- )
808
- )
809
- ]
810
- ], colWidths=[doc.width/2, doc.width/2])
811
-
812
- prob_table.setStyle(TableStyle([
813
- ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
814
- ('BOTTOMPADDING', (0,0), (-1,0), 8),
815
- ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#F5F5F5')),
816
- ('BOX', (0,0), (-1,-1), 0.5, colors.HexColor('#E0E0E0')),
817
- ('LEFTPADDING', (0,0), (-1,-1), 8),
818
- ('RIGHTPADDING', (0,0), (-1,-1), 8),
819
- ]))
820
-
821
- story.append(Paragraph("Candidate Evaluation Summary", h2))
822
- story.append(Spacer(1, 0.1 * inch))
823
- story.append(prob_table)
824
- story.append(Spacer(1, 0.3 * inch))
825
-
826
- # --- Voice Analysis ---
827
- story.append(Paragraph("2. Voice Analysis", h2))
828
  voice_analysis = analysis_data.get('voice_analysis', {})
829
-
830
  if voice_analysis and 'error' not in voice_analysis:
831
- # Create a more professional table
832
  table_data = [
833
- [
834
- Paragraph("<b>Metric</b>", body_text),
835
- Paragraph("<b>Value</b>", body_text),
836
- Paragraph("<b>Interpretation</b>", body_text)
837
- ]
838
- ]
839
-
840
- # Add metrics with conditional formatting
841
- metrics = [
842
- ('Speaking Rate', f"{voice_analysis['speaking_rate']:.2f} words/sec",
843
- 'Optimal range: 2.5-3.0 words/sec', voice_analysis['speaking_rate']),
844
- ('Filler Words', f"{voice_analysis['filler_ratio'] * 100:.1f}%",
845
- 'Ideal: <3% of total words', voice_analysis['filler_ratio']),
846
- ('Repetition Score', f"{voice_analysis['repetition_score']:.3f}",
847
- 'Lower is better (0-0.1 optimal)', voice_analysis['repetition_score']),
848
- ('Anxiety Level', voice_analysis['interpretation']['anxiety_level'].upper(),
849
- f"Score: {voice_analysis['composite_scores']['anxiety']:.3f}",
850
- voice_analysis['composite_scores']['anxiety']),
851
- ('Confidence Level', voice_analysis['interpretation']['confidence_level'].upper(),
852
- f"Score: {voice_analysis['composite_scores']['confidence']:.3f}",
853
- voice_analysis['composite_scores']['confidence']),
854
- ('Fluency', voice_analysis['interpretation']['fluency_level'].upper(),
855
- 'Overall speech flow', 0)
856
  ]
857
-
858
- for metric in metrics:
859
- row = []
860
- row.append(Paragraph(metric[0], body_text))
861
-
862
- # Apply conditional formatting
863
- if metric[0] == 'Filler Words' and metric[3] > 0.03:
864
- row.append(Paragraph(metric[1], ParagraphStyle(
865
- name='AlertText', parent=body_text, textColor=colors.red
866
- )))
867
- elif metric[0] == 'Anxiety Level' and metric[3] > 0.15:
868
- row.append(Paragraph(metric[1], ParagraphStyle(
869
- name='AlertText', parent=body_text, textColor=colors.red
870
- )))
871
- else:
872
- row.append(Paragraph(metric[1], body_text))
873
-
874
- row.append(Paragraph(metric[2], body_text))
875
- table_data.append(row)
876
-
877
- voice_table = Table(table_data, colWidths=[doc.width*0.3, doc.width*0.25, doc.width*0.45])
878
-
879
- voice_table.setStyle(TableStyle([
880
- ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#5B9BD5')),
881
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
882
- ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
883
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
884
- ('VALIGN', (0,0), (-1,-1), 'TOP'),
885
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#D3D3D3')),
886
- ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F9F9F9')),
887
- ('LEFTPADDING', (0,0), (-1,-1), 4),
888
- ('RIGHTPADDING', (0,0), (-1,-1), 4),
889
- ('TOPPADDING', (0,0), (-1,-1), 4),
890
- ('BOTTOMPADDING', (0,0), (-1,-1), 4),
891
  ]))
892
-
893
- story.append(voice_table)
894
- story.append(Spacer(1, 0.3 * inch))
895
-
896
- # Add interpretation section
897
- story.append(Paragraph("Key Observations:", h3))
898
-
899
- observations = [
900
- f"The candidate demonstrated {voice_analysis['interpretation']['fluency_level'].lower()} speech fluency overall.",
901
- f"Confidence levels were rated as {voice_analysis['interpretation']['confidence_level'].lower()} (score: {voice_analysis['composite_scores']['confidence']:.3f}).",
902
- f"Anxiety levels were {voice_analysis['interpretation']['anxiety_level'].lower()} (score: {voice_analysis['composite_scores']['anxiety']:.3f}), which may indicate nervousness.",
903
- f"Filler word usage at {voice_analysis['filler_ratio'] * 100:.1f}% {'exceeds' if voice_analysis['filler_ratio'] > 0.03 else 'is within'} the ideal range."
904
- ]
905
-
906
- for obs in observations:
907
- story.append(Paragraph(obs, bullet_style))
908
-
909
- story.append(Spacer(1, 0.3 * inch))
910
-
911
- # --- Content Analysis ---
912
- story.append(Paragraph("3. Content Analysis", h2))
913
-
914
- # Strengths Section
915
- story.append(Paragraph("Key Strengths:", h3))
916
- strengths = [
917
- "Strong academic background in Computational Biology from MIT",
918
- "Demonstrated passion for applying engineering to healthcare challenges",
919
- "Meaningful leadership experience in community programs",
920
- "Honest self-assessment of technical skills with growth mindset"
921
- ]
922
-
923
- for strength in strengths:
924
- story.append(Paragraph(strength, bullet_style))
925
-
926
- story.append(Spacer(1, 0.2 * inch))
927
-
928
- # Development Areas Section
929
- story.append(Paragraph("Areas for Development:", h3))
930
- dev_areas = [
931
- "Quantifying achievements with specific metrics and outcomes",
932
- "Structuring responses more clearly using frameworks like STAR",
933
- "Developing a concrete plan to address technical skill gaps",
934
- "Reducing filler words for more professional communication"
935
- ]
936
-
937
- for area in dev_areas:
938
- story.append(Paragraph(area, bullet_style))
939
-
940
- story.append(Spacer(1, 0.3 * inch))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
941
 
942
- # --- Recommendations ---
943
- story.append(Paragraph("4. Actionable Recommendations", h2))
944
-
945
- # Communication
946
- story.append(Paragraph("Communication Skills:", h3))
947
- comm_recs = [
948
- "Practice speaking at 2.8-3.0 words/sec for optimal clarity",
949
- "Use recording tools to identify and reduce filler words",
950
- "Conduct mock interviews focusing on pacing and fluency"
951
- ]
952
-
953
- for rec in comm_recs:
954
- story.append(Paragraph(rec, bullet_style))
955
-
956
- story.append(Spacer(1, 0.2 * inch))
957
-
958
- # Content
959
- story.append(Paragraph("Content Delivery:", h3))
960
- content_recs = [
961
- "Prepare 3-5 quantified achievement statements",
962
- "Use STAR method (Situation-Task-Action-Result) for responses",
963
- "Develop structured answers for common interview questions"
964
- ]
965
-
966
- for rec in content_recs:
967
- story.append(Paragraph(rec, bullet_style))
968
-
969
- story.append(Spacer(1, 0.2 * inch))
970
-
971
- # Technical
972
- story.append(Paragraph("Technical Preparation:", h3))
973
- tech_recs = [
974
- "Complete introductory Python certification (e.g., MITx 6.00.1x)",
975
- "Build small genomics data analysis project for portfolio",
976
- "Research company's technical stack and prepare relevant examples"
977
- ]
978
-
979
- for rec in tech_recs:
980
- story.append(Paragraph(rec, bullet_style))
981
-
982
- story.append(Spacer(1, 0.3 * inch))
983
-
984
- # --- Footer ---
985
- footer = Paragraph(
986
- "Analysis generated by EvalBot AI Interview Assessment System | Confidential",
987
- ParagraphStyle(
988
- name='Footer',
989
- parent=styles['Normal'],
990
- fontSize=8,
991
- textColor=colors.HexColor('#666666'),
992
- alignment=TA_CENTER
993
- )
994
- )
995
-
996
- story.append(Spacer(1, 0.5 * inch))
997
- story.append(footer)
998
 
999
- doc.build(story)
 
1000
  return True
1001
-
1002
  except Exception as e:
1003
- logger.error(f"PDF creation failed: {str(e)}", exc_info=True)
1004
  return False
1005
 
1006
 
 
594
  return float(f"{acceptance_probability * 100:.2f}") # Return as percentage
595
 
596
 
597
+
598
+
599
+
600
  def generate_report(analysis_data: Dict) -> str:
601
  try:
602
  voice = analysis_data.get('voice_analysis', {})
603
  voice_interpretation = generate_voice_interpretation(voice)
604
+ interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
605
+ acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
606
+ acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
607
+ if acceptance_prob >= 80:
608
+ acceptance_line += "HR Verdict: Outstanding candidate, recommended for immediate advancement."
609
+ elif acceptance_prob >= 60:
610
+ acceptance_line += "HR Verdict: Strong candidate, suitable for further evaluation."
611
+ elif acceptance_prob >= 40:
612
+ acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
613
+ else:
614
+ acceptance_line += "HR Verdict: Limited fit, significant improvement required."
 
 
 
 
 
 
 
 
 
 
615
  prompt = f"""
616
+ 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.
 
 
617
  {acceptance_line}
 
618
  **1. Executive Summary**
619
+ - Summarize performance, key metrics, and hiring potential.
620
+ - Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
621
+ - Speaker Turns: {analysis_data['text_analysis']['speaker_turns']}
622
+ - Participants: {', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))}
623
+ **2. Communication and Vocal Dynamics**
624
+ - Evaluate vocal delivery (rate, fluency, confidence).
625
+ - Provide HR insights on workplace alignment.
626
  {voice_interpretation}
627
+ **3. Competency and Content**
628
+ - Assess leadership, problem-solving, communication, adaptability.
629
+ - List strengths and growth areas separately with examples.
630
+ - Sample responses:
631
+ {chr(10).join(interviewee_responses)}
632
+ **4. Role Fit and Potential**
633
+ - Analyze cultural fit, role readiness, and growth potential.
634
+ **5. Recommendations**
635
+ - Provide prioritized strategies for growth (communication, technical skills, presence).
636
+ - Suggest next steps for hiring managers (advance, train, assess).
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  """
 
 
638
  response = gemini_model.generate_content(prompt)
639
+ return re.sub(r'[^\x00-\x7F]+', '', response.text) # Sanitize non-ASCII characters
 
640
  except Exception as e:
641
  logger.error(f"Report generation failed: {str(e)}")
642
  return f"Error generating report: {str(e)}"
643
 
644
+ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str) -> bool:
645
  try:
646
+ doc = SimpleDocTemplate(output_path, pagesize=letter,
647
+ rightMargin=0.75*inch, leftMargin=0.75*inch,
648
+ topMargin=1*inch, bottomMargin=1*inch)
649
  styles = getSampleStyleSheet()
650
+ h1 = ParagraphStyle(name='Heading1', fontSize=20, leading=24, spaceAfter=18, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
651
+ h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=16, spaceBefore=12, spaceAfter=8, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
652
+ h3 = ParagraphStyle(name='Heading3', fontSize=10, leading=12, spaceBefore=8, spaceAfter=6, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
653
+ body_text = ParagraphStyle(name='BodyText', fontSize=9, leading=12, spaceAfter=6, fontName='Helvetica', textColor=colors.HexColor('#333333'))
654
+ bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=8, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=9)
 
 
 
 
 
 
655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  story = []
657
 
658
+ def header_footer(canvas, doc):
659
+ canvas.saveState()
660
+ canvas.setFont('Helvetica', 8)
661
+ canvas.setFillColor(colors.HexColor('#666666'))
662
+ canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
663
+ canvas.setStrokeColor(colors.HexColor('#0050BC'))
664
+ canvas.setLineWidth(0.8)
665
+ canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
666
+ canvas.setFont('Helvetica-Bold', 9)
667
+ canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
668
+ canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
669
+ canvas.restoreState()
670
+
671
+ # Title Page
672
+ story.append(Paragraph("Candidate Interview Analysis", h1))
673
+ story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=9, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
674
+ story.append(Spacer(1, 0.4*inch))
675
+ acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
676
+ story.append(Paragraph("Hiring Suitability Snapshot", h2))
677
+ prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
678
+ story.append(Paragraph(f"Suitability Score: <font size=15 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
679
+ ParagraphStyle(name='Prob', fontSize=11, spaceAfter=10, alignment=1, fontName='Helvetica-Bold')))
680
+ if acceptance_prob >= 80:
681
+ story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
682
+ elif acceptance_prob >= 60:
683
+ story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation.", body_text))
684
+ elif acceptance_prob >= 40:
685
+ story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
686
+ else:
687
+ story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
688
+ story.append(Spacer(1, 0.3*inch))
689
+ table_data = [
690
+ ['Metric', 'Value'],
691
+ ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
692
+ ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
693
+ ['Participants', ', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))],
694
+ ]
695
+ table = Table(table_data, colWidths=[2.3*inch, 3.7*inch])
696
+ table.setStyle(TableStyle([
697
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
698
+ ('TEXTCOLOR', (0,0), (-1,0), colors.white),
699
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
700
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
701
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
702
+ ('FONTSIZE', (0,0), (-1,-1), 9),
703
+ ('BOTTOMPADDING', (0,0), (-1,0), 8),
704
+ ('TOPPADDING', (0,0), (-1,0), 8),
705
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
706
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  ]))
708
+ story.append(table)
709
+ story.append(Spacer(1, 0.4*inch))
710
+ story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
711
+ story.append(PageBreak())
712
+
713
+ # Detailed Analysis
714
+ story.append(Paragraph("Detailed Candidate Evaluation", h1))
715
 
716
+ # Communication and Vocal Dynamics
717
+ story.append(Paragraph("1. Communication & Vocal Dynamics", h2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  voice_analysis = analysis_data.get('voice_analysis', {})
 
719
  if voice_analysis and 'error' not in voice_analysis:
 
720
  table_data = [
721
+ ['Metric', 'Value', 'HR Insight'],
722
+ ['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
723
+ ['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
724
+ ['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
725
+ ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
726
+ ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  ]
728
+ table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.2*inch])
729
+ table.setStyle(TableStyle([
730
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
 
732
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
733
+ ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
734
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
735
+ ('FONTSIZE', (0,0), (-1,-1), 9),
736
+ ('BOTTOMPADDING', (0,0), (-1,0), 8),
737
+ ('TOPPADDING', (0,0), (-1,0), 8),
738
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
739
+ ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
740
  ]))
741
+ story.append(table)
742
+ story.append(Spacer(1, 0.2*inch))
743
+ chart_buffer = io.BytesIO()
744
+ generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
745
+ chart_buffer.seek(0)
746
+ img = Image(chart_buffer, width=4.5*inch, height=3*inch)
747
+ img.hAlign = 'CENTER'
748
+ story.append(img)
749
+ else:
750
+ story.append(Paragraph("Vocal analysis unavailable.", body_text))
751
+ story.append(Spacer(1, 0.2*inch))
752
+
753
+ # Parse Gemini Report
754
+ sections = {
755
+ "Executive Summary": [],
756
+ "Communication": [],
757
+ "Competency": {"Strengths": [], "Growth Areas": []},
758
+ "Recommendations": {"Development": [], "Next Steps": []},
759
+ "Role Fit": [],
760
+ }
761
+ current_section = None
762
+ current_subsection = None
763
+ lines = gemini_report_text.split('\n')
764
+ for line in lines:
765
+ line = line.strip()
766
+ if not line: continue
767
+ # Simplified regex to avoid parenthesis issues
768
+ if line.startswith('**') and line.endswith('**'):
769
+ section_title = line.strip('**').strip()
770
+ if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
771
+ section_title = section_title[2:].strip()
772
+ if 'Executive Summary' in section_title:
773
+ current_section = 'Executive Summary'
774
+ current_subsection = None
775
+ elif 'Communication' in section_title:
776
+ current_section = 'Communication'
777
+ current_subsection = None
778
+ elif 'Competency' in section_title:
779
+ current_section = 'Competency'
780
+ current_subsection = None
781
+ elif 'Role Fit' in section_title:
782
+ current_section = 'Role Fit'
783
+ current_subsection = None
784
+ elif 'Recommendations' in section_title:
785
+ current_section = 'Recommendations'
786
+ current_subsection = None
787
+ elif line.startswith(('-', '*', '•')) and current_section:
788
+ clean_line = line.lstrip('-*• ').strip()
789
+ if not clean_line: continue
790
+ clean_line = re.sub(r'[()]', '', clean_line) # Remove parentheses
791
+ if current_section == 'Competency':
792
+ if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength']):
793
+ current_subsection = 'Strengths'
794
+ elif any(k in clean_line.lower() for k in ['improv', 'grow', 'depth']):
795
+ current_subsection = 'Growth Areas'
796
+ if current_subsection:
797
+ sections[current_section][current_subsection].append(clean_line)
798
+ elif current_section == 'Recommendations':
799
+ if any(k in clean_line.lower() for k in ['commun', 'tech', 'depth', 'pres']):
800
+ current_subsection = 'Development'
801
+ elif any(k in clean_line.lower() for k in ['adv', 'train', 'assess', 'next', 'mentor']):
802
+ current_subsection = 'Next Steps'
803
+ if current_subsection:
804
+ sections[current_section][current_subsection].append(clean_line)
805
+ else:
806
+ sections[current_section].append(clean_line)
807
 
808
+ # Executive Summary
809
+ story.append(Paragraph("2. Executive Summary", h2))
810
+ if sections['Executive Summary']:
811
+ for line in sections['Executive Summary']:
812
+ story.append(Paragraph(line, bullet_style))
813
+ else:
814
+ story.append(Paragraph("No summary provided.", body_text))
815
+ story.append(Spacer(1, 0.2*inch))
816
+
817
+ # Competency and Content
818
+ story.append(Paragraph("3. Competency & Evaluation", h2))
819
+ story.append(Paragraph("Strengths", h3))
820
+ if sections['Competency']['Strengths']:
821
+ for line in sections['Competency']['Strengths']:
822
+ story.append(Paragraph(line, bullet_style))
823
+ else:
824
+ story.append(Paragraph("No strengths identified.", body_text))
825
+ story.append(Spacer(1, 0.1*inch))
826
+ story.append(Paragraph("Growth Areas", h3))
827
+ if sections['Competency']['Growth Areas']:
828
+ for line in sections['Competency']['Growth Areas']:
829
+ story.append(Paragraph(line, bullet_style))
830
+ else:
831
+ story.append(Paragraph("No growth areas identified; maintain current strengths.", body_text))
832
+ story.append(Spacer(1, 0.2*inch))
833
+
834
+ # Role Fit
835
+ story.append(Paragraph("4. Role Fit & Potential", h2))
836
+ if sections['Role Fit']:
837
+ for line in sections['Role Fit']:
838
+ story.append(Paragraph(line, bullet_style))
839
+ else:
840
+ story.append(Paragraph("No fit analysis provided.", body_text))
841
+ story.append(Spacer(1, 0.2*inch))
842
+
843
+ # Recommendations
844
+ story.append(Paragraph("5. Recommendations", h2))
845
+ story.append(Paragraph("Development Priorities", h3))
846
+ if sections['Recommendations']['Development']:
847
+ for line in sections['Recommendations']['Development']:
848
+ story.append(Paragraph(line, bullet_style))
849
+ else:
850
+ story.append(Paragraph("No development priorities specified.", body_text))
851
+ story.append(Spacer(1, 0.1*inch))
852
+ story.append(Paragraph("Next Steps", h3))
853
+ if sections['Recommendations']['Next Steps']:
854
+ for line in sections['Recommendations']['Next Steps']:
855
+ story.append(Paragraph(line, bullet_style))
856
+ else:
857
+ story.append(Paragraph("No next steps provided.", body_text))
858
+ story.append(Spacer(1, 0.2*inch))
 
 
 
 
 
859
 
860
+ doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
861
+ logger.info(f"PDF report successfully generated at {output_path}")
862
  return True
 
863
  except Exception as e:
864
+ logger.error(f"PDF generation failed: {str(e)}", exc_info=True)
865
  return False
866
 
867