husseinelsaadi commited on
Commit
c11e18e
·
1 Parent(s): a746471
Files changed (1) hide show
  1. backend/services/report_generator.py +197 -136
backend/services/report_generator.py CHANGED
@@ -153,183 +153,194 @@ from typing import List, Tuple
153
  import textwrap
154
 
155
  def create_pdf_report(report_text: str) -> BytesIO:
156
- """Convert a formatted report into a visually appealing PDF with enhanced formatting."""
 
 
 
 
 
 
157
  buffer = BytesIO()
158
 
159
- # Configuration
160
  PAGE_WIDTH = 8.27 # A4 width in inches
161
  PAGE_HEIGHT = 11.69 # A4 height in inches
162
- MARGIN_LEFT = 0.75
163
- MARGIN_RIGHT = 0.75
164
- MARGIN_TOP = 1.0
165
- MARGIN_BOTTOM = 1.0
166
-
167
- # Calculate usable width for text
168
- usable_width = PAGE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT
169
- chars_per_inch = 12 # Approximate for 10pt font
170
- wrap_width = int(usable_width * chars_per_inch)
171
 
172
- # Text styling
173
- FONT_SIZE_NORMAL = 10
174
- FONT_SIZE_HEADING = 12
175
- LINE_HEIGHT = 0.02 # Relative to page height
176
- QUESTION_COLOR = '#2C3E50' # Dark blue-gray
177
- ANSWER_COLOR = '#34495E' # Slightly lighter
178
- SCORE_COLOR = '#27AE60' # Green
179
- FEEDBACK_COLOR = '#E74C3C' # Red
180
 
181
- # Prepare formatted content
182
- raw_lines = report_text.split("\n")
183
- wrapper = textwrap.TextWrapper(width=wrap_width, break_long_words=False, replace_whitespace=False)
 
 
 
 
 
 
184
 
185
- formatted_content = []
 
 
 
186
 
187
- for line in raw_lines:
188
- stripped = line.strip()
189
-
190
- if stripped.startswith("Question"):
191
- # Add spacing before questions (except the first one)
192
- if formatted_content:
193
- formatted_content.append({"text": "", "style": "normal"})
194
 
195
- # Question with special formatting
196
- formatted_content.append({
197
- "text": stripped,
198
- "style": "question",
199
- "color": QUESTION_COLOR,
200
- "bold": True,
201
- "size": FONT_SIZE_HEADING
 
 
 
202
  })
203
-
204
- elif stripped.startswith("Answer:"):
205
- # Extract and wrap answer text
206
- answer_text = stripped.replace("Answer:", "", 1).strip()
207
- wrapped_lines = wrapper.wrap(f"Answer: {answer_text}") if answer_text else ["Answer:"]
208
-
209
- for idx, wrapped_line in enumerate(wrapped_lines):
210
- formatted_content.append({
211
- "text": " " + wrapped_line if idx == 0 else " " + wrapped_line,
212
- "style": "answer",
213
- "color": ANSWER_COLOR,
214
- "size": FONT_SIZE_NORMAL
215
  })
216
-
217
- elif stripped.startswith("Score:"):
218
- formatted_content.append({
219
- "text": f" {stripped}",
220
- "style": "score",
221
- "color": SCORE_COLOR,
222
- "bold": True,
223
- "size": FONT_SIZE_NORMAL
224
  })
225
-
226
- elif stripped.startswith("Feedback:"):
227
- # Wrap feedback text
228
- feedback_text = stripped.replace("Feedback:", "", 1).strip()
229
- wrapped_lines = wrapper.wrap(f"Feedback: {feedback_text}") if feedback_text else ["Feedback:"]
230
-
231
- for idx, wrapped_line in enumerate(wrapped_lines):
232
- formatted_content.append({
233
- "text": " " + wrapped_line if idx == 0 else " " + wrapped_line,
234
- "style": "feedback",
235
- "color": FEEDBACK_COLOR,
236
- "size": FONT_SIZE_NORMAL
237
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  else:
239
  # Regular text
240
- if stripped:
241
- wrapped_lines = wrapper.wrap(line)
242
- for wrapped_line in wrapped_lines:
243
- formatted_content.append({
244
- "text": wrapped_line,
245
- "style": "normal",
246
- "color": "black",
247
- "size": FONT_SIZE_NORMAL
248
- })
249
- else:
250
- formatted_content.append({"text": "", "style": "normal"})
251
-
252
- # Calculate lines per page based on actual line height
253
- usable_height = PAGE_HEIGHT - MARGIN_TOP - MARGIN_BOTTOM
254
- lines_per_page = int(usable_height / (LINE_HEIGHT * PAGE_HEIGHT))
255
 
256
- # Create PDF
257
  with PdfPages(buffer) as pdf:
258
- page_start = 0
259
- page_num = 1
260
 
261
- while page_start < len(formatted_content):
262
- # Create figure
263
  fig = plt.figure(figsize=(PAGE_WIDTH, PAGE_HEIGHT))
264
  fig.patch.set_facecolor('white')
265
  ax = fig.add_subplot(111)
266
  ax.axis('off')
 
 
267
 
268
  # Add subtle page border
269
  border = mpatches.Rectangle(
270
- (0.5, 0.5), PAGE_WIDTH - 1, PAGE_HEIGHT - 1,
271
- fill=False, edgecolor='#BDC3C7', linewidth=0.5
 
 
 
 
272
  )
273
  ax.add_patch(border)
274
 
275
- # Add page content
276
- y_position = 1 - MARGIN_TOP / PAGE_HEIGHT
277
  lines_on_page = 0
278
 
279
- for idx in range(page_start, min(page_start + lines_per_page, len(formatted_content))):
280
- item = formatted_content[idx]
 
281
 
282
- # Apply text styling
283
- weight = 'bold' if item.get('bold', False) else 'normal'
284
- size = item.get('size', FONT_SIZE_NORMAL)
285
- color = item.get('color', 'black')
 
 
 
 
286
 
287
- # Add text to page
288
  ax.text(
289
- MARGIN_LEFT / PAGE_WIDTH,
290
- y_position,
291
- item['text'],
292
  transform=ax.transAxes,
293
  fontsize=size,
294
  fontweight=weight,
295
  color=color,
296
- fontfamily='DejaVu Sans',
297
  verticalalignment='top'
298
  )
299
 
300
  # Move to next line
301
- y_position -= LINE_HEIGHT
 
302
  lines_on_page += 1
303
-
304
- # Check if we need a new page (with some buffer)
305
- if y_position < MARGIN_BOTTOM / PAGE_HEIGHT + 0.05:
306
- break
307
 
308
- # Add page number
 
309
  ax.text(
310
  0.5,
311
- MARGIN_BOTTOM / PAGE_HEIGHT / 2,
312
- f"Page {page_num}",
313
  transform=ax.transAxes,
314
  fontsize=9,
315
- color='#7F8C8D',
316
  horizontalalignment='center'
317
  )
318
 
319
  # Save page
320
- pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
321
  plt.close(fig)
322
-
323
- # Move to next page
324
- page_start += lines_on_page
325
- page_num += 1
326
 
327
  buffer.seek(0)
328
  return buffer
329
 
 
330
  def create_pdf_report_advanced(report_text: str) -> BytesIO:
331
  """
332
- Alternative implementation using reportlab for better PDF generation.
 
333
  Install with: pip install reportlab
334
  """
335
  try:
@@ -338,8 +349,11 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
338
  from reportlab.lib.units import inch
339
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
340
  from reportlab.lib.colors import HexColor
 
341
 
342
  buffer = BytesIO()
 
 
343
  doc = SimpleDocTemplate(
344
  buffer,
345
  pagesize=A4,
@@ -349,43 +363,68 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
349
  bottomMargin=1*inch
350
  )
351
 
352
- # Create custom styles
353
  styles = getSampleStyleSheet()
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  question_style = ParagraphStyle(
356
  'Question',
357
  parent=styles['Heading2'],
358
- fontSize=12,
359
- textColor=HexColor('#2C3E50'),
 
360
  spaceAfter=6,
361
- spaceBefore=12
362
  )
363
 
 
364
  answer_style = ParagraphStyle(
365
  'Answer',
366
  parent=styles['Normal'],
367
  fontSize=10,
368
- textColor=HexColor('#34495E'),
369
  leftIndent=20,
370
- spaceAfter=3
371
  )
372
 
 
373
  score_style = ParagraphStyle(
374
  'Score',
375
  parent=styles['Normal'],
376
  fontSize=10,
377
- textColor=HexColor('#27AE60'),
378
  leftIndent=20,
379
- fontName='Helvetica-Bold'
 
380
  )
381
 
 
382
  feedback_style = ParagraphStyle(
383
  'Feedback',
384
  parent=styles['Normal'],
385
  fontSize=10,
386
- textColor=HexColor('#E74C3C'),
387
  leftIndent=20,
388
- spaceAfter=6
389
  )
390
 
391
  # Build document content
@@ -395,7 +434,19 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
395
  for line in lines:
396
  stripped = line.strip()
397
 
398
- if stripped.startswith('Question'):
 
 
 
 
 
 
 
 
 
 
 
 
399
  story.append(Paragraph(stripped, question_style))
400
  elif stripped.startswith('Answer:'):
401
  story.append(Paragraph(stripped, answer_style))
@@ -403,18 +454,28 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
403
  story.append(Paragraph(stripped, score_style))
404
  elif stripped.startswith('Feedback:'):
405
  story.append(Paragraph(stripped, feedback_style))
406
- elif stripped:
407
- story.append(Paragraph(stripped, styles['Normal']))
408
  else:
409
- story.append(Spacer(1, 12))
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  # Build PDF
412
  doc.build(story)
413
  buffer.seek(0)
414
  return buffer
415
-
416
  except ImportError:
417
- # Fallback to matplotlib version
 
418
  return create_pdf_report(report_text)
419
-
420
  __all__ = ['generate_llm_interview_report', 'create_pdf_report']
 
153
  import textwrap
154
 
155
  def create_pdf_report(report_text: str) -> BytesIO:
156
+ """Convert a formatted report into a clean, well-organized PDF."""
157
+ from matplotlib.backends.backend_pdf import PdfPages
158
+ import matplotlib.pyplot as plt
159
+ import matplotlib.patches as mpatches
160
+ from io import BytesIO
161
+ import textwrap
162
+
163
  buffer = BytesIO()
164
 
165
+ # Page configuration - A4 size
166
  PAGE_WIDTH = 8.27 # A4 width in inches
167
  PAGE_HEIGHT = 11.69 # A4 height in inches
168
+ MARGIN = 0.75 # Uniform margins
 
 
 
 
 
 
 
 
169
 
170
+ # Text configuration
171
+ CHARS_PER_LINE = 80 # Characters per line for wrapping
172
+ LINES_PER_PAGE = 45 # Lines that fit on one page
 
 
 
 
 
173
 
174
+ # Colors
175
+ COLORS = {
176
+ 'question': '#1e3a8a', # Dark blue
177
+ 'answer': '#374151', # Dark gray
178
+ 'score': '#059669', # Green
179
+ 'feedback': '#dc2626', # Red
180
+ 'header': '#111827', # Almost black
181
+ 'normal': '#374151' # Gray
182
+ }
183
 
184
+ # Process the text
185
+ lines = report_text.split('\n')
186
+ processed_lines = []
187
+ wrapper = textwrap.TextWrapper(width=CHARS_PER_LINE, break_long_words=False)
188
 
189
+ for line in lines:
190
+ if not line.strip():
191
+ processed_lines.append({'text': '', 'type': 'blank'})
192
+ continue
 
 
 
193
 
194
+ # Identify line type
195
+ if line.strip().startswith('Question'):
196
+ # Add spacing before questions (except first)
197
+ if processed_lines and processed_lines[-1]['type'] != 'blank':
198
+ processed_lines.append({'text': '', 'type': 'blank'})
199
+ processed_lines.append({
200
+ 'text': line.strip(),
201
+ 'type': 'question',
202
+ 'size': 11,
203
+ 'bold': True
204
  })
205
+ elif line.strip().startswith('Answer:'):
206
+ wrapped = wrapper.wrap(line.strip())
207
+ for i, wrapped_line in enumerate(wrapped):
208
+ processed_lines.append({
209
+ 'text': (' ' + wrapped_line) if i == 0 else (' ' + wrapped_line),
210
+ 'type': 'answer',
211
+ 'size': 10
 
 
 
 
 
212
  })
213
+ elif line.strip().startswith('Score:'):
214
+ processed_lines.append({
215
+ 'text': ' ' + line.strip(),
216
+ 'type': 'score',
217
+ 'size': 10,
218
+ 'bold': True
 
 
219
  })
220
+ elif line.strip().startswith('Feedback:'):
221
+ wrapped = wrapper.wrap(line.strip())
222
+ for i, wrapped_line in enumerate(wrapped):
223
+ processed_lines.append({
224
+ 'text': (' ' + wrapped_line) if i == 0 else (' ' + wrapped_line),
225
+ 'type': 'feedback',
226
+ 'size': 10
 
 
 
 
 
227
  })
228
+ elif any(line.strip().startswith(x) for x in ['Interview Report', 'Candidate Name:', 'Candidate Email:',
229
+ 'Job Applied:', 'Company:', 'Date Applied:',
230
+ 'Skills Match Summary:', 'Interview Transcript']):
231
+ # Headers and metadata
232
+ if 'Interview Report' in line:
233
+ processed_lines.append({
234
+ 'text': line.strip(),
235
+ 'type': 'header',
236
+ 'size': 14,
237
+ 'bold': True
238
+ })
239
+ processed_lines.append({'text': '=' * 50, 'type': 'header', 'size': 10})
240
+ else:
241
+ wrapped = wrapper.wrap(line.strip())
242
+ for wrapped_line in enumerate(wrapped):
243
+ processed_lines.append({
244
+ 'text': wrapped_line[1],
245
+ 'type': 'header',
246
+ 'size': 10,
247
+ 'bold': True if ':' in wrapped_line[1] and wrapped_line[0] == 0 else False
248
+ })
249
  else:
250
  # Regular text
251
+ wrapped = wrapper.wrap(line)
252
+ for wrapped_line in wrapped:
253
+ processed_lines.append({
254
+ 'text': wrapped_line,
255
+ 'type': 'normal',
256
+ 'size': 10
257
+ })
 
 
 
 
 
 
 
 
258
 
259
+ # Create PDF with consistent pages
260
  with PdfPages(buffer) as pdf:
261
+ page_count = 0
262
+ line_index = 0
263
 
264
+ while line_index < len(processed_lines):
265
+ # Create new page
266
  fig = plt.figure(figsize=(PAGE_WIDTH, PAGE_HEIGHT))
267
  fig.patch.set_facecolor('white')
268
  ax = fig.add_subplot(111)
269
  ax.axis('off')
270
+ ax.set_xlim(0, 1)
271
+ ax.set_ylim(0, 1)
272
 
273
  # Add subtle page border
274
  border = mpatches.Rectangle(
275
+ (MARGIN/PAGE_WIDTH, MARGIN/PAGE_HEIGHT),
276
+ 1 - 2*MARGIN/PAGE_WIDTH,
277
+ 1 - 2*MARGIN/PAGE_HEIGHT,
278
+ fill=False,
279
+ edgecolor='#e5e7eb',
280
+ linewidth=1
281
  )
282
  ax.add_patch(border)
283
 
284
+ # Current y position (start from top)
285
+ y_pos = 1 - MARGIN/PAGE_HEIGHT - 0.05
286
  lines_on_page = 0
287
 
288
+ # Add content to page
289
+ while line_index < len(processed_lines) and lines_on_page < LINES_PER_PAGE:
290
+ line_data = processed_lines[line_index]
291
 
292
+ # Skip if too close to bottom
293
+ if y_pos < MARGIN/PAGE_HEIGHT + 0.05:
294
+ break
295
+
296
+ # Set text properties
297
+ color = COLORS.get(line_data['type'], COLORS['normal'])
298
+ size = line_data.get('size', 10)
299
+ weight = 'bold' if line_data.get('bold', False) else 'normal'
300
 
301
+ # Add text
302
  ax.text(
303
+ MARGIN/PAGE_WIDTH + 0.02,
304
+ y_pos,
305
+ line_data['text'],
306
  transform=ax.transAxes,
307
  fontsize=size,
308
  fontweight=weight,
309
  color=color,
310
+ fontfamily='sans-serif',
311
  verticalalignment='top'
312
  )
313
 
314
  # Move to next line
315
+ line_height = 0.018 if line_data['type'] == 'blank' else 0.022
316
+ y_pos -= line_height
317
  lines_on_page += 1
318
+ line_index += 1
 
 
 
319
 
320
+ # Add page number at bottom
321
+ page_count += 1
322
  ax.text(
323
  0.5,
324
+ MARGIN/PAGE_HEIGHT - 0.03,
325
+ f'Page {page_count}',
326
  transform=ax.transAxes,
327
  fontsize=9,
328
+ color='#9ca3af',
329
  horizontalalignment='center'
330
  )
331
 
332
  # Save page
333
+ pdf.savefig(fig, bbox_inches='tight', pad_inches=0.1)
334
  plt.close(fig)
 
 
 
 
335
 
336
  buffer.seek(0)
337
  return buffer
338
 
339
+
340
  def create_pdf_report_advanced(report_text: str) -> BytesIO:
341
  """
342
+ Alternative implementation using reportlab for professional PDF generation.
343
+ This creates cleaner, more consistent PDFs with better text handling.
344
  Install with: pip install reportlab
345
  """
346
  try:
 
349
  from reportlab.lib.units import inch
350
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
351
  from reportlab.lib.colors import HexColor
352
+ from reportlab.lib.enums import TA_LEFT, TA_CENTER
353
 
354
  buffer = BytesIO()
355
+
356
+ # Create document with consistent margins
357
  doc = SimpleDocTemplate(
358
  buffer,
359
  pagesize=A4,
 
363
  bottomMargin=1*inch
364
  )
365
 
366
+ # Define consistent styles
367
  styles = getSampleStyleSheet()
368
 
369
+ # Title style
370
+ title_style = ParagraphStyle(
371
+ 'CustomTitle',
372
+ parent=styles['Heading1'],
373
+ fontSize=16,
374
+ textColor=HexColor('#111827'),
375
+ spaceAfter=12,
376
+ alignment=TA_CENTER
377
+ )
378
+
379
+ # Header style for metadata
380
+ header_style = ParagraphStyle(
381
+ 'Header',
382
+ parent=styles['Normal'],
383
+ fontSize=10,
384
+ textColor=HexColor('#111827'),
385
+ spaceAfter=4
386
+ )
387
+
388
+ # Question style
389
  question_style = ParagraphStyle(
390
  'Question',
391
  parent=styles['Heading2'],
392
+ fontSize=11,
393
+ textColor=HexColor('#1e3a8a'),
394
+ spaceBefore=12,
395
  spaceAfter=6,
396
+ fontName='Helvetica-Bold'
397
  )
398
 
399
+ # Answer style
400
  answer_style = ParagraphStyle(
401
  'Answer',
402
  parent=styles['Normal'],
403
  fontSize=10,
404
+ textColor=HexColor('#374151'),
405
  leftIndent=20,
406
+ spaceAfter=4
407
  )
408
 
409
+ # Score style
410
  score_style = ParagraphStyle(
411
  'Score',
412
  parent=styles['Normal'],
413
  fontSize=10,
414
+ textColor=HexColor('#059669'),
415
  leftIndent=20,
416
+ fontName='Helvetica-Bold',
417
+ spaceAfter=4
418
  )
419
 
420
+ # Feedback style
421
  feedback_style = ParagraphStyle(
422
  'Feedback',
423
  parent=styles['Normal'],
424
  fontSize=10,
425
+ textColor=HexColor('#dc2626'),
426
  leftIndent=20,
427
+ spaceAfter=8
428
  )
429
 
430
  # Build document content
 
434
  for line in lines:
435
  stripped = line.strip()
436
 
437
+ if not stripped:
438
+ story.append(Spacer(1, 6))
439
+ elif 'Interview Report' in stripped:
440
+ story.append(Paragraph(stripped, title_style))
441
+ story.append(Spacer(1, 12))
442
+ elif any(stripped.startswith(x) for x in ['Candidate Name:', 'Candidate Email:',
443
+ 'Job Applied:', 'Company:', 'Date Applied:']):
444
+ story.append(Paragraph(stripped, header_style))
445
+ elif stripped.startswith('Skills Match Summary:') or stripped.startswith('Interview Transcript'):
446
+ story.append(Spacer(1, 12))
447
+ story.append(Paragraph(f"<b>{stripped}</b>", header_style))
448
+ story.append(Spacer(1, 6))
449
+ elif stripped.startswith('Question'):
450
  story.append(Paragraph(stripped, question_style))
451
  elif stripped.startswith('Answer:'):
452
  story.append(Paragraph(stripped, answer_style))
 
454
  story.append(Paragraph(stripped, score_style))
455
  elif stripped.startswith('Feedback:'):
456
  story.append(Paragraph(stripped, feedback_style))
 
 
457
  else:
458
+ # Regular text with proper indentation for sub-items
459
+ if stripped.startswith(' '):
460
+ indent_style = ParagraphStyle(
461
+ 'Indented',
462
+ parent=styles['Normal'],
463
+ fontSize=10,
464
+ leftIndent=20,
465
+ spaceAfter=2
466
+ )
467
+ story.append(Paragraph(stripped, indent_style))
468
+ else:
469
+ story.append(Paragraph(stripped, styles['Normal']))
470
 
471
  # Build PDF
472
  doc.build(story)
473
  buffer.seek(0)
474
  return buffer
475
+
476
  except ImportError:
477
+ # Fallback to matplotlib version if reportlab not available
478
+ print("Reportlab not installed. Using matplotlib version.")
479
  return create_pdf_report(report_text)
480
+
481
  __all__ = ['generate_llm_interview_report', 'create_pdf_report']