Update process_interview.py
Browse files- process_interview.py +351 -138
process_interview.py
CHANGED
|
@@ -198,9 +198,9 @@ def transcribe(audio_path: str) -> Dict:
|
|
| 198 |
elif result['status'] == 'error':
|
| 199 |
raise Exception(result['error'])
|
| 200 |
time.sleep(5)
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
|
| 205 |
def process_utterance(utterance: Dict, full_audio: AudioSegment, wav_file: str) -> Dict:
|
| 206 |
try:
|
|
@@ -279,7 +279,7 @@ def train_role_classifier(utterances: List[Dict]):
|
|
| 279 |
sum(1 for token in doc if token.pos_ == 'NOUN')
|
| 280 |
])
|
| 281 |
features.append(feat)
|
| 282 |
-
labels.append(0 if i % 2 == 0 else 1)
|
| 283 |
scaler = StandardScaler()
|
| 284 |
X = scaler.fit_transform(features)
|
| 285 |
clf = RandomForestClassifier(
|
|
@@ -402,7 +402,12 @@ def generate_voice_interpretation(analysis: Dict) -> str:
|
|
| 402 |
"- High filler word usage undermines perceived credibility.",
|
| 403 |
"- Elevated anxiety suggests pressure; training can improve resilience.",
|
| 404 |
"- Strong confidence supports leadership presence.",
|
| 405 |
-
"- Fluent speech enhances engagement in team settings."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
]
|
| 407 |
return "\n".join(interpretation_lines)
|
| 408 |
except Exception as e:
|
|
@@ -458,10 +463,18 @@ def generate_report(analysis_data: Dict) -> str:
|
|
| 458 |
try:
|
| 459 |
voice = analysis_data.get('voice_analysis', {})
|
| 460 |
voice_interpretation = generate_voice_interpretation(voice)
|
| 461 |
-
interviewee_responses = [u['text'] for u in analysis_data['transcript'] if u['role'] == 'Interviewee']
|
| 462 |
if not interviewee_responses:
|
| 463 |
logger.warning("No interviewee responses found for report generation")
|
| 464 |
-
return f"""**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
- Insufficient interviewee content to generate a summary.
|
| 466 |
- Interview duration suggests limited engagement.
|
| 467 |
|
|
@@ -488,9 +501,9 @@ def generate_report(analysis_data: Dict) -> str:
|
|
| 488 |
acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
|
| 489 |
else:
|
| 490 |
acceptance_line += "HR Verdict: Limited fit, significant improvement required."
|
| 491 |
-
transcript_text = "\n".join([f"- {u['speaker']}: {u['text']}" for u in analysis_data['transcript']])
|
| 492 |
prompt = f"""
|
| 493 |
-
You are EvalBot, a senior HR consultant delivering a
|
| 494 |
|
| 495 |
**Input Data**
|
| 496 |
- Suitability Score: {acceptance_prob:.2f}%
|
|
@@ -500,11 +513,17 @@ You are EvalBot, a senior HR consultant delivering a professional interview anal
|
|
| 500 |
- Voice Analysis:
|
| 501 |
{voice_interpretation}
|
| 502 |
- Transcript Sample:
|
| 503 |
-
{transcript_text
|
| 504 |
|
| 505 |
**Report Structure**
|
| 506 |
{acceptance_line}
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
**1. Executive Summary**
|
| 509 |
- Provide a narrative overview of the candidate’s performance, focusing on key strengths and role fit.
|
| 510 |
- Highlight communication style and engagement based on voice analysis and transcript.
|
|
@@ -529,11 +548,19 @@ You are EvalBot, a senior HR consultant delivering a professional interview anal
|
|
| 529 |
"""
|
| 530 |
response = gemini_model.generate_content(prompt)
|
| 531 |
report_text = re.sub(r'[^\x00-\x7F]+|[()]+', '', response.text)
|
| 532 |
-
logger.info(f"Generated Gemini report: {report_text[:500]}...")
|
| 533 |
return report_text
|
| 534 |
except Exception as e:
|
| 535 |
logger.error(f"Report generation failed: {str(e)}", exc_info=True)
|
| 536 |
-
return f"""**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
- Report generation failed due to processing error.
|
| 538 |
|
| 539 |
**2. Communication and Vocal Dynamics**
|
|
@@ -550,7 +577,7 @@ You are EvalBot, a senior HR consultant delivering a professional interview anal
|
|
| 550 |
- Development: Investigate processing error.
|
| 551 |
- Next Steps: Retry analysis with corrected audio."""
|
| 552 |
|
| 553 |
-
def
|
| 554 |
try:
|
| 555 |
doc = SimpleDocTemplate(output_path, pagesize=letter,
|
| 556 |
rightMargin=0.75*inch, leftMargin=0.75*inch,
|
|
@@ -561,82 +588,119 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 561 |
h3 = ParagraphStyle(name='Heading3', fontSize=9, leading=11, spaceBefore=6, spaceAfter=4, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
|
| 562 |
body_text = ParagraphStyle(name='BodyText', fontSize=8, leading=10, spaceAfter=4, fontName='Helvetica', textColor=colors.HexColor('#333333'))
|
| 563 |
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=16, bulletIndent=6, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=8)
|
| 564 |
-
|
| 565 |
story = []
|
| 566 |
|
| 567 |
def header_footer(canvas, doc):
|
| 568 |
canvas.saveState()
|
| 569 |
canvas.setFont('Helvetica', 7)
|
| 570 |
canvas.setFillColor(colors.HexColor('#666666'))
|
| 571 |
-
canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot
|
| 572 |
canvas.setStrokeColor(colors.HexColor('#0050BC'))
|
| 573 |
canvas.setLineWidth(0.5)
|
| 574 |
canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
|
| 575 |
canvas.setFont('Helvetica-Bold', 8)
|
| 576 |
-
canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "
|
| 577 |
canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
|
| 578 |
canvas.restoreState()
|
| 579 |
|
| 580 |
# Title Page
|
| 581 |
-
story.append(Paragraph("
|
| 582 |
story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
|
| 583 |
story.append(Spacer(1, 0.3*inch))
|
| 584 |
-
|
| 585 |
-
story.append(Paragraph("Hiring Suitability Snapshot", h2))
|
| 586 |
-
prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
|
| 587 |
-
story.append(Paragraph(f"Suitability Score: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
|
| 588 |
-
ParagraphStyle(name='Prob', fontSize=10, spaceAfter=8, alignment=1, fontName='Helvetica-Bold')))
|
| 589 |
-
if acceptance_prob >= 80:
|
| 590 |
-
story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
|
| 591 |
-
elif acceptance_prob >= 60:
|
| 592 |
-
story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation.", body_text))
|
| 593 |
-
elif acceptance_prob >= 40:
|
| 594 |
-
story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
|
| 595 |
-
else:
|
| 596 |
-
story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
|
| 597 |
story.append(Spacer(1, 0.2*inch))
|
| 598 |
-
|
| 599 |
-
participants_str = ', '.join(participants)
|
| 600 |
-
table_data = [
|
| 601 |
-
['Metric', 'Value'],
|
| 602 |
-
['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
|
| 603 |
-
['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
|
| 604 |
-
['Participants', participants_str],
|
| 605 |
-
]
|
| 606 |
-
table = Table(table_data, colWidths=[2.0*inch, 4.0*inch])
|
| 607 |
-
table.setStyle(TableStyle([
|
| 608 |
-
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
|
| 609 |
-
('TEXTCOLOR', (0,0), (-1,0), colors.white),
|
| 610 |
-
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
| 611 |
-
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 612 |
-
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
|
| 613 |
-
('FONTSIZE', (0,0), (-1,-1), 8),
|
| 614 |
-
('BOTTOMPADDING', (0,0), (-1,0), 6),
|
| 615 |
-
('TOPPADDING', (0,0), (-1,0), 6),
|
| 616 |
-
('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
|
| 617 |
-
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
|
| 618 |
-
('LEFTPADDING', (1,3), (1,3), 10),
|
| 619 |
-
('WORDWRAP', (1,3), (1,3), 'CJK'),
|
| 620 |
-
]))
|
| 621 |
-
story.append(table)
|
| 622 |
-
story.append(Spacer(1, 0.3*inch))
|
| 623 |
-
story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
|
| 624 |
story.append(PageBreak())
|
| 625 |
|
| 626 |
-
#
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 632 |
if voice_analysis and 'error' not in voice_analysis:
|
| 633 |
table_data = [
|
| 634 |
-
['Metric', 'Value', '
|
| 635 |
-
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", '
|
| 636 |
-
['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", '
|
| 637 |
-
['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'),
|
| 638 |
-
['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'),
|
| 639 |
-
['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), '
|
| 640 |
]
|
| 641 |
table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
|
| 642 |
table.setStyle(TableStyle([
|
|
@@ -653,6 +717,150 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 653 |
]))
|
| 654 |
story.append(table)
|
| 655 |
story.append(Spacer(1, 0.15*inch))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
chart_buffer = io.BytesIO()
|
| 657 |
generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
|
| 658 |
chart_buffer.seek(0)
|
|
@@ -665,6 +873,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 665 |
|
| 666 |
# Parse Gemini Report
|
| 667 |
sections = {
|
|
|
|
| 668 |
"Executive Summary": [],
|
| 669 |
"Communication": [],
|
| 670 |
"Competency": {"Strengths": [], "Growth Areas": []},
|
|
@@ -678,7 +887,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 678 |
line = line.strip()
|
| 679 |
if not line:
|
| 680 |
continue
|
| 681 |
-
logger.debug(f"Parsing line: {line}")
|
| 682 |
if line.startswith('**') and line.endswith('**'):
|
| 683 |
section_title = line.strip('**').strip()
|
| 684 |
if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
|
|
@@ -686,7 +895,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 686 |
if 'Executive Summary' in section_title:
|
| 687 |
current_section = 'Executive Summary'
|
| 688 |
current_subsection = None
|
| 689 |
-
elif 'Communication
|
| 690 |
current_section = 'Communication'
|
| 691 |
current_subsection = None
|
| 692 |
elif 'Competency' in section_title:
|
|
@@ -695,7 +904,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 695 |
elif 'Role Fit' in section_title:
|
| 696 |
current_section = 'Role Fit'
|
| 697 |
current_subsection = None
|
| 698 |
-
elif 'Recommendations'
|
| 699 |
current_section = 'Recommendations'
|
| 700 |
current_subsection = None
|
| 701 |
logger.debug(f"Set section: {current_section}")
|
|
@@ -704,22 +913,19 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 704 |
if not clean_line:
|
| 705 |
continue
|
| 706 |
clean_line = re.sub(r'[^\w\s.,;:-]', '', clean_line)
|
| 707 |
-
logger.debug(f"Processing bullet: {clean_line}, section: {current_section}, subsection:
|
| 708 |
if current_section in ['Competency', 'Recommendations']:
|
| 709 |
-
# For dictionary sections, append to subsection
|
| 710 |
if current_subsection is None:
|
| 711 |
-
# Set default subsection if unset
|
| 712 |
if current_section == 'Competency':
|
| 713 |
current_subsection = 'Strengths'
|
| 714 |
elif current_section == 'Recommendations':
|
| 715 |
current_subsection = 'Development'
|
| 716 |
-
logger.debug(f"Default
|
| 717 |
if current_subsection:
|
| 718 |
sections[current_section][current_subsection].append(clean_line)
|
| 719 |
else:
|
| 720 |
logger.warning(f"Skipping line due to unset subsection: {clean_line}")
|
| 721 |
else:
|
| 722 |
-
# For list sections, append directly
|
| 723 |
sections[current_section].append(clean_line)
|
| 724 |
elif current_section and line:
|
| 725 |
clean_line = re.sub(r'[^\w\s.,;:-]', '', line)
|
|
@@ -728,72 +934,71 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 728 |
if current_subsection:
|
| 729 |
sections[current_section][current_subsection].append(clean_line)
|
| 730 |
else:
|
| 731 |
-
# Default subsection
|
| 732 |
current_subsection = 'Strengths' if current_section == 'Competency' else 'Development'
|
| 733 |
sections[current_section][current_subsection].append(clean_line)
|
| 734 |
-
logger.debug(f"Default
|
| 735 |
else:
|
| 736 |
sections[current_section].append(clean_line)
|
| 737 |
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
|
| 792 |
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 793 |
-
logger.info(f"PDF report successfully generated at {output_path}")
|
| 794 |
return True
|
| 795 |
except Exception as e:
|
| 796 |
-
logger.error(f"PDF generation failed: {str(e)}\nFull Gemini report text:\n{gemini_report_text}", exc_info=True)
|
| 797 |
return False
|
| 798 |
|
| 799 |
def convert_to_serializable(obj):
|
|
@@ -851,21 +1056,28 @@ def process_interview(audio_url: str) -> Dict:
|
|
| 851 |
analysis_data['acceptance_probability'] = calculate_acceptance_probability(analysis_data)
|
| 852 |
gemini_report_text = generate_report(analysis_data)
|
| 853 |
base_name = str(uuid.uuid4())
|
| 854 |
-
|
|
|
|
| 855 |
json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
|
| 856 |
-
|
|
|
|
| 857 |
with open(json_path, 'w') as f:
|
| 858 |
serializable_data = convert_to_serializable(analysis_data)
|
| 859 |
json.dump(serializable_data, f, indent=2)
|
| 860 |
-
if not
|
| 861 |
-
logger.warning(f"PDF
|
| 862 |
return {
|
| 863 |
-
'
|
|
|
|
| 864 |
'json_path': json_path,
|
| 865 |
-
'error': 'PDF
|
| 866 |
}
|
| 867 |
logger.info(f"Processing completed for {audio_url}")
|
| 868 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
except Exception as e:
|
| 870 |
logger.error(f"Processing failed for {audio_url}: {str(e)}", exc_info=True)
|
| 871 |
base_name = str(uuid.uuid4())
|
|
@@ -873,7 +1085,8 @@ def process_interview(audio_url: str) -> Dict:
|
|
| 873 |
with open(json_path, 'w') as f:
|
| 874 |
json.dump({'error': str(e)}, f, indent=2)
|
| 875 |
return {
|
| 876 |
-
'
|
|
|
|
| 877 |
'json_path': json_path,
|
| 878 |
'error': str(e)
|
| 879 |
}
|
|
|
|
| 198 |
elif result['status'] == 'error':
|
| 199 |
raise Exception(result['error'])
|
| 200 |
time.sleep(5)
|
| 201 |
+
except Exception as e:
|
| 202 |
+
logger.error(f"Transcription failed: {str(e)}")
|
| 203 |
+
raise
|
| 204 |
|
| 205 |
def process_utterance(utterance: Dict, full_audio: AudioSegment, wav_file: str) -> Dict:
|
| 206 |
try:
|
|
|
|
| 279 |
sum(1 for token in doc if token.pos_ == 'NOUN')
|
| 280 |
])
|
| 281 |
features.append(feat)
|
| 282 |
+
labels.append(0 if i % 2 == 0 else 1)
|
| 283 |
scaler = StandardScaler()
|
| 284 |
X = scaler.fit_transform(features)
|
| 285 |
clf = RandomForestClassifier(
|
|
|
|
| 402 |
"- High filler word usage undermines perceived credibility.",
|
| 403 |
"- Elevated anxiety suggests pressure; training can improve resilience.",
|
| 404 |
"- Strong confidence supports leadership presence.",
|
| 405 |
+
"- Fluent speech enhances engagement in team settings.",
|
| 406 |
+
"",
|
| 407 |
+
"Candidate Tips:",
|
| 408 |
+
"- Practice pacing to maintain a steady speaking rate (2.0-3.0 words/sec).",
|
| 409 |
+
"- Reduce filler words (e.g., 'um', 'like') through mock interviews.",
|
| 410 |
+
"- Use breathing exercises to lower anxiety and stabilize pitch."
|
| 411 |
]
|
| 412 |
return "\n".join(interpretation_lines)
|
| 413 |
except Exception as e:
|
|
|
|
| 463 |
try:
|
| 464 |
voice = analysis_data.get('voice_analysis', {})
|
| 465 |
voice_interpretation = generate_voice_interpretation(voice)
|
| 466 |
+
interviewee_responses = [u['text'] for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
|
| 467 |
if not interviewee_responses:
|
| 468 |
logger.warning("No interviewee responses found for report generation")
|
| 469 |
+
return f"""**Suitability Score: 50.00%**
|
| 470 |
+
HR Verdict: Insufficient data for evaluation.
|
| 471 |
+
|
| 472 |
+
**User Feedback**
|
| 473 |
+
- Insufficient content to provide feedback.
|
| 474 |
+
- Practice answering common interview questions to improve engagement.
|
| 475 |
+
|
| 476 |
+
**HR Evaluation**
|
| 477 |
+
**1. Executive Summary**
|
| 478 |
- Insufficient interviewee content to generate a summary.
|
| 479 |
- Interview duration suggests limited engagement.
|
| 480 |
|
|
|
|
| 501 |
acceptance_line += "HR Verdict: Moderate potential, needs additional assessment."
|
| 502 |
else:
|
| 503 |
acceptance_line += "HR Verdict: Limited fit, significant improvement required."
|
| 504 |
+
transcript_text = "\n".join([f"- {u['speaker']}: {u['text']}" for u in analysis_data['transcript']][:10])
|
| 505 |
prompt = f"""
|
| 506 |
+
You are EvalBot, a senior HR consultant delivering a dual-purpose interview analysis report. Generate two sections: one for the candidate (**User Feedback**) with actionable self-improvement tips, and one for HR (**HR Evaluation**) with professional analysis. Use clear headings with '**', bullet points ('-'), complete sentences, and formal language for HR, friendly language for User Feedback. Avoid redundancy, vague terms, and special characters that could break formatting. Ensure each section is unique, actionable, and contains at least 2-3 bullet points.
|
| 507 |
|
| 508 |
**Input Data**
|
| 509 |
- Suitability Score: {acceptance_prob:.2f}%
|
|
|
|
| 513 |
- Voice Analysis:
|
| 514 |
{voice_interpretation}
|
| 515 |
- Transcript Sample:
|
| 516 |
+
{transcript_text}
|
| 517 |
|
| 518 |
**Report Structure**
|
| 519 |
{acceptance_line}
|
| 520 |
|
| 521 |
+
**User Feedback**
|
| 522 |
+
- Provide friendly, actionable tips for the candidate to improve communication, confidence, and content.
|
| 523 |
+
- Focus on practical steps (e.g., practice pacing, reduce fillers).
|
| 524 |
+
- Keep tone motivational and concise.
|
| 525 |
+
|
| 526 |
+
**HR Evaluation**
|
| 527 |
**1. Executive Summary**
|
| 528 |
- Provide a narrative overview of the candidate’s performance, focusing on key strengths and role fit.
|
| 529 |
- Highlight communication style and engagement based on voice analysis and transcript.
|
|
|
|
| 548 |
"""
|
| 549 |
response = gemini_model.generate_content(prompt)
|
| 550 |
report_text = re.sub(r'[^\x00-\x7F]+|[()]+', '', response.text)
|
| 551 |
+
logger.info(f"Generated Gemini report: {report_text[:500]}...")
|
| 552 |
return report_text
|
| 553 |
except Exception as e:
|
| 554 |
logger.error(f"Report generation failed: {str(e)}", exc_info=True)
|
| 555 |
+
return f"""**Suitability Score: 50.00%**
|
| 556 |
+
HR Verdict: Report generation failed.
|
| 557 |
+
|
| 558 |
+
**User Feedback**
|
| 559 |
+
- Unable to provide feedback due to processing error.
|
| 560 |
+
- Practice answering questions clearly to improve future interviews.
|
| 561 |
+
|
| 562 |
+
**HR Evaluation**
|
| 563 |
+
**1. Executive Summary**
|
| 564 |
- Report generation failed due to processing error.
|
| 565 |
|
| 566 |
**2. Communication and Vocal Dynamics**
|
|
|
|
| 577 |
- Development: Investigate processing error.
|
| 578 |
- Next Steps: Retry analysis with corrected audio."""
|
| 579 |
|
| 580 |
+
def create_user_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str) -> bool:
|
| 581 |
try:
|
| 582 |
doc = SimpleDocTemplate(output_path, pagesize=letter,
|
| 583 |
rightMargin=0.75*inch, leftMargin=0.75*inch,
|
|
|
|
| 588 |
h3 = ParagraphStyle(name='Heading3', fontSize=9, leading=11, spaceBefore=6, spaceAfter=4, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
|
| 589 |
body_text = ParagraphStyle(name='BodyText', fontSize=8, leading=10, spaceAfter=4, fontName='Helvetica', textColor=colors.HexColor('#333333'))
|
| 590 |
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=16, bulletIndent=6, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=8)
|
| 591 |
+
|
| 592 |
story = []
|
| 593 |
|
| 594 |
def header_footer(canvas, doc):
|
| 595 |
canvas.saveState()
|
| 596 |
canvas.setFont('Helvetica', 7)
|
| 597 |
canvas.setFillColor(colors.HexColor('#666666'))
|
| 598 |
+
canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot Personal Feedback Report")
|
| 599 |
canvas.setStrokeColor(colors.HexColor('#0050BC'))
|
| 600 |
canvas.setLineWidth(0.5)
|
| 601 |
canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
|
| 602 |
canvas.setFont('Helvetica-Bold', 8)
|
| 603 |
+
canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Personal Interview Feedback")
|
| 604 |
canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
|
| 605 |
canvas.restoreState()
|
| 606 |
|
| 607 |
# Title Page
|
| 608 |
+
story.append(Paragraph("Your Interview Feedback Report", h1))
|
| 609 |
story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
|
| 610 |
story.append(Spacer(1, 0.3*inch))
|
| 611 |
+
story.append(Paragraph("This report provides personalized tips to help you shine in future interviews.", body_text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
story.append(Spacer(1, 0.2*inch))
|
| 613 |
+
story.append(Paragraph("Prepared by: EvalBot - AI-Powered Interview Coach", body_text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
story.append(PageBreak())
|
| 615 |
|
| 616 |
+
# Parse Gemini Report
|
| 617 |
+
sections = {
|
| 618 |
+
"User Feedback": [],
|
| 619 |
+
"Executive Summary": [],
|
| 620 |
+
"Communication": [],
|
| 621 |
+
"Competency": {"Strengths": [], "Growth Areas": []},
|
| 622 |
+
"Recommendations": {"Development": [], "Next Steps": []},
|
| 623 |
+
"Role Fit": [],
|
| 624 |
+
}
|
| 625 |
+
current_section = None
|
| 626 |
+
current_subsection = None
|
| 627 |
+
lines = gemini_report_text.split('\n')
|
| 628 |
+
for line in lines:
|
| 629 |
+
line = line.strip()
|
| 630 |
+
if not line:
|
| 631 |
+
continue
|
| 632 |
+
logger.debug(f"Parsing line: {line}")
|
| 633 |
+
if line.startswith('**') and line.endswith('**'):
|
| 634 |
+
section_title = line.strip('**').strip()
|
| 635 |
+
if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
|
| 636 |
+
section_title = section_title[2:].strip()
|
| 637 |
+
if 'User Feedback' in section_title:
|
| 638 |
+
current_section = 'User Feedback'
|
| 639 |
+
current_subsection = None
|
| 640 |
+
elif 'Executive Summary' in section_title:
|
| 641 |
+
current_section = 'Executive Summary'
|
| 642 |
+
current_subsection = None
|
| 643 |
+
elif 'Communication' in section_title:
|
| 644 |
+
current_section = 'Communication'
|
| 645 |
+
current_subsection = None
|
| 646 |
+
elif 'Competency' in section_title:
|
| 647 |
+
current_section = 'Competency'
|
| 648 |
+
current_subsection = None
|
| 649 |
+
elif 'Role Fit' in section_title:
|
| 650 |
+
current_section = 'Role Fit'
|
| 651 |
+
current_subsection = None
|
| 652 |
+
elif 'Recommendations' in section_title:
|
| 653 |
+
current_section = 'Recommendations'
|
| 654 |
+
current_subsection = None
|
| 655 |
+
logger.debug(f"Set section: {current_section}")
|
| 656 |
+
elif line.startswith('-') and current_section:
|
| 657 |
+
clean_line = line.lstrip('-').strip()
|
| 658 |
+
if not clean_line:
|
| 659 |
+
continue
|
| 660 |
+
clean_line = re.sub(r'[^\w\s.,;:-]', '', clean_line)
|
| 661 |
+
logger.debug(f"Processing bullet: {clean_line}, section: {current_section}, subsection: {current_subsection}")
|
| 662 |
+
if current_section in ['Competency', 'Recommendations']:
|
| 663 |
+
if current_subsection is None:
|
| 664 |
+
if current_section == 'Competency':
|
| 665 |
+
current_subsection = 'Strengths'
|
| 666 |
+
elif current_section == 'Recommendations':
|
| 667 |
+
current_subsection = 'Development'
|
| 668 |
+
logger.debug(f"Default subsection set to: {current_subsection}")
|
| 669 |
+
if current_subsection:
|
| 670 |
+
sections[current_section][current_subsection].append(clean_line)
|
| 671 |
+
else:
|
| 672 |
+
logger.warning(f"Skipping line due to unset subsection: {clean_line}")
|
| 673 |
+
else:
|
| 674 |
+
sections[current_section].append(clean_line)
|
| 675 |
+
elif current_section and line:
|
| 676 |
+
clean_line = re.sub(r'[^\w\s.,;:-]', '', line)
|
| 677 |
+
logger.debug(f"Processing non-bullet: {clean_line}, section: {current_section}, subsection: {current_subsection}")
|
| 678 |
+
if current_section in ['Competency', 'Recommendations']:
|
| 679 |
+
if current_subsection:
|
| 680 |
+
sections[current_section][current_subsection].append(clean_line)
|
| 681 |
+
else:
|
| 682 |
+
current_subsection = 'Strengths' if current_section == 'Competency' else 'Development'
|
| 683 |
+
sections[current_section][current_subsection].append(clean_line)
|
| 684 |
+
logger.debug(f"Default subsection for non-bullet set to: {current_subsection}")
|
| 685 |
+
else:
|
| 686 |
+
sections[current_section].append(clean_line)
|
| 687 |
+
|
| 688 |
+
# Introduction
|
| 689 |
+
story.append(Paragraph("How to Use This Report", h2))
|
| 690 |
+
story.append(Paragraph("This report is designed to help you improve your interview skills. Review the feedback below and try the suggested tips to boost your confidence and clarity.", body_text))
|
| 691 |
+
story.append(Spacer(1, 0.15*inch))
|
| 692 |
+
|
| 693 |
+
# Your Communication Style
|
| 694 |
+
story.append(Paragraph("Your Communication Style", h2))
|
| 695 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 696 |
if voice_analysis and 'error' not in voice_analysis:
|
| 697 |
table_data = [
|
| 698 |
+
['Metric', 'Value', 'What It Means'],
|
| 699 |
+
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'How fast you speak'],
|
| 700 |
+
['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'Words like "um" or "like"'],
|
| 701 |
+
['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), 'Your stress level'],
|
| 702 |
+
['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), 'Your vocal strength'],
|
| 703 |
+
['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'How smoothly you speak'],
|
| 704 |
]
|
| 705 |
table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
|
| 706 |
table.setStyle(TableStyle([
|
|
|
|
| 717 |
]))
|
| 718 |
story.append(table)
|
| 719 |
story.append(Spacer(1, 0.15*inch))
|
| 720 |
+
story.append(Paragraph("Tips to Improve:", h3))
|
| 721 |
+
for line in sections['Communication'][-3:]: # Use candidate tips from voice_interpretation
|
| 722 |
+
story.append(Paragraph(line, bullet_style))
|
| 723 |
+
else:
|
| 724 |
+
story.append(Paragraph(f"Voice analysis unavailable: {voice_analysis.get('error', 'Unknown error')}", body_text))
|
| 725 |
+
story.append(Spacer(1, 0.15*inch))
|
| 726 |
+
|
| 727 |
+
# Your Responses
|
| 728 |
+
story.append(Paragraph("Your Responses", h2))
|
| 729 |
+
if sections['Competency']['Strengths'] or sections['Competency']['Growth Areas']:
|
| 730 |
+
story.append(Paragraph("Strengths", h3))
|
| 731 |
+
for line in sections['Competency']['Strengths'][:3]:
|
| 732 |
+
story.append(Paragraph(line, bullet_style))
|
| 733 |
+
story.append(Spacer(1, 0.1*inch))
|
| 734 |
+
story.append(Paragraph("Areas to Work On", h3))
|
| 735 |
+
for line in sections['Competency']['Growth Areas'][:3]:
|
| 736 |
+
story.append(Paragraph(line, bullet_style))
|
| 737 |
+
else:
|
| 738 |
+
story.append(Paragraph("You showed effort in responding; try to provide more specific examples.", bullet_style))
|
| 739 |
+
story.append(Paragraph("Practice structuring answers using the STAR method (Situation, Task, Action, Result).", bullet_style))
|
| 740 |
+
story.append(Spacer(1, 0.15*inch))
|
| 741 |
+
|
| 742 |
+
# Action Plan
|
| 743 |
+
story.append(Paragraph("Your Action Plan", h2))
|
| 744 |
+
if sections['User Feedback']:
|
| 745 |
+
for line in sections['User Feedback']:
|
| 746 |
+
story.append(Paragraph(line, bullet_style))
|
| 747 |
+
else:
|
| 748 |
+
story.append(Paragraph("Practice mock interviews to build confidence.", bullet_style))
|
| 749 |
+
story.append(Paragraph("Record yourself to identify and reduce filler words.", bullet_style))
|
| 750 |
+
story.append(Paragraph("Join a public speaking group to improve fluency.", bullet_style))
|
| 751 |
+
story.append(Spacer(1, 0.15*inch))
|
| 752 |
+
story.append(Paragraph("Keep practicing, and you'll see improvement in your next interview!", body_text))
|
| 753 |
+
|
| 754 |
+
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 755 |
+
logger.info(f"User PDF report successfully generated at {output_path}")
|
| 756 |
+
return True
|
| 757 |
+
except Exception as e:
|
| 758 |
+
logger.error(f"User PDF generation failed: {str(e)}\nFull Gemini report text:\n{gemini_report_text}", exc_info=True)
|
| 759 |
+
return False
|
| 760 |
+
|
| 761 |
+
def create_company_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str) -> bool:
|
| 762 |
+
try:
|
| 763 |
+
doc = SimpleDocTemplate(output_path, pagesize=letter,
|
| 764 |
+
rightMargin=0.75*inch, leftMargin=0.75*inch,
|
| 765 |
+
topMargin=1*inch, bottomMargin=1*inch)
|
| 766 |
+
styles = getSampleStyleSheet()
|
| 767 |
+
h1 = ParagraphStyle(name='Heading1', fontSize=18, leading=22, spaceAfter=16, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
|
| 768 |
+
h2 = ParagraphStyle(name='Heading2', fontSize=13, leading=15, spaceBefore=10, spaceAfter=6, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
|
| 769 |
+
h3 = ParagraphStyle(name='Heading3', fontSize=9, leading=11, spaceBefore=6, spaceAfter=4, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
|
| 770 |
+
body_text = ParagraphStyle(name='BodyText', fontSize=8, leading=10, spaceAfter=4, fontName='Helvetica', textColor=colors.HexColor('#333333'))
|
| 771 |
+
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=16, bulletIndent=6, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=8)
|
| 772 |
+
|
| 773 |
+
story = []
|
| 774 |
+
|
| 775 |
+
def header_footer(canvas, doc):
|
| 776 |
+
canvas.saveState()
|
| 777 |
+
canvas.setFont('Helvetica', 7)
|
| 778 |
+
canvas.setFillColor(colors.HexColor('#666666'))
|
| 779 |
+
canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
|
| 780 |
+
canvas.setStrokeColor(colors.HexColor('#0050BC'))
|
| 781 |
+
canvas.setLineWidth(0.5)
|
| 782 |
+
canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
|
| 783 |
+
canvas.setFont('Helvetica-Bold', 8)
|
| 784 |
+
canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
|
| 785 |
+
canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
|
| 786 |
+
canvas.restoreState()
|
| 787 |
+
|
| 788 |
+
# Title Page
|
| 789 |
+
story.append(Paragraph("Candidate Interview Analysis", h1))
|
| 790 |
+
story.append(Paragraph(f"Generated {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
|
| 791 |
+
story.append(Spacer(1, 0.3*inch))
|
| 792 |
+
acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
|
| 793 |
+
story.append(Paragraph("Hiring Suitability Snapshot", h2))
|
| 794 |
+
prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F')
|
| 795 |
+
story.append(Paragraph(f"Suitability Score: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
|
| 796 |
+
ParagraphStyle(name='Prob', fontSize=10, spaceAfter=8, alignment=1, fontName='Helvetica-Bold')))
|
| 797 |
+
if acceptance_prob >= 80:
|
| 798 |
+
story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
|
| 799 |
+
elif acceptance_prob >= 60:
|
| 800 |
+
story.append(Paragraph("<b>HR Verdict:</b> Strong candidate, suitable for further evaluation.", body_text))
|
| 801 |
+
elif acceptance_prob >= 40:
|
| 802 |
+
story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
|
| 803 |
+
else:
|
| 804 |
+
story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
|
| 805 |
+
story.append(Spacer(1, 0.2*inch))
|
| 806 |
+
participants = sorted([p for u in set(u['speaker'] for u in analysis_data['transcript'] if u['speaker'] != 'Unknown'])
|
| 807 |
+
participants_str = ', '.join(participants)
|
| 808 |
+
table_data = [
|
| 809 |
+
['Metric', 'Value'],
|
| 810 |
+
['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
|
| 811 |
+
['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
|
| 812 |
+
['Participants', participants_str],
|
| 813 |
+
]
|
| 814 |
+
table = Table(table_data, colWidths=[2.0*inch, 4.0*inch])
|
| 815 |
+
table.setStyle(TableStyle([
|
| 816 |
+
('Background', (0,0), (-1,0), colors.HexColor('#0050BC')),
|
| 817 |
+
('TEXTCOLOR', (0,0), (-1,0), colors.white),
|
| 818 |
+
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
| 819 |
+
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 820 |
+
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
|
| 821 |
+
('FONTSIZE', (0,0), (-1,-1), 8),
|
| 822 |
+
('BOTTOMPADING', (0,0), (-1,0), 6),
|
| 823 |
+
('TOPPADING', (0,0), (-1,0), 6),
|
| 824 |
+
('Background', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
|
| 825 |
+
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
|
| 826 |
+
('LEFTPAD', (1,3), (1,3), 10),
|
| 827 |
+
('WORDWRAP', (1,3), (1,3), 'CJK'),
|
| 828 |
+
]))
|
| 829 |
+
story.append(table)
|
| 830 |
+
story.append(Spacer(1, 0.3*inch))
|
| 831 |
+
story.append(Paragraph("Prepared by EvalBot - AI-Powered HR Analysis", body_text))
|
| 832 |
+
story.append(PageBreak())
|
| 833 |
+
|
| 834 |
+
# Detailed Analysis
|
| 835 |
+
story.append(Paragraph("Detailed Candidate Evaluation", h1))
|
| 836 |
+
|
| 837 |
+
# Communication and Vocal Dynamics
|
| 838 |
+
story.append(Paragraph("1. Communication & Vocal Dynamics", h2))
|
| 839 |
+
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 840 |
+
if voice_analysis and 'error' not in voice_analysis:
|
| 841 |
+
table_data = [
|
| 842 |
+
['Metric', 'Value', 'HR Insight'],
|
| 843 |
+
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Benchmark: 2.0-3.0 wps; impacts clarity'],
|
| 844 |
+
['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'High usage reduces credibility'],
|
| 845 |
+
['Anxiety', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
|
| 846 |
+
['Confidence', voice_analysis.get('('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('('composite_scores', {}).get('confidence', 0):.3f}"],
|
| 847 |
+
['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
|
| 848 |
+
]
|
| 849 |
+
table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
|
| 850 |
+
table.setStyle(TableStyle([
|
| 851 |
+
('Background', (0,0), (-1,0), colors.HexColor('#0050BC')),
|
| 852 |
+
('TEXTCOLOR', (0,0), (-1,0), colors.white),
|
| 853 |
+
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
| 854 |
+
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 855 |
+
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
|
| 856 |
+
('FONTSIZE', (0,0), (-1,-1), 8),
|
| 857 |
+
('BOTTOMPADING', (0,0), (-1,0), 6),
|
| 858 |
+
('TOPPADING', (0,0), (-1,0), 6),
|
| 859 |
+
('Background', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
|
| 860 |
+
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
|
| 861 |
+
]))
|
| 862 |
+
story.append(table)
|
| 863 |
+
story.append(Spacer(1, 0.15*inch))
|
| 864 |
chart_buffer = io.BytesIO()
|
| 865 |
generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
|
| 866 |
chart_buffer.seek(0)
|
|
|
|
| 873 |
|
| 874 |
# Parse Gemini Report
|
| 875 |
sections = {
|
| 876 |
+
"User Feedback": [],
|
| 877 |
"Executive Summary": [],
|
| 878 |
"Communication": [],
|
| 879 |
"Competency": {"Strengths": [], "Growth Areas": []},
|
|
|
|
| 887 |
line = line.strip()
|
| 888 |
if not line:
|
| 889 |
continue
|
| 890 |
+
logger.debug(f"Parsing line: {line}")
|
| 891 |
if line.startswith('**') and line.endswith('**'):
|
| 892 |
section_title = line.strip('**').strip()
|
| 893 |
if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
|
|
|
|
| 895 |
if 'Executive Summary' in section_title:
|
| 896 |
current_section = 'Executive Summary'
|
| 897 |
current_subsection = None
|
| 898 |
+
elif section_title == 'Communication and Vocal Dynamics':
|
| 899 |
current_section = 'Communication'
|
| 900 |
current_subsection = None
|
| 901 |
elif 'Competency' in section_title:
|
|
|
|
| 904 |
elif 'Role Fit' in section_title:
|
| 905 |
current_section = 'Role Fit'
|
| 906 |
current_subsection = None
|
| 907 |
+
elif section_title == 'Recommendations':
|
| 908 |
current_section = 'Recommendations'
|
| 909 |
current_subsection = None
|
| 910 |
logger.debug(f"Set section: {current_section}")
|
|
|
|
| 913 |
if not clean_line:
|
| 914 |
continue
|
| 915 |
clean_line = re.sub(r'[^\w\s.,;:-]', '', clean_line)
|
| 916 |
+
logger.debug(f"Processing bullet: {clean_line}, section: {current_section}, {subsection: current_subsection}")
|
| 917 |
if current_section in ['Competency', 'Recommendations']:
|
|
|
|
| 918 |
if current_subsection is None:
|
|
|
|
| 919 |
if current_section == 'Competency':
|
| 920 |
current_subsection = 'Strengths'
|
| 921 |
elif current_section == 'Recommendations':
|
| 922 |
current_subsection = 'Development'
|
| 923 |
+
logger.debug(f"Default subsetion set to: {current_subsection}")
|
| 924 |
if current_subsection:
|
| 925 |
sections[current_section][current_subsection].append(clean_line)
|
| 926 |
else:
|
| 927 |
logger.warning(f"Skipping line due to unset subsection: {clean_line}")
|
| 928 |
else:
|
|
|
|
| 929 |
sections[current_section].append(clean_line)
|
| 930 |
elif current_section and line:
|
| 931 |
clean_line = re.sub(r'[^\w\s.,;:-]', '', line)
|
|
|
|
| 934 |
if current_subsection:
|
| 935 |
sections[current_section][current_subsection].append(clean_line)
|
| 936 |
else:
|
|
|
|
| 937 |
current_subsection = 'Strengths' if current_section == 'Competency' else 'Development'
|
| 938 |
sections[current_section][current_subsection].append(clean_line)
|
| 939 |
+
logger.debug(f"Default subsetion for non-bullet set to: {current_subsection}")
|
| 940 |
else:
|
| 941 |
sections[current_section].append(clean_line)
|
| 942 |
|
| 943 |
+
# Executive Summary
|
| 944 |
+
story.append(Paragraph("2. Executive Summary", h2))
|
| 945 |
+
if sections['Executive Summary']:
|
| 946 |
+
for line in sections['Executive Summary']:
|
| 947 |
+
story.append(Paragraph(line, bullet_style))
|
| 948 |
+
else:
|
| 949 |
+
story.append(Paragraph("Candidate showed moderate engagement; further assessment needed.", bullet_style))
|
| 950 |
+
story.append(Paragraph(f"Interview lasted {analysis_data['text_analysis']['total_duration']:.2f} seconds with {analysis_data['text_analysis']['speaker_turns']} turns.", bullet_style))
|
| 951 |
+
story.append(Spacer(1, 0.15*inch))
|
| 952 |
|
| 953 |
+
# Competency and Content
|
| 954 |
+
story.append(Paragraph("3. Competency & Content", h2))
|
| 955 |
+
story.append(Paragraph("Strengths", h3))
|
| 956 |
+
if sections['Competency']['Strengths']:
|
| 957 |
+
for line in sections['Competency']['Strengths']:
|
| 958 |
+
story.append(Paragraph(line, bullet_style))
|
| 959 |
+
else:
|
| 960 |
+
story.append(Paragraph("Strengths not fully assessed; candidate demonstrated consistent communication.", bullet_style))
|
| 961 |
+
story.append(Spacer(1, 0.1*inch))
|
| 962 |
+
story.append(Paragraph("Growth Areas", h3))
|
| 963 |
+
if sections['Competency']['Growth Areas']:
|
| 964 |
+
for line in sections['Competency']['Growth Areas']:
|
| 965 |
+
story.append(Paragraph(line, bullet_style))
|
| 966 |
+
else:
|
| 967 |
+
story.append(Paragraph("Consider enhancing specificity in responses to highlight expertise.", bullet_style))
|
| 968 |
+
story.append(Spacer(1, 0.15*inch))
|
| 969 |
|
| 970 |
+
# Role Fit
|
| 971 |
+
story.append(Paragraph("4. Role Fit & Potential", h2))
|
| 972 |
+
if sections['Role Fit']:
|
| 973 |
+
for line in sections['Role Fit']:
|
| 974 |
+
story.append(Paragraph(line, bullet_style))
|
| 975 |
+
else:
|
| 976 |
+
story.append(Paragraph("Potential for role fit exists; further evaluation needed to confirm alignment.", bullet_style))
|
| 977 |
+
story.append(Spacer(1, 0.15*inch))
|
| 978 |
+
|
| 979 |
+
# Recommendations
|
| 980 |
+
story.append(Paragraph("5. Recommendations", h2))
|
| 981 |
+
story.append(Paragraph("Development Priorities", h3))
|
| 982 |
+
if sections['Recommendations']['Development']:
|
| 983 |
+
for line in sections['Recommendations']['Development']:
|
| 984 |
+
story.append(Paragraph(line, bullet_style))
|
| 985 |
+
else:
|
| 986 |
+
story.append(Paragraph("Enrollment in communication training to reduce filler words.", bullet_style))
|
| 987 |
+
story.append(Spacer(1, 0.1*inch))
|
| 988 |
+
story.append(Paragraph("Next Steps for Hiring Managers", h3))
|
| 989 |
+
if sections['Recommendations']['Next Steps']:
|
| 990 |
+
for line in sections['Recommendations']['Next Steps']:
|
| 991 |
+
story.append(Paragraph(line, bullet_style))
|
| 992 |
+
else:
|
| 993 |
+
story.append(Paragraph("Schedule a technical assessment to evaluate role-specific skills.", bullet_style))
|
| 994 |
+
story.append(Spacer(1, 0.15*inch))
|
| 995 |
+
story.append(Paragraph("This report provides actionable insights to support hiring decisions.", body_text))
|
| 996 |
|
| 997 |
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 998 |
+
logger.info(f"Company PDF report successfully generated at {output_path}")
|
| 999 |
return True
|
| 1000 |
except Exception as e:
|
| 1001 |
+
logger.error(f"Company PDF generation failed: {str(e)}\nFull Gemini report text:\n{gemini_report_text}", exc_info=True)
|
| 1002 |
return False
|
| 1003 |
|
| 1004 |
def convert_to_serializable(obj):
|
|
|
|
| 1056 |
analysis_data['acceptance_probability'] = calculate_acceptance_probability(analysis_data)
|
| 1057 |
gemini_report_text = generate_report(analysis_data)
|
| 1058 |
base_name = str(uuid.uuid4())
|
| 1059 |
+
user_pdf_path = os.path.join(OUTPUT_DIR, f"{base_name}_user_report.pdf")
|
| 1060 |
+
company_pdf_path = os.path.join(OUTPUT_DIR, f"{base_name}_company_report.pdf")
|
| 1061 |
json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
|
| 1062 |
+
user_pdf_success = create_user_pdf_report(analysis_data, user_pdf_path, gemini_report_text)
|
| 1063 |
+
company_pdf_success = create_company_pdf_report(analysis_data, company_pdf_path, gemini_report_text)
|
| 1064 |
with open(json_path, 'w') as f:
|
| 1065 |
serializable_data = convert_to_serializable(analysis_data)
|
| 1066 |
json.dump(serializable_data, f, indent=2)
|
| 1067 |
+
if not (user_pdf_success and company_pdf_success):
|
| 1068 |
+
logger.warning(f"One or both PDF reports failed to generate for {audio_url}")
|
| 1069 |
return {
|
| 1070 |
+
'user_pdf_path': user_pdf_path if user_pdf_success else None,
|
| 1071 |
+
'company_pdf_path': company_pdf_path if company_pdf_success else None,
|
| 1072 |
'json_path': json_path,
|
| 1073 |
+
'error': 'One or both PDF generations failed'
|
| 1074 |
}
|
| 1075 |
logger.info(f"Processing completed for {audio_url}")
|
| 1076 |
+
return {
|
| 1077 |
+
'user_pdf_path': user_pdf_path,
|
| 1078 |
+
'company_pdf_path': company_pdf_path,
|
| 1079 |
+
'json_path': json_path
|
| 1080 |
+
}
|
| 1081 |
except Exception as e:
|
| 1082 |
logger.error(f"Processing failed for {audio_url}: {str(e)}", exc_info=True)
|
| 1083 |
base_name = str(uuid.uuid4())
|
|
|
|
| 1085 |
with open(json_path, 'w') as f:
|
| 1086 |
json.dump({'error': str(e)}, f, indent=2)
|
| 1087 |
return {
|
| 1088 |
+
'user_pdf_path': None,
|
| 1089 |
+
'company_pdf_path': None,
|
| 1090 |
'json_path': json_path,
|
| 1091 |
'error': str(e)
|
| 1092 |
}
|