Update process_interview.py
Browse files- process_interview.py +118 -57
process_interview.py
CHANGED
|
@@ -35,7 +35,15 @@ import spacy
|
|
| 35 |
import google.generativeai as genai
|
| 36 |
import joblib
|
| 37 |
from concurrent.futures import ThreadPoolExecutor
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Setup logging
|
| 40 |
logging.basicConfig(level=logging.INFO)
|
| 41 |
logger = logging.getLogger(__name__)
|
|
@@ -495,69 +503,75 @@ def generate_report(analysis_data: Dict) -> str:
|
|
| 495 |
return f"Error generating report: {str(e)}"
|
| 496 |
|
| 497 |
|
|
|
|
|
|
|
| 498 |
def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text: str):
|
| 499 |
try:
|
| 500 |
-
doc = SimpleDocTemplate(output_path, pagesize=letter
|
|
|
|
|
|
|
|
|
|
| 501 |
styles = getSampleStyleSheet()
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
body_text = ParagraphStyle(name='BodyText', parent=styles['Normal'],
|
| 506 |
bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=9)
|
|
|
|
| 507 |
story = []
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
story.append(Spacer(1, 0.3 * inch))
|
|
|
|
| 512 |
acceptance_prob = analysis_data.get('acceptance_probability')
|
| 513 |
if acceptance_prob is not None:
|
| 514 |
-
story.append(Paragraph("
|
| 515 |
-
story.append(Spacer(1, 0.1 * inch))
|
| 516 |
prob_color = colors.green if acceptance_prob >= 70 else (colors.orange if acceptance_prob >= 40 else colors.red)
|
| 517 |
-
story.append(Paragraph(f"<font size=
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
section_patterns = {
|
| 526 |
-
r'^\s*\*\*\s*1\.\s*Executive Summary\s*\*\*': 'Executive Summary',
|
| 527 |
-
r'^\s*\*\*\s*2\.\s*Voice Analysis Insights\s*\*\*': 'Voice Analysis Insights',
|
| 528 |
-
r'^\s*\*\*\s*3\.\s*Content Analysis & Strengths/Areas for Development\s*\*\*': 'Content Analysis',
|
| 529 |
-
r'^\s*\*\*\s*4\.\s*Actionable Recommendations\s*\*\*': 'Recommendations'
|
| 530 |
-
}
|
| 531 |
-
for line in gemini_report_text.split('\n'):
|
| 532 |
-
matched_section = False
|
| 533 |
-
for pattern, section_name in section_patterns.items():
|
| 534 |
-
if re.match(pattern, line):
|
| 535 |
-
current_section = section_name
|
| 536 |
-
sections[current_section] = []
|
| 537 |
-
matched_section = True
|
| 538 |
-
break
|
| 539 |
-
if not matched_section and current_section:
|
| 540 |
-
sections[current_section].append(line)
|
| 541 |
-
|
| 542 |
-
story.append(PageBreak()) # Start detailed report on a new page
|
| 543 |
|
| 544 |
-
story.append(
|
|
|
|
|
|
|
|
|
|
| 545 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 546 |
if voice_analysis and 'error' not in voice_analysis:
|
|
|
|
| 547 |
table_data = [
|
| 548 |
['Metric', 'Value', 'Interpretation'],
|
| 549 |
-
['Speaking Rate', f"{voice_analysis
|
| 550 |
-
['Filler Words', f"{voice_analysis
|
| 551 |
-
['
|
| 552 |
-
['
|
| 553 |
-
['
|
| 554 |
-
['Fluency', voice_analysis['interpretation']['fluency_level'].upper(), 'Overall speech flow']
|
| 555 |
]
|
| 556 |
table = Table(table_data, colWidths=[1.5*inch, 1.5*inch, 3*inch])
|
| 557 |
table.setStyle(TableStyle([
|
| 558 |
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#4682B4')),
|
| 559 |
('TEXTCOLOR',(0,0),(-1,0),colors.whitesmoke),
|
| 560 |
('ALIGN', (0,0), (-1,-1), 'CENTER'),
|
|
|
|
| 561 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 562 |
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
| 563 |
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#F0F8FF')),
|
|
@@ -566,31 +580,78 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
|
|
| 566 |
story.append(table)
|
| 567 |
story.append(Spacer(1, 0.2 * inch))
|
| 568 |
|
|
|
|
| 569 |
chart_buffer = io.BytesIO()
|
| 570 |
-
generate_anxiety_confidence_chart(voice_analysis
|
| 571 |
chart_buffer.seek(0)
|
| 572 |
img = Image(chart_buffer, width=4*inch, height=2.5*inch)
|
| 573 |
story.append(img)
|
| 574 |
else:
|
| 575 |
-
story.append(Paragraph("Voice analysis not available.", body_text))
|
| 576 |
|
| 577 |
story.append(PageBreak())
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
return True
|
| 589 |
except Exception as e:
|
| 590 |
-
logger.error(f"PDF creation failed: {str(e)}", exc_info=True)
|
| 591 |
return False
|
| 592 |
|
| 593 |
|
|
|
|
| 594 |
def convert_to_serializable(obj):
|
| 595 |
if isinstance(obj, np.generic): return obj.item()
|
| 596 |
if isinstance(obj, dict): return {k: convert_to_serializable(v) for k, v in obj.items()}
|
|
|
|
| 35 |
import google.generativeai as genai
|
| 36 |
import joblib
|
| 37 |
from concurrent.futures import ThreadPoolExecutor
|
| 38 |
+
# --- Imports to ensure are present at the top of process_interview.py ---
|
| 39 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image
|
| 40 |
+
from reportlab.lib.pagesizes import letter
|
| 41 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 42 |
+
from reportlab.lib.units import inch
|
| 43 |
+
from reportlab.lib import colors
|
| 44 |
+
import time
|
| 45 |
+
import re
|
| 46 |
+
import io
|
| 47 |
# Setup logging
|
| 48 |
logging.basicConfig(level=logging.INFO)
|
| 49 |
logger = logging.getLogger(__name__)
|
|
|
|
| 503 |
return f"Error generating report: {str(e)}"
|
| 504 |
|
| 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/2, leftMargin=inch/2,
|
| 512 |
+
topMargin=inch, bottomMargin=inch/2)
|
| 513 |
+
|
| 514 |
styles = getSampleStyleSheet()
|
| 515 |
+
styles.add(ParagraphStyle(name='Justify', alignment=4)) # TA_JUSTIFY = 4
|
| 516 |
+
h1 = ParagraphStyle(name='Heading1', fontSize=18, leading=22, spaceAfter=20, alignment=1, textColor=colors.HexColor('#003366'))
|
| 517 |
+
h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=18, spaceBefore=12, spaceAfter=10, textColor=colors.HexColor('#336699'))
|
| 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.drawString(inch, 0.5 * inch, f"Page {doc.page} | EvalBot Analysis")
|
| 529 |
+
# Header line
|
| 530 |
+
canvas.setStrokeColorRGB(0, 0.2, 0.4)
|
| 531 |
+
canvas.setLineWidth(1)
|
| 532 |
+
canvas.line(doc.leftMargin, doc.height + doc.topMargin - inch/2, doc.width + doc.leftMargin, doc.height + doc.topMargin - inch/2)
|
| 533 |
+
canvas.restoreState()
|
| 534 |
+
|
| 535 |
+
# --- First Page: Summary ---
|
| 536 |
+
story.append(Paragraph("EvalBot Interview Analysis Report", h1))
|
| 537 |
+
story.append(Spacer(1, 0.1 * inch))
|
| 538 |
+
story.append(Paragraph(f"Analysis Date: {time.strftime('%Y-%m-%d')}", styles['Normal']))
|
| 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.orange if acceptance_prob >= 40 else colors.red)
|
| 545 |
+
story.append(Paragraph(f"<font size=14>Estimated Acceptance Probability: <b><font color='{prob_color.hexval()}'>{acceptance_prob:.2f}%</font></b></font>",
|
| 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 with areas for improvement.", body_text))
|
| 551 |
+
else:
|
| 552 |
+
story.append(Paragraph("This candidate may require significant development or may not be an ideal fit.", body_text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
|
| 554 |
+
story.append(PageBreak())
|
| 555 |
+
|
| 556 |
+
# --- Second Page and beyond: Detailed Analysis ---
|
| 557 |
+
story.append(Paragraph("1. Detailed Voice Analysis", h2))
|
| 558 |
voice_analysis = analysis_data.get('voice_analysis', {})
|
| 559 |
if voice_analysis and 'error' not in voice_analysis:
|
| 560 |
+
# Table for voice metrics
|
| 561 |
table_data = [
|
| 562 |
['Metric', 'Value', 'Interpretation'],
|
| 563 |
+
['Speaking Rate', f"{voice_analysis.get('speaking_rate', 0):.2f} words/sec", 'Average rate'],
|
| 564 |
+
['Filler Words', f"{voice_analysis.get('filler_ratio', 0) * 100:.1f}%", '% of total words'],
|
| 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.5*inch, 1.5*inch, 3*inch])
|
| 570 |
table.setStyle(TableStyle([
|
| 571 |
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#4682B4')),
|
| 572 |
('TEXTCOLOR',(0,0),(-1,0),colors.whitesmoke),
|
| 573 |
('ALIGN', (0,0), (-1,-1), 'CENTER'),
|
| 574 |
+
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 575 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 576 |
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
| 577 |
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#F0F8FF')),
|
|
|
|
| 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 |
+
# --- Parse and display Gemini report sections ---
|
| 595 |
+
sections = {}
|
| 596 |
+
current_section = None
|
| 597 |
+
# Simplified patterns to find sections
|
| 598 |
+
section_patterns = {
|
| 599 |
+
'Executive Summary': r'executive summary',
|
| 600 |
+
'Voice Analysis Insights': r'voice analysis insights',
|
| 601 |
+
'Content Analysis & Strengths/Areas for Development': r'content analysis|strengths/areas for development',
|
| 602 |
+
'Actionable Recommendations': r'actionable recommendations'
|
| 603 |
+
}
|
| 604 |
+
# Pre-populate sections to maintain order
|
| 605 |
+
for name in section_patterns.keys():
|
| 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 |
+
if not matched and current_section:
|
| 618 |
+
sections[current_section].append(line)
|
| 619 |
+
|
| 620 |
+
# Display Content Analysis and Recommendations
|
| 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 in the report.", body_text))
|
| 632 |
+
|
| 633 |
+
story.append(Spacer(1, 0.2*inch))
|
| 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 in the report.", body_text))
|
| 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()}
|