Update process_interview.py
Browse files- process_interview.py +78 -70
process_interview.py
CHANGED
|
@@ -505,153 +505,161 @@ def generate_report(analysis_data: Dict) -> str:
|
|
| 505 |
|
| 506 |
|
| 507 |
# --- NEW, ENHANCED PDF GENERATION FUNCTION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
|
| 509 |
try:
|
| 510 |
doc = SimpleDocTemplate(output_path, pagesize=letter,
|
| 511 |
-
rightMargin=inch
|
| 512 |
-
topMargin=inch, bottomMargin=inch
|
| 513 |
|
| 514 |
styles = getSampleStyleSheet()
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
body_text = ParagraphStyle(name='BodyText', parent=styles['Normal'], spaceAfter=6)
|
| 519 |
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=9)
|
| 520 |
-
|
| 521 |
story = []
|
| 522 |
|
| 523 |
-
# --- Header and Footer ---
|
| 524 |
def header_footer(canvas, doc):
|
| 525 |
canvas.saveState()
|
| 526 |
# Footer
|
| 527 |
canvas.setFont('Helvetica', 9)
|
| 528 |
-
canvas.
|
|
|
|
| 529 |
# Header line
|
| 530 |
-
canvas.
|
| 531 |
canvas.setLineWidth(1)
|
| 532 |
-
canvas.line(doc.leftMargin, doc.height +
|
| 533 |
canvas.restoreState()
|
| 534 |
|
| 535 |
-
# --- First Page: Summary ---
|
| 536 |
-
story.append(Paragraph("
|
| 537 |
-
story.append(
|
| 538 |
-
story.append(
|
| 539 |
-
story.append(Spacer(1, 0.3 * inch))
|
| 540 |
|
| 541 |
acceptance_prob = analysis_data.get('acceptance_probability')
|
| 542 |
if acceptance_prob is not None:
|
| 543 |
story.append(Paragraph("Candidate Evaluation Summary", h2))
|
| 544 |
-
prob_color = colors.green if acceptance_prob >= 70 else (colors.
|
| 545 |
-
story.append(Paragraph(f"
|
| 546 |
ParagraphStyle(name='Prob', fontSize=12, spaceAfter=10)))
|
| 547 |
if acceptance_prob >= 80:
|
| 548 |
-
story.append(Paragraph("This indicates a very strong candidate with high potential.", body_text))
|
| 549 |
elif acceptance_prob >= 50:
|
| 550 |
-
story.append(Paragraph("This candidate shows solid potential
|
| 551 |
else:
|
| 552 |
-
story.append(Paragraph("This candidate may require significant development or may not be
|
| 553 |
|
| 554 |
story.append(PageBreak())
|
| 555 |
|
| 556 |
-
# --- Second Page
|
| 557 |
-
story.append(Paragraph("
|
|
|
|
|
|
|
| 558 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 559 |
if voice_analysis and 'error' not in voice_analysis:
|
| 560 |
-
#
|
| 561 |
table_data = [
|
| 562 |
['Metric', 'Value', 'Interpretation'],
|
| 563 |
-
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", '
|
| 564 |
-
['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", '
|
| 565 |
['Anxiety Level', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A').upper(), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
|
| 566 |
['Confidence Level', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A').upper(), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
|
| 567 |
-
['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A').upper(), 'Overall speech flow']
|
| 568 |
]
|
| 569 |
-
table = Table(table_data, colWidths=[1.
|
| 570 |
table.setStyle(TableStyle([
|
| 571 |
-
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#
|
| 572 |
('TEXTCOLOR',(0,0),(-1,0),colors.whitesmoke),
|
| 573 |
-
('ALIGN', (0,0), (-1,-1), '
|
| 574 |
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 575 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 576 |
-
('
|
|
|
|
|
|
|
| 577 |
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#F0F8FF')),
|
| 578 |
-
('GRID', (0,0), (-1,-1), 1, colors.
|
| 579 |
]))
|
| 580 |
story.append(table)
|
| 581 |
story.append(Spacer(1, 0.2 * inch))
|
| 582 |
|
| 583 |
-
# Chart generation
|
| 584 |
chart_buffer = io.BytesIO()
|
| 585 |
generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
|
| 586 |
chart_buffer.seek(0)
|
| 587 |
img = Image(chart_buffer, width=4*inch, height=2.5*inch)
|
|
|
|
| 588 |
story.append(img)
|
| 589 |
else:
|
| 590 |
story.append(Paragraph("Voice analysis data not available.", body_text))
|
| 591 |
|
| 592 |
story.append(PageBreak())
|
| 593 |
-
|
| 594 |
-
# ---
|
| 595 |
sections = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
current_section = None
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
sections[name] = []
|
| 607 |
-
|
| 608 |
-
# Parse text into sections based on keywords
|
| 609 |
-
for line in gemini_report_text.split('\n'):
|
| 610 |
-
line_lower = line.lower()
|
| 611 |
-
matched = False
|
| 612 |
-
for name, pattern in section_patterns.items():
|
| 613 |
-
if re.search(pattern, line_lower):
|
| 614 |
-
current_section = name
|
| 615 |
-
matched = True
|
| 616 |
break
|
| 617 |
-
|
| 618 |
-
|
|
|
|
| 619 |
|
| 620 |
-
# Display Content
|
| 621 |
-
story.append(Paragraph("2. Content Analysis", h2))
|
| 622 |
if sections['Content Analysis & Strengths/Areas for Development']:
|
| 623 |
for line in sections['Content Analysis & Strengths/Areas for Development']:
|
| 624 |
-
line = line.strip()
|
| 625 |
-
if not line: continue
|
| 626 |
if line.startswith(('-', '•', '*')):
|
| 627 |
story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
|
| 628 |
else:
|
| 629 |
story.append(Paragraph(line, body_text))
|
| 630 |
else:
|
| 631 |
-
story.append(Paragraph("Content analysis not provided
|
| 632 |
|
| 633 |
-
story.append(Spacer(1, 0.
|
| 634 |
-
|
| 635 |
-
story.append(Paragraph("3. Recommendations", h2))
|
| 636 |
if sections['Actionable Recommendations']:
|
| 637 |
for line in sections['Actionable Recommendations']:
|
| 638 |
-
line = line.strip()
|
| 639 |
-
if not line: continue
|
| 640 |
if line.startswith(('-', '•', '*')):
|
| 641 |
story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
|
| 642 |
else:
|
| 643 |
story.append(Paragraph(line, body_text))
|
| 644 |
else:
|
| 645 |
-
story.append(Paragraph("Recommendations not provided
|
| 646 |
-
|
| 647 |
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 648 |
return True
|
| 649 |
except Exception as e:
|
| 650 |
logger.error(f"Enhanced PDF creation failed: {str(e)}", exc_info=True)
|
| 651 |
return False
|
| 652 |
|
| 653 |
-
|
| 654 |
-
|
| 655 |
def convert_to_serializable(obj):
|
| 656 |
if isinstance(obj, np.generic): return obj.item()
|
| 657 |
if isinstance(obj, dict): return {k: convert_to_serializable(v) for k, v in obj.items()}
|
|
|
|
| 505 |
|
| 506 |
|
| 507 |
# --- NEW, ENHANCED PDF GENERATION FUNCTION ---
|
| 508 |
+
# --- Make sure these imports are at the top of your process_interview.py file ---
|
| 509 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image
|
| 510 |
+
from reportlab.lib.pagesizes import letter
|
| 511 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 512 |
+
from reportlab.lib.units import inch
|
| 513 |
+
from reportlab.lib import colors
|
| 514 |
+
import time
|
| 515 |
+
import re
|
| 516 |
+
import io
|
| 517 |
+
|
| 518 |
+
# --- New, Enhanced PDF Generation Function ---
|
| 519 |
def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
|
| 520 |
try:
|
| 521 |
doc = SimpleDocTemplate(output_path, pagesize=letter,
|
| 522 |
+
rightMargin=0.75*inch, leftMargin=0.75*inch,
|
| 523 |
+
topMargin=1*inch, bottomMargin=1*inch)
|
| 524 |
|
| 525 |
styles = getSampleStyleSheet()
|
| 526 |
+
h1 = ParagraphStyle(name='Heading1', fontSize=20, leading=24, spaceAfter=18, alignment=1, textColor=colors.HexColor('#00205B'))
|
| 527 |
+
h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=18, spaceBefore=12, spaceAfter=6, textColor=colors.HexColor('#003366'))
|
| 528 |
+
body_text = ParagraphStyle(name='BodyText', parent=styles['Normal'], fontSize=10, leading=14, spaceAfter=6)
|
|
|
|
| 529 |
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=9)
|
| 530 |
+
|
| 531 |
story = []
|
| 532 |
|
| 533 |
+
# --- Header and Footer Logic ---
|
| 534 |
def header_footer(canvas, doc):
|
| 535 |
canvas.saveState()
|
| 536 |
# Footer
|
| 537 |
canvas.setFont('Helvetica', 9)
|
| 538 |
+
canvas.setFillColor(colors.grey)
|
| 539 |
+
canvas.drawString(doc.leftMargin, 0.5 * inch, f"Page {doc.page} | EvalBot Confidential Report")
|
| 540 |
# Header line
|
| 541 |
+
canvas.setStrokeColor(colors.HexColor('#003366'))
|
| 542 |
canvas.setLineWidth(1)
|
| 543 |
+
canvas.line(doc.leftMargin, doc.height + 0.75*inch, doc.width + doc.leftMargin, doc.height + 0.75*inch)
|
| 544 |
canvas.restoreState()
|
| 545 |
|
| 546 |
+
# --- First Page: Title and Summary ---
|
| 547 |
+
story.append(Paragraph("Interview Performance Analysis", h1))
|
| 548 |
+
story.append(Paragraph(f"Analysis Date: {time.strftime('%Y-%m-%d')}", ParagraphStyle(name='Date', alignment=1, fontSize=9, textColor=colors.grey)))
|
| 549 |
+
story.append(Spacer(1, 0.4 * inch))
|
|
|
|
| 550 |
|
| 551 |
acceptance_prob = analysis_data.get('acceptance_probability')
|
| 552 |
if acceptance_prob is not None:
|
| 553 |
story.append(Paragraph("Candidate Evaluation Summary", h2))
|
| 554 |
+
prob_color = colors.green if acceptance_prob >= 70 else (colors.darkorange if acceptance_prob >= 40 else colors.red)
|
| 555 |
+
story.append(Paragraph(f"Estimated Acceptance Probability: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
|
| 556 |
ParagraphStyle(name='Prob', fontSize=12, spaceAfter=10)))
|
| 557 |
if acceptance_prob >= 80:
|
| 558 |
+
story.append(Paragraph("<b>Overall Assessment:</b> This indicates a very strong candidate with high potential. Recommended for the next round.", body_text))
|
| 559 |
elif acceptance_prob >= 50:
|
| 560 |
+
story.append(Paragraph("<b>Overall Assessment:</b> This candidate shows solid potential but has key areas for improvement.", body_text))
|
| 561 |
else:
|
| 562 |
+
story.append(Paragraph("<b>Overall Assessment:</b> This candidate may require significant development or may not be the ideal fit at this time.", body_text))
|
| 563 |
|
| 564 |
story.append(PageBreak())
|
| 565 |
|
| 566 |
+
# --- Second Page: Detailed Analysis ---
|
| 567 |
+
story.append(Paragraph("Detailed Analysis", h1))
|
| 568 |
+
|
| 569 |
+
story.append(Paragraph("1. Voice & Speech Metrics", h2))
|
| 570 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 571 |
if voice_analysis and 'error' not in voice_analysis:
|
| 572 |
+
# --- This is the corrected table ---
|
| 573 |
table_data = [
|
| 574 |
['Metric', 'Value', 'Interpretation'],
|
| 575 |
+
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Indicator of pace and confidence.'],
|
| 576 |
+
['Filler Words Ratio', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", 'Measures use of "um", "uh", etc.'],
|
| 577 |
['Anxiety Level', voice_analysis.get('interpretation', {}).get('anxiety_level', 'N/A').upper(), f"Score: {voice_analysis.get('composite_scores', {}).get('anxiety', 0):.3f}"],
|
| 578 |
['Confidence Level', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A').upper(), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
|
| 579 |
+
['Fluency Level', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A').upper(), 'Overall speech flow and coherence.']
|
| 580 |
]
|
| 581 |
+
table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.7*inch])
|
| 582 |
table.setStyle(TableStyle([
|
| 583 |
+
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#003366')),
|
| 584 |
('TEXTCOLOR',(0,0),(-1,0),colors.whitesmoke),
|
| 585 |
+
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
| 586 |
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 587 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 588 |
+
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 589 |
+
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
| 590 |
+
('TOPPADDING', (0, 0), (-1, 0), 10),
|
| 591 |
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#F0F8FF')),
|
| 592 |
+
('GRID', (0,0), (-1,-1), 1, colors.lightgrey)
|
| 593 |
]))
|
| 594 |
story.append(table)
|
| 595 |
story.append(Spacer(1, 0.2 * inch))
|
| 596 |
|
|
|
|
| 597 |
chart_buffer = io.BytesIO()
|
| 598 |
generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
|
| 599 |
chart_buffer.seek(0)
|
| 600 |
img = Image(chart_buffer, width=4*inch, height=2.5*inch)
|
| 601 |
+
img.hAlign = 'CENTER'
|
| 602 |
story.append(img)
|
| 603 |
else:
|
| 604 |
story.append(Paragraph("Voice analysis data not available.", body_text))
|
| 605 |
|
| 606 |
story.append(PageBreak())
|
| 607 |
+
|
| 608 |
+
# --- Gemini Report Parsing and Display ---
|
| 609 |
sections = {}
|
| 610 |
+
# Pre-populate to maintain order
|
| 611 |
+
section_titles = ["Executive Summary", "Voice Analysis Insights", "Content Analysis & Strengths/Areas for Development", "Actionable Recommendations"]
|
| 612 |
+
for title in section_titles:
|
| 613 |
+
sections[title] = []
|
| 614 |
+
|
| 615 |
+
# Use a more robust way to capture content under each heading
|
| 616 |
+
# This regex captures the heading line itself to exclude it from the content
|
| 617 |
+
report_parts = re.split(r'(\s*\*\*\s*\d\.\s*.*?\s*\*\*)', gemini_report_text)
|
| 618 |
+
|
| 619 |
current_section = None
|
| 620 |
+
for part in report_parts:
|
| 621 |
+
if not part.strip(): continue
|
| 622 |
+
|
| 623 |
+
is_heading = False
|
| 624 |
+
for title in section_titles:
|
| 625 |
+
# Check if the part is a heading
|
| 626 |
+
if title.lower() in part.lower():
|
| 627 |
+
current_section = title
|
| 628 |
+
is_heading = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
break
|
| 630 |
+
|
| 631 |
+
if not is_heading and current_section:
|
| 632 |
+
sections[current_section].append(part.strip())
|
| 633 |
|
| 634 |
+
# Display Content and Recommendations
|
| 635 |
+
story.append(Paragraph("2. Content Analysis (from Gemini)", h2))
|
| 636 |
if sections['Content Analysis & Strengths/Areas for Development']:
|
| 637 |
for line in sections['Content Analysis & Strengths/Areas for Development']:
|
|
|
|
|
|
|
| 638 |
if line.startswith(('-', '•', '*')):
|
| 639 |
story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
|
| 640 |
else:
|
| 641 |
story.append(Paragraph(line, body_text))
|
| 642 |
else:
|
| 643 |
+
story.append(Paragraph("Content analysis not provided.", body_text))
|
| 644 |
|
| 645 |
+
story.append(Spacer(1, 0.3*inch))
|
| 646 |
+
|
| 647 |
+
story.append(Paragraph("3. Actionable Recommendations (from Gemini)", h2))
|
| 648 |
if sections['Actionable Recommendations']:
|
| 649 |
for line in sections['Actionable Recommendations']:
|
|
|
|
|
|
|
| 650 |
if line.startswith(('-', '•', '*')):
|
| 651 |
story.append(Paragraph(line.lstrip('-•* ').strip(), bullet_style))
|
| 652 |
else:
|
| 653 |
story.append(Paragraph(line, body_text))
|
| 654 |
else:
|
| 655 |
+
story.append(Paragraph("Recommendations not provided.", body_text))
|
| 656 |
+
|
| 657 |
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 658 |
return True
|
| 659 |
except Exception as e:
|
| 660 |
logger.error(f"Enhanced PDF creation failed: {str(e)}", exc_info=True)
|
| 661 |
return False
|
| 662 |
|
|
|
|
|
|
|
| 663 |
def convert_to_serializable(obj):
|
| 664 |
if isinstance(obj, np.generic): return obj.item()
|
| 665 |
if isinstance(obj, dict): return {k: convert_to_serializable(v) for k, v in obj.items()}
|