norhan12 commited on
Commit
1c3a078
·
verified ·
1 Parent(s): a9a21fe

Update process_interview.py

Browse files
Files changed (1) hide show
  1. process_interview.py +61 -58
process_interview.py CHANGED
@@ -355,11 +355,11 @@ def analyze_interviewee_voice(audio_path: str, utterances: List[Dict]) -> Dict:
355
  jitter = np.mean(np.abs(np.diff(pitches))) / pitch_mean if len(pitches) > 1 and pitch_mean > 0 else 0
356
  intensities = []
357
  for segment in segments:
358
- rms = librosa.feature.rms(y=segment)[0]
359
  intensities.extend(rms)
360
  intensity_mean = np.mean(intensities) if intensities else 0
361
  intensity_std = np.std(intensities) if intensities else 0
362
- shimmer = np.mean(np.abs(np.diff(intensities))) / intensity_mean if len(intensities) > 1 and intensity_mean > 0 else 0
363
  anxiety_score = 0.6 * (pitch_std / pitch_mean) + 0.4 * (jitter + shimmer) if pitch_mean > 0 else 0
364
  confidence_score = 0.7 * (1 / (1 + intensity_std)) + 0.3 * (1 / (1 + filler_ratio))
365
  hesitation_score = filler_ratio + repetition_score
@@ -446,7 +446,7 @@ def generate_report(analysis_data: Dict) -> str:
446
  try:
447
  voice = analysis_data.get('voice_analysis', {})
448
  voice_interpretation = generate_voice_interpretation(voice)
449
- interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:5]
450
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
451
  acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
452
  if acceptance_prob >= 80:
@@ -458,30 +458,29 @@ def generate_report(analysis_data: Dict) -> str:
458
  else:
459
  acceptance_line += "HR Verdict: Limited fit, significant improvement required."
460
  prompt = f"""
461
- 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.
462
  {acceptance_line}
463
  **1. Executive Summary**
464
- - Summarize performance, key metrics, and hiring potential.
465
  - Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
466
  - Speaker Turns: {analysis_data['text_analysis']['speaker_turns']}
467
  - Participants: {', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))}
468
  **2. Communication and Vocal Dynamics**
469
- - Evaluate vocal delivery (rate, fluency, confidence).
470
- - Provide HR insights on workplace alignment.
471
  {voice_interpretation}
472
  **3. Competency and Content**
473
- - Assess leadership, problem-solving, communication, adaptability.
474
- - List strengths and growth areas separately with examples.
475
  - Sample responses:
476
  {chr(10).join(interviewee_responses)}
477
  **4. Role Fit and Potential**
478
- - Analyze cultural fit, role readiness, and growth potential.
479
  **5. Recommendations**
480
- - Provide prioritized strategies for growth (communication, technical skills, presence).
481
- - Suggest next steps for hiring managers (advance, train, assess).
482
  """
483
  response = gemini_model.generate_content(prompt)
484
- return re.sub(r'[^\x00-\x7F]+', '', response.text) # Sanitize non-ASCII characters
485
  except Exception as e:
486
  logger.error(f"Report generation failed: {str(e)}")
487
  return f"Error generating report: {str(e)}"
@@ -492,36 +491,36 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
492
  rightMargin=0.75*inch, leftMargin=0.75*inch,
493
  topMargin=1*inch, bottomMargin=1*inch)
494
  styles = getSampleStyleSheet()
495
- h1 = ParagraphStyle(name='Heading1', fontSize=20, leading=24, spaceAfter=18, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
496
- h2 = ParagraphStyle(name='Heading2', fontSize=14, leading=16, spaceBefore=12, spaceAfter=8, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
497
- h3 = ParagraphStyle(name='Heading3', fontSize=10, leading=12, spaceBefore=8, spaceAfter=6, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
498
- body_text = ParagraphStyle(name='BodyText', fontSize=9, leading=12, spaceAfter=6, fontName='Helvetica', textColor=colors.HexColor('#333333'))
499
- bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=18, bulletIndent=8, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=9)
500
 
501
  story = []
502
 
503
  def header_footer(canvas, doc):
504
  canvas.saveState()
505
- canvas.setFont('Helvetica', 8)
506
  canvas.setFillColor(colors.HexColor('#666666'))
507
  canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
508
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
509
- canvas.setLineWidth(0.8)
510
  canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
511
- canvas.setFont('Helvetica-Bold', 9)
512
  canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
513
  canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
514
  canvas.restoreState()
515
 
516
  # Title Page
517
  story.append(Paragraph("Candidate Interview Analysis", h1))
518
- story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=9, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
519
- story.append(Spacer(1, 0.4*inch))
520
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
521
  story.append(Paragraph("Hiring Suitability Snapshot", h2))
522
  prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
523
- story.append(Paragraph(f"Suitability Score: <font size=15 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
524
- ParagraphStyle(name='Prob', fontSize=11, spaceAfter=10, alignment=1, fontName='Helvetica-Bold')))
525
  if acceptance_prob >= 80:
526
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
527
  elif acceptance_prob >= 60:
@@ -530,28 +529,29 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
530
  story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
531
  else:
532
  story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
533
- story.append(Spacer(1, 0.3*inch))
 
534
  table_data = [
535
  ['Metric', 'Value'],
536
  ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
537
  ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
538
- ['Participants', ', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))],
539
  ]
540
- table = Table(table_data, colWidths=[2.3*inch, 3.7*inch])
541
  table.setStyle(TableStyle([
542
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
543
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
544
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
545
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
546
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
547
- ('FONTSIZE', (0,0), (-1,-1), 9),
548
- ('BOTTOMPADDING', (0,0), (-1,0), 8),
549
- ('TOPPADDING', (0,0), (-1,0), 8),
550
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
551
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
552
  ]))
553
  story.append(table)
554
- story.append(Spacer(1, 0.4*inch))
555
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
556
  story.append(PageBreak())
557
 
@@ -570,30 +570,30 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
570
  ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
571
  ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
572
  ]
573
- table = Table(table_data, colWidths=[1.6*inch, 1.2*inch, 3.2*inch])
574
  table.setStyle(TableStyle([
575
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
576
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
577
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
578
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
579
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
580
- ('FONTSIZE', (0,0), (-1,-1), 9),
581
- ('BOTTOMPADDING', (0,0), (-1,0), 8),
582
- ('TOPPADDING', (0,0), (-1,0), 8),
583
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
584
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#DDE4EB')),
585
  ]))
586
  story.append(table)
587
- story.append(Spacer(1, 0.2*inch))
588
  chart_buffer = io.BytesIO()
589
  generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
590
  chart_buffer.seek(0)
591
- img = Image(chart_buffer, width=4.5*inch, height=3*inch)
592
  img.hAlign = 'CENTER'
593
  story.append(img)
594
  else:
595
- story.append(Paragraph("Vocal analysis unavailable.", body_text))
596
- story.append(Spacer(1, 0.2*inch))
597
 
598
  # Parse Gemini Report
599
  sections = {
@@ -609,7 +609,6 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
609
  for line in lines:
610
  line = line.strip()
611
  if not line: continue
612
- # Simplified regex to avoid parenthesis issues
613
  if line.startswith('**') and line.endswith('**'):
614
  section_title = line.strip('**').strip()
615
  if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
@@ -632,7 +631,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
632
  elif line.startswith(('-', '*', '•')) and current_section:
633
  clean_line = line.lstrip('-*• ').strip()
634
  if not clean_line: continue
635
- clean_line = re.sub(r'[()]', '', clean_line) # Remove parentheses
636
  if current_section == 'Competency':
637
  if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength']):
638
  current_subsection = 'Strengths'
@@ -657,10 +656,10 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
657
  story.append(Paragraph(line, bullet_style))
658
  else:
659
  story.append(Paragraph("No summary provided.", body_text))
660
- story.append(Spacer(1, 0.2*inch))
661
 
662
  # Competency and Content
663
- story.append(Paragraph("3. Competency & Evaluation", h2))
664
  story.append(Paragraph("Strengths", h3))
665
  if sections['Competency']['Strengths']:
666
  for line in sections['Competency']['Strengths']:
@@ -674,7 +673,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
674
  story.append(Paragraph(line, bullet_style))
675
  else:
676
  story.append(Paragraph("No growth areas identified; maintain current strengths.", body_text))
677
- story.append(Spacer(1, 0.2*inch))
678
 
679
  # Role Fit
680
  story.append(Paragraph("4. Role Fit & Potential", h2))
@@ -683,7 +682,7 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
683
  story.append(Paragraph(line, bullet_style))
684
  else:
685
  story.append(Paragraph("No fit analysis provided.", body_text))
686
- story.append(Spacer(1, 0.2*inch))
687
 
688
  # Recommendations
689
  story.append(Paragraph("5. Recommendations", h2))
@@ -694,13 +693,14 @@ def create_pdf_report(analysis_data: Dict, output_path: str, gemini_report_text:
694
  else:
695
  story.append(Paragraph("No development priorities specified.", body_text))
696
  story.append(Spacer(1, 0.1*inch))
697
- story.append(Paragraph("Next Steps", h3))
698
  if sections['Recommendations']['Next Steps']:
699
  for line in sections['Recommendations']['Next Steps']:
700
  story.append(Paragraph(line, bullet_style))
701
  else:
702
  story.append(Paragraph("No next steps provided.", body_text))
703
- story.append(Spacer(1, 0.2*inch))
 
704
 
705
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
706
  logger.info(f"PDF report successfully generated at {output_path}")
@@ -716,17 +716,20 @@ def convert_to_serializable(obj):
716
  if isinstance(obj, np.ndarray): return obj.tolist()
717
  return obj
718
 
719
- def process_interview(audio_path_or_url: str) -> Dict:
 
720
  local_audio_path = None
721
  wav_file = None
722
  is_downloaded = False
723
  try:
724
- logger.info(f"Starting processing for {audio_path_or_url}")
725
- if audio_path_or_url.startswith(('http://', 'https://')):
726
- local_audio_path = download_audio_from_url(audio_path_or_url)
 
 
727
  is_downloaded = True
728
  else:
729
- local_audio_path = audio_path_or_url
730
  if not os.path.exists(local_audio_path):
731
  raise FileNotFoundError(f"Local audio file not found: {local_audio_path}")
732
  wav_file = convert_to_wav(local_audio_path)
@@ -762,16 +765,16 @@ def process_interview(audio_path_or_url: str) -> Dict:
762
  serializable_data = convert_to_serializable(analysis_data)
763
  json.dump(serializable_data, f, indent=2)
764
  if not pdf_success:
765
- logger.warning(f"PDF report failed to generate for {audio_path_or_url}")
766
  return {
767
  'pdf_path': None,
768
  'json_path': json_path,
769
  'error': 'PDF generation failed'
770
  }
771
- logger.info(f"Processing completed for {audio_path_or_url}")
772
  return {'pdf_path': pdf_path, 'json_path': json_path}
773
  except Exception as e:
774
- logger.error(f"Processing failed for {audio_path_or_url}: {str(e)}", exc_info=True)
775
  base_name = str(uuid.uuid4())
776
  json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
777
  with open(json_path, 'w') as f:
 
355
  jitter = np.mean(np.abs(np.diff(pitches))) / pitch_mean if len(pitches) > 1 and pitch_mean > 0 else 0
356
  intensities = []
357
  for segment in segments:
358
+ rms = np.mean(librosa.feature.rms(y=segment)[0])
359
  intensities.extend(rms)
360
  intensity_mean = np.mean(intensities) if intensities else 0
361
  intensity_std = np.std(intensities) if intensities else 0
362
+ shimmer = np.mean(np.abs(np.diff(intensities))) / intensity_mean if intensity_mean > 0 else 0
363
  anxiety_score = 0.6 * (pitch_std / pitch_mean) + 0.4 * (jitter + shimmer) if pitch_mean > 0 else 0
364
  confidence_score = 0.7 * (1 / (1 + intensity_std)) + 0.3 * (1 / (1 + filler_ratio))
365
  hesitation_score = filler_ratio + repetition_score
 
446
  try:
447
  voice = analysis_data.get('voice_analysis', {})
448
  voice_interpretation = generate_voice_interpretation(voice)
449
+ interviewee_responses = [f"- {u['text']}" for u in analysis_data['transcript'] if u['role'] == 'Interviewee'][:3]
450
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
451
  acceptance_line = f"\n**Suitability Score: {acceptance_prob:.2f}%**\n"
452
  if acceptance_prob >= 80:
 
458
  else:
459
  acceptance_line += "HR Verdict: Limited fit, significant improvement required."
460
  prompt = f"""
461
+ You are EvalBot, a senior HR consultant delivering a concise, professional interview analysis report. Use clear headings, bullet points ('-'), complete sentences, and formal language. Avoid redundancy, vague terms, and special characters that could break formatting. Ensure each section is unique and actionable.
462
  {acceptance_line}
463
  **1. Executive Summary**
464
+ - Provide a narrative overview of the candidate’s performance, highlighting key strengths and fit.
465
  - Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds
466
  - Speaker Turns: {analysis_data['text_analysis']['speaker_turns']}
467
  - Participants: {', '.join(sorted(set(u['speaker'] for u in analysis_data['transcript'])))}
468
  **2. Communication and Vocal Dynamics**
469
+ - Evaluate vocal delivery (rate, fluency, confidence) with specific insights.
 
470
  {voice_interpretation}
471
  **3. Competency and Content**
472
+ - Assess leadership, problem-solving, communication, and adaptability with clear examples.
473
+ - List strengths and growth areas separately, using quantifiable achievements where possible.
474
  - Sample responses:
475
  {chr(10).join(interviewee_responses)}
476
  **4. Role Fit and Potential**
477
+ - Analyze cultural fit, role readiness, and long-term growth potential with specific alignment to role requirements.
478
  **5. Recommendations**
479
+ - Provide prioritized development strategies (e.g., communication training, technical assessments).
480
+ - Suggest specific next steps for hiring managers (e.g., advance, schedule tests).
481
  """
482
  response = gemini_model.generate_content(prompt)
483
+ return re.sub(r'[^\x00-\x7F]+|[()]+', '', response.text) # Sanitize non-ASCII and parentheses
484
  except Exception as e:
485
  logger.error(f"Report generation failed: {str(e)}")
486
  return f"Error generating report: {str(e)}"
 
491
  rightMargin=0.75*inch, leftMargin=0.75*inch,
492
  topMargin=1*inch, bottomMargin=1*inch)
493
  styles = getSampleStyleSheet()
494
+ h1 = ParagraphStyle(name='Heading1', fontSize=18, leading=22, spaceAfter=16, alignment=1, textColor=colors.HexColor('#003087'), fontName='Helvetica-Bold')
495
+ h2 = ParagraphStyle(name='Heading2', fontSize=13, leading=15, spaceBefore=10, spaceAfter=6, textColor=colors.HexColor('#0050BC'), fontName='Helvetica-Bold')
496
+ h3 = ParagraphStyle(name='Heading3', fontSize=9, leading=11, spaceBefore=6, spaceAfter=4, textColor=colors.HexColor('#3F7CFF'), fontName='Helvetica')
497
+ body_text = ParagraphStyle(name='BodyText', fontSize=8, leading=10, spaceAfter=4, fontName='Helvetica', textColor=colors.HexColor('#333333'))
498
+ bullet_style = ParagraphStyle(name='Bullet', parent=body_text, leftIndent=16, bulletIndent=6, fontName='Helvetica', bulletFontName='Helvetica', bulletFontSize=8)
499
 
500
  story = []
501
 
502
  def header_footer(canvas, doc):
503
  canvas.saveState()
504
+ canvas.setFont('Helvetica', 7)
505
  canvas.setFillColor(colors.HexColor('#666666'))
506
  canvas.drawString(doc.leftMargin, 0.5*inch, f"Page {doc.page} | EvalBot HR Interview Report | Confidential")
507
  canvas.setStrokeColor(colors.HexColor('#0050BC'))
508
+ canvas.setLineWidth(0.5)
509
  canvas.line(doc.leftMargin, doc.height + 0.9*inch, doc.width + doc.leftMargin, doc.height + 0.9*inch)
510
+ canvas.setFont('Helvetica-Bold', 8)
511
  canvas.drawString(doc.leftMargin, doc.height + 0.95*inch, "Candidate Interview Analysis")
512
  canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.95*inch, time.strftime('%B %d, %Y'))
513
  canvas.restoreState()
514
 
515
  # Title Page
516
  story.append(Paragraph("Candidate Interview Analysis", h1))
517
+ story.append(Paragraph(f"Generated: {time.strftime('%B %d, %Y')}", ParagraphStyle(name='Date', alignment=1, fontSize=8, textColor=colors.HexColor('#666666'), fontName='Helvetica')))
518
+ story.append(Spacer(1, 0.3*inch))
519
  acceptance_prob = analysis_data.get('acceptance_probability', 50.0)
520
  story.append(Paragraph("Hiring Suitability Snapshot", h2))
521
  prob_color = colors.HexColor('#2E7D32') if acceptance_prob >= 80 else (colors.HexColor('#F57C00') if acceptance_prob >= 60 else colors.HexColor('#D32F2F'))
522
+ story.append(Paragraph(f"Suitability Score: <font size=14 color='{prob_color.hexval()}'><b>{acceptance_prob:.2f}%</b></font>",
523
+ ParagraphStyle(name='Prob', fontSize=10, spaceAfter=8, alignment=1, fontName='Helvetica-Bold')))
524
  if acceptance_prob >= 80:
525
  story.append(Paragraph("<b>HR Verdict:</b> Outstanding candidate, recommended for immediate advancement.", body_text))
526
  elif acceptance_prob >= 60:
 
529
  story.append(Paragraph("<b>HR Verdict:</b> Moderate potential, needs additional assessment.", body_text))
530
  else:
531
  story.append(Paragraph("<b>HR Verdict:</b> Limited fit, significant improvement required.", body_text))
532
+ story.append(Spacer(1, 0.2*inch))
533
+ participants = sorted([p for p in set(u['speaker'] for u in analysis_data['transcript']) if p != 'Unknown'])
534
  table_data = [
535
  ['Metric', 'Value'],
536
  ['Interview Duration', f"{analysis_data['text_analysis']['total_duration']:.2f} seconds"],
537
  ['Speaker Turns', f"{analysis_data['text_analysis']['speaker_turns']}"],
538
+ ['Participants', ', '.join(participants)],
539
  ]
540
+ table = Table(table_data, colWidths=[2.2*inch, 3.8*inch])
541
  table.setStyle(TableStyle([
542
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
543
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
544
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
545
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
546
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
547
+ ('FONTSIZE', (0,0), (-1,-1), 8),
548
+ ('BOTTOMPADDING', (0,0), (-1,0), 6),
549
+ ('TOPPADDING', (0,0), (-1,0), 6),
550
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
551
+ ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
552
  ]))
553
  story.append(table)
554
+ story.append(Spacer(1, 0.3*inch))
555
  story.append(Paragraph("Prepared by: EvalBot - AI-Powered HR Analysis", body_text))
556
  story.append(PageBreak())
557
 
 
570
  ['Confidence', voice_analysis.get('interpretation', {}).get('confidence_level', 'N/A'), f"Score: {voice_analysis.get('composite_scores', {}).get('confidence', 0):.3f}"],
571
  ['Fluency', voice_analysis.get('interpretation', {}).get('fluency_level', 'N/A'), 'Drives engagement'],
572
  ]
573
+ table = Table(table_data, colWidths=[1.5*inch, 1.3*inch, 3.2*inch])
574
  table.setStyle(TableStyle([
575
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0050BC')),
576
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
577
  ('ALIGN', (0,0), (-1,-1), 'LEFT'),
578
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
579
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
580
+ ('FONTSIZE', (0,0), (-1,-1), 8),
581
+ ('BOTTOMPADDING', (0,0), (-1,0), 6),
582
+ ('TOPPADDING', (0,0), (-1,0), 6),
583
  ('BACKGROUND', (0,1), (-1,-1), colors.HexColor('#F5F6FA')),
584
+ ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#DDE4EB')),
585
  ]))
586
  story.append(table)
587
+ story.append(Spacer(1, 0.15*inch))
588
  chart_buffer = io.BytesIO()
589
  generate_anxiety_confidence_chart(voice_analysis.get('composite_scores', {}), chart_buffer)
590
  chart_buffer.seek(0)
591
+ img = Image(chart_buffer, width=4.2*inch, height=2.8*inch)
592
  img.hAlign = 'CENTER'
593
  story.append(img)
594
  else:
595
+ story.append(Paragraph("Vocal analysis unavailable due to processing limitations.", body_text))
596
+ story.append(Spacer(1, 0.15*inch))
597
 
598
  # Parse Gemini Report
599
  sections = {
 
609
  for line in lines:
610
  line = line.strip()
611
  if not line: continue
 
612
  if line.startswith('**') and line.endswith('**'):
613
  section_title = line.strip('**').strip()
614
  if section_title.startswith(('1.', '2.', '3.', '4.', '5.')):
 
631
  elif line.startswith(('-', '*', '•')) and current_section:
632
  clean_line = line.lstrip('-*• ').strip()
633
  if not clean_line: continue
634
+ clean_line = re.sub(r'[()]', '', clean_line)
635
  if current_section == 'Competency':
636
  if any(k in clean_line.lower() for k in ['leader', 'problem', 'commun', 'adapt', 'strength']):
637
  current_subsection = 'Strengths'
 
656
  story.append(Paragraph(line, bullet_style))
657
  else:
658
  story.append(Paragraph("No summary provided.", body_text))
659
+ story.append(Spacer(1, 0.15*inch))
660
 
661
  # Competency and Content
662
+ story.append(Paragraph("3. Competency & Content", h2))
663
  story.append(Paragraph("Strengths", h3))
664
  if sections['Competency']['Strengths']:
665
  for line in sections['Competency']['Strengths']:
 
673
  story.append(Paragraph(line, bullet_style))
674
  else:
675
  story.append(Paragraph("No growth areas identified; maintain current strengths.", body_text))
676
+ story.append(Spacer(1, 0.15*inch))
677
 
678
  # Role Fit
679
  story.append(Paragraph("4. Role Fit & Potential", h2))
 
682
  story.append(Paragraph(line, bullet_style))
683
  else:
684
  story.append(Paragraph("No fit analysis provided.", body_text))
685
+ story.append(Spacer(1, 0.15*inch))
686
 
687
  # Recommendations
688
  story.append(Paragraph("5. Recommendations", h2))
 
693
  else:
694
  story.append(Paragraph("No development priorities specified.", body_text))
695
  story.append(Spacer(1, 0.1*inch))
696
+ story.append(Paragraph("Next Steps for Hiring Managers", h3))
697
  if sections['Recommendations']['Next Steps']:
698
  for line in sections['Recommendations']['Next Steps']:
699
  story.append(Paragraph(line, bullet_style))
700
  else:
701
  story.append(Paragraph("No next steps provided.", body_text))
702
+ story.append(Spacer(1, 0.15*inch))
703
+ story.append(Paragraph("This report provides actionable insights to support hiring and candidate development.", body_text))
704
 
705
  doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
706
  logger.info(f"PDF report successfully generated at {output_path}")
 
716
  if isinstance(obj, np.ndarray): return obj.tolist()
717
  return obj
718
 
719
+ def process_interview(audio_url: str) -> Dict:
720
+ """Process a single audio URL and generate analysis report."""
721
  local_audio_path = None
722
  wav_file = None
723
  is_downloaded = False
724
  try:
725
+ if not isinstance(audio_url, str):
726
+ raise ValueError("Input must be a single URL string")
727
+ logger.info(f"Starting processing for {audio_url}")
728
+ if audio_url.startswith(('http://', 'https://')):
729
+ local_audio_path = download_audio_from_url(audio_url)
730
  is_downloaded = True
731
  else:
732
+ local_audio_path = audio_url
733
  if not os.path.exists(local_audio_path):
734
  raise FileNotFoundError(f"Local audio file not found: {local_audio_path}")
735
  wav_file = convert_to_wav(local_audio_path)
 
765
  serializable_data = convert_to_serializable(analysis_data)
766
  json.dump(serializable_data, f, indent=2)
767
  if not pdf_success:
768
+ logger.warning(f"PDF report failed to generate for {audio_url}")
769
  return {
770
  'pdf_path': None,
771
  'json_path': json_path,
772
  'error': 'PDF generation failed'
773
  }
774
+ logger.info(f"Processing completed for {audio_url}")
775
  return {'pdf_path': pdf_path, 'json_path': json_path}
776
  except Exception as e:
777
+ logger.error(f"Processing failed for {audio_url}: {str(e)}", exc_info=True)
778
  base_name = str(uuid.uuid4())
779
  json_path = os.path.join(OUTPUT_DIR, f"{base_name}_analysis.json")
780
  with open(json_path, 'w') as f: