satyaki-mitra commited on
Commit
520de88
Β·
1 Parent(s): ffe6715

pdf_generator function fixed

Browse files
Files changed (1) hide show
  1. reporter/report_generator.py +592 -256
reporter/report_generator.py CHANGED
@@ -82,6 +82,18 @@ class ReportGenerator:
82
  # Convert DetectionResult to dict for consistent access
83
  detection_dict = detection_result.to_dict() if hasattr(detection_result, 'to_dict') else detection_result
84
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # Generate detailed reasoning
86
  reasoning = self.reasoning_generator.generate(ensemble_result = detection_result.ensemble_result,
87
  metric_results = detection_result.metric_results,
@@ -91,7 +103,7 @@ class ReportGenerator:
91
  )
92
 
93
  # Extract detailed metrics from ACTUAL detection results
94
- detailed_metrics = self._extract_detailed_metrics(detection_dict)
95
 
96
  # Timestamp for filenames
97
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -100,7 +112,8 @@ class ReportGenerator:
100
 
101
  # Generate requested formats
102
  if ("json" in formats):
103
- json_path = self._generate_json_report(detection_dict = detection_dict,
 
104
  reasoning = reasoning,
105
  detailed_metrics = detailed_metrics,
106
  attribution_result = attribution_result,
@@ -111,7 +124,8 @@ class ReportGenerator:
111
 
112
  if ("pdf" in formats):
113
  try:
114
- pdf_path = self._generate_pdf_report(detection_dict = detection_dict,
 
115
  reasoning = reasoning,
116
  detailed_metrics = detailed_metrics,
117
  attribution_result = attribution_result,
@@ -129,44 +143,56 @@ class ReportGenerator:
129
  return generated_files
130
 
131
 
132
- def _extract_detailed_metrics(self, detection_dict: Dict) -> List[DetailedMetric]:
133
  """
134
  Extract detailed metrics with sub-metrics from ACTUAL detection result
135
  """
136
  detailed_metrics = list()
137
- metrics_data = detection_dict.get("metrics", {})
138
- ensemble_data = detection_dict.get("ensemble", {})
139
 
140
  # Get actual metric weights from ensemble
141
  metric_weights = ensemble_data.get("metric_contributions", {})
142
 
 
 
 
 
143
  # Extract actual metric data
144
  for metric_name, metric_result in metrics_data.items():
145
- if not isinstance(metric_result, dict):
 
146
  continue
147
 
148
- if metric_result.get("error") is not None:
 
149
  continue
150
 
151
  # Get actual probabilities and confidence
152
- ai_prob = metric_result.get("ai_probability", 0) * 100
153
- human_prob = metric_result.get("human_probability", 0) * 100
154
- confidence = metric_result.get("confidence", 0) * 100
 
 
 
155
 
156
  # Determine verdict based on actual probability
157
- if (ai_prob >= 60):
 
158
  verdict = "AI"
159
-
160
- elif (ai_prob <= 40):
 
161
  verdict = "HUMAN"
162
 
163
  else:
164
- verdict = "MIXED (AI + HUMAN)"
165
 
166
  # Get actual weight or use default
167
  weight = 0.0
 
168
  if metric_name in metric_weights:
169
- weight = metric_weights[metric_name].get("weight", 0.0) * 100
170
 
171
  # Extract actual detailed metrics from metric result
172
  detailed_metrics_data = self._extract_metric_details(metric_name = metric_name,
@@ -177,16 +203,17 @@ class ReportGenerator:
177
  description = self._get_metric_description(metric_name = metric_name)
178
 
179
  detailed_metrics.append(DetailedMetric(name = metric_name,
180
- ai_probability = ai_prob,
181
- human_probability = human_prob,
182
- confidence = confidence,
183
  verdict = verdict,
184
  description = description,
185
  detailed_metrics = detailed_metrics_data,
186
- weight = weight,
187
  )
188
  )
189
 
 
190
  return detailed_metrics
191
 
192
 
@@ -226,7 +253,7 @@ class ReportGenerator:
226
  return descriptions.get(metric_name, "Advanced text analysis metric.")
227
 
228
 
229
- def _generate_json_report(self, detection_dict: Dict, reasoning: DetailedReasoning, detailed_metrics: List[DetailedMetric],
230
  attribution_result: Optional[AttributionResult], highlighted_sentences: Optional[List] = None, filename: str = None) -> Path:
231
  """
232
  Generate JSON format report with detailed metrics
@@ -273,54 +300,54 @@ class ReportGenerator:
273
  }
274
 
275
  # Use ACTUAL detection results from dictionary
276
- ensemble_data = detection_dict.get("ensemble", {})
277
- analysis_data = detection_dict.get("analysis", {})
278
- metrics_data_dict = detection_dict.get("metrics", {})
279
- performance_data = detection_dict.get("performance", {})
280
-
281
- report_data = {"report_metadata" : {"generated_at" : datetime.now().isoformat(),
282
- "version" : "1.0.0",
283
- "format" : "json",
284
- "report_id" : filename.replace('.json', ''),
285
- },
286
- "overall_results" : {"final_verdict" : ensemble_data.get("final_verdict", "Unknown"),
287
- "ai_probability" : ensemble_data.get("ai_probability", 0),
288
- "human_probability" : ensemble_data.get("human_probability", 0),
289
- "mixed_probability" : ensemble_data.get("mixed_probability", 0),
290
- "overall_confidence" : ensemble_data.get("overall_confidence", 0),
291
- "uncertainty_score" : ensemble_data.get("uncertainty_score", 0),
292
- "consensus_level" : ensemble_data.get("consensus_level", 0),
293
- "domain" : analysis_data.get("domain", "general"),
294
- "domain_confidence" : analysis_data.get("domain_confidence", 0),
295
- "text_length" : analysis_data.get("text_length", 0),
296
- "sentence_count" : analysis_data.get("sentence_count", 0),
297
- },
298
- "ensemble_analysis" : {"method_used" : "confidence_calibrated",
299
- "metric_weights" : ensemble_data.get("metric_contributions", {}),
300
- "reasoning" : ensemble_data.get("reasoning", []),
301
- },
302
- "detailed_metrics" : metrics_data,
303
- "detection_reasoning" : {"summary" : reasoning.summary,
304
- "key_indicators" : reasoning.key_indicators,
305
- "metric_explanations" : reasoning.metric_explanations,
306
- "supporting_evidence" : reasoning.supporting_evidence,
307
- "contradicting_evidence" : reasoning.contradicting_evidence,
308
- "confidence_explanation" : reasoning.confidence_explanation,
309
- "domain_analysis" : reasoning.domain_analysis,
310
- "ensemble_analysis" : reasoning.ensemble_analysis,
311
- "uncertainty_analysis" : reasoning.uncertainty_analysis,
312
- "recommendations" : reasoning.recommendations,
313
- },
314
- "highlighted_text" : highlighted_data,
315
- "model_attribution" : attribution_data,
316
- "performance_metrics" : {"total_processing_time" : performance_data.get("total_time", 0),
317
- "metrics_execution_time" : performance_data.get("metrics_time", {}),
318
- "warnings" : detection_dict.get("warnings", []),
319
- "errors" : detection_dict.get("errors", []),
320
- }
321
- }
322
-
323
- output_path = self.output_dir / filename
324
 
325
  with open(output_path, 'w', encoding='utf-8') as f:
326
  json.dump(obj = report_data,
@@ -333,271 +360,580 @@ class ReportGenerator:
333
  return output_path
334
 
335
 
336
- def _generate_pdf_report(self, detection_dict: Dict, reasoning: DetailedReasoning, detailed_metrics: List[DetailedMetric],
337
  attribution_result: Optional[AttributionResult], highlighted_sentences: Optional[List] = None, filename: str = None) -> Path:
338
  """
339
  Generate PDF format report with detailed metrics
340
  """
341
  try:
342
  from reportlab.lib import colors
343
- from reportlab.lib.pagesizes import letter, A4
344
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
345
  from reportlab.lib.units import inch
346
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
347
- from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
348
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  except ImportError:
350
  raise ImportError("reportlab is required for PDF generation. Install: pip install reportlab")
351
 
352
  output_path = self.output_dir / filename
353
 
354
- # Create PDF
355
  doc = SimpleDocTemplate(str(output_path),
356
- pagesize = letter,
357
- rightMargin = 50,
358
- leftMargin = 50,
359
- topMargin = 50,
360
- bottomMargin = 20,
361
  )
362
 
363
  # Container for PDF elements
364
- elements = list()
365
- styles = getSampleStyleSheet()
366
-
367
- # Custom styles
368
- title_style = ParagraphStyle('CustomTitle',
369
- parent = styles['Heading1'],
370
- fontSize = 20,
371
- textColor = colors.HexColor('#667eea'),
372
- spaceAfter = 20,
373
- alignment = TA_CENTER,
374
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
- heading_style = ParagraphStyle('CustomHeading',
377
- parent = styles['Heading2'],
378
- fontSize = 14,
379
- textColor = colors.HexColor('#111827'),
380
- spaceAfter = 12,
381
- spaceBefore = 12,
382
- )
383
-
384
- body_style = ParagraphStyle('CustomBody',
385
- parent = styles['BodyText'],
386
- fontSize = 10,
387
- alignment = TA_JUSTIFY,
388
- spaceAfter = 8,
389
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- # Use detection results from dictionary
392
- ensemble_data = detection_dict.get("ensemble", {})
393
- analysis_data = detection_dict.get("analysis", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
  # Title and main sections
396
  elements.append(Paragraph("AI Text Detection Analysis Report", title_style))
397
- elements.append(Paragraph(f"Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", styles['Normal']))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  elements.append(Spacer(1, 0.3*inch))
399
 
400
- # Verdict section with ensemble metrics
401
- elements.append(Paragraph("Detection Summary", heading_style))
402
- verdict_data = [['Final Verdict:', ensemble_data.get("final_verdict", "Unknown")],
403
- ['AI Probability:', f"{ensemble_data.get('ai_probability', 0):.1%}"],
404
- ['Human Probability:', f"{ensemble_data.get('human_probability', 0):.1%}"],
405
- ['Mixed Probability:', f"{ensemble_data.get('mixed_probability', 0):.1%}"],
406
- ['Overall Confidence:', f"{ensemble_data.get('overall_confidence', 0):.1%}"],
407
- ['Uncertainty Score:', f"{ensemble_data.get('uncertainty_score', 0):.1%}"],
408
- ['Consensus Level:', f"{ensemble_data.get('consensus_level', 0):.1%}"],
409
- ]
410
-
411
- verdict_table = Table(verdict_data, colWidths=[2*inch, 3*inch])
412
- verdict_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8fafc')),
413
- ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
414
- ('FONTSIZE', (0, 0), (-1, -1), 10),
415
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
416
- ])
417
- )
418
 
419
- elements.append(verdict_table)
420
- elements.append(Spacer(1, 0.2*inch))
 
421
 
422
- # Content analysis
423
- elements.append(Paragraph("Content Analysis", heading_style))
424
- content_data = [['Content Domain:', analysis_data.get("domain", "general").title()],
425
- ['Domain Confidence:', f"{analysis_data.get('domain_confidence', 0):.1%}"],
426
- ['Word Count:', str(analysis_data.get("text_length", 0))],
427
- ['Sentence Count:', str(analysis_data.get("sentence_count", 0))],
428
- ['Processing Time:', f"{detection_dict.get('performance', {}).get('total_time', 0):.2f}s"],
429
- ]
430
-
431
- content_table = Table(content_data, colWidths=[2*inch, 3*inch])
432
- content_table.setStyle(TableStyle([('FONTSIZE', (0, 0), (-1, -1), 10),
433
- ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  ])
435
  )
436
-
437
  elements.append(content_table)
438
- elements.append(Spacer(1, 0.2*inch))
439
 
440
- # Ensemble Analysis
441
- elements.append(Paragraph("Ensemble Analysis", heading_style))
442
- elements.append(Paragraph("Method: Confidence Calibrated Aggregation", styles['Normal']))
443
- elements.append(Spacer(1, 0.1*inch))
444
 
445
- # Metric weights table
446
  metric_contributions = ensemble_data.get("metric_contributions", {})
447
- if metric_contributions:
448
- elements.append(Paragraph("Metric Weights", styles['Heading3']))
449
- weight_data = [['Metric', 'Weight']]
450
- for metric, contribution in metric_contributions.items():
451
- weight = contribution.get("weight", 0)
452
- weight_data.append([metric.title(), f"{weight:.1%}"])
 
 
 
 
 
 
 
 
453
 
454
- weight_table = Table(weight_data, colWidths=[3*inch, 1*inch])
455
- weight_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#667eea')),
456
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
457
  ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
458
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
459
  ('FONTSIZE', (0, 0), (-1, -1), 9),
460
- ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
461
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
 
 
 
462
  ])
463
- )
 
464
  elements.append(weight_table)
465
- elements.append(Spacer(1, 0.2*inch))
466
 
467
- # Detailed metrics
468
- elements.append(Paragraph("Detailed Metric Analysis", heading_style))
469
- for metric in detailed_metrics:
470
- elements.append(Paragraph(f"{metric.name.title().replace('_', ' ')}", styles['Heading3']))
471
- metric_data = [['Verdict:', metric.verdict],
472
- ['AI Probability:', f"{metric.ai_probability:.1f}%"],
473
- ['Human Probability:', f"{metric.human_probability:.1f}%"],
474
- ['Confidence:', f"{metric.confidence:.1f}%"],
475
- ['Ensemble Weight:', f"{metric.weight:.1f}%"],
476
- ]
477
-
478
- metric_table = Table(metric_data, colWidths=[1.5*inch, 1.5*inch])
479
- metric_table.setStyle(TableStyle([('FONTSIZE', (0, 0), (-1, -1), 9),
480
- ('BOTTOMPADDING', (0, 0), (-1, -1), 2),
481
- ])
482
- )
483
-
484
- elements.append(metric_table)
485
- elements.append(Paragraph(metric.description, body_style))
486
-
487
- # Add detailed sub-metrics if available
488
- if metric.detailed_metrics:
489
- elements.append(Paragraph("Detailed Metrics:", styles['Heading4']))
490
- sub_metric_data = [['Metric', 'Value']]
491
- for sub_name, sub_value in list(metric.detailed_metrics.items())[:6]: # Show top 6
492
- sub_metric_data.append([sub_name.replace('_', ' ').title(), f"{sub_value:.2f}"])
493
 
494
- sub_metric_table = Table(sub_metric_data, colWidths=[2*inch, 1*inch])
495
- sub_metric_table.setStyle(TableStyle([('FONTSIZE', (0, 0), (-1, -1), 8),
496
- ('BOTTOMPADDING', (0, 0), (-1, -1), 2),
497
- ('GRID', (0, 0), (-1, -1), 1, colors.grey),
498
- ])
499
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
- elements.append(sub_metric_table)
502
-
503
- elements.append(Spacer(1, 0.1*inch))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
  # Detection Reasoning
506
- elements.append(Paragraph("Detection Reasoning", heading_style))
507
- elements.append(Paragraph(reasoning.summary, body_style))
508
- elements.append(Spacer(1, 0.1*inch))
 
 
 
 
 
 
 
 
 
509
 
510
  # Key Indicators
511
- elements.append(Paragraph("Key Indicators", styles['Heading3']))
512
- for indicator in reasoning.key_indicators[:5]: # Show top 5
513
- elements.append(Paragraph(f"β€’ {indicator}", body_style))
514
-
515
- elements.append(Spacer(1, 0.1*inch))
516
-
517
- # Confidence Explanation
518
- elements.append(Paragraph("Confidence Analysis", styles['Heading3']))
519
- elements.append(Paragraph(reasoning.confidence_explanation, body_style))
520
- elements.append(Spacer(1, 0.1*inch))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
 
522
- # Uncertainty Analysis
523
- elements.append(Paragraph("Uncertainty Analysis", styles['Heading3']))
524
- elements.append(Paragraph(reasoning.uncertainty_analysis, body_style))
525
 
526
  # Model Attribution Section
527
  if attribution_result:
528
- elements.append(PageBreak())
529
- elements.append(Paragraph("AI Model Attribution", heading_style))
 
 
530
 
531
- # Attribution summary
532
- predicted_model = attribution_result.predicted_model.value.replace("_", " ").title()
533
- confidence = attribution_result.confidence * 100
 
534
 
535
- attribution_summary = [['Predicted Model:', predicted_model],
536
- ['Attribution Confidence:', f"{confidence:.1f}%"],
537
- ['Domain Used:', attribution_result.domain_used.value.title()],
538
- ]
539
 
540
- attribution_table = Table(attribution_summary, colWidths=[2*inch, 3*inch])
541
- attribution_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8fafc')),
542
  ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
543
- ('FONTSIZE', (0, 0), (-1, -1), 10),
544
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
 
 
545
  ])
546
  )
547
-
548
  elements.append(attribution_table)
549
- elements.append(Spacer(1, 0.1*inch))
550
 
551
  # Model probabilities table
552
  if attribution_result.model_probabilities:
553
- elements.append(Paragraph("Model Probability Breakdown", styles['Heading3']))
554
 
555
- prob_data = [['Model', 'Probability']]
556
 
557
- # Show top 5
558
- sorted_models = sorted(attribution_result.model_probabilities.items(),
559
- key = lambda x: x[1],
560
- reverse = True)[:5]
561
 
562
  for model_name, probability in sorted_models:
563
  display_name = model_name.replace("_", " ").replace("-", " ").title()
564
- prob_data.append([display_name, f"{probability:.1%}"])
565
-
566
- prob_table = Table(prob_data, colWidths=[3*inch, 1*inch])
567
- prob_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#667eea')),
 
 
 
 
 
 
568
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
569
  ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
 
570
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
571
  ('FONTSIZE', (0, 0), (-1, -1), 9),
572
- ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
573
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
 
 
 
574
  ])
575
  )
576
-
577
  elements.append(prob_table)
578
- elements.append(Spacer(1, 0.2*inch))
 
 
 
 
579
 
580
- # Attribution reasoning
581
- if attribution_result.reasoning:
582
- elements.append(Paragraph("Attribution Reasoning", styles['Heading3']))
583
- for reason in attribution_result.reasoning[:3]: # Show top 3 reasons
584
- elements.append(Paragraph(f"β€’ {reason}", body_style))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
- # Recommendations
587
- elements.append(PageBreak())
588
- elements.append(Paragraph("Recommendations", heading_style))
589
- for recommendation in reasoning.recommendations:
590
- elements.append(Paragraph(f"β€’ {recommendation}", body_style))
 
 
 
591
 
592
- # Footer
593
- elements.append(Spacer(1, 0.3*inch))
594
- elements.append(Paragraph(f"Generated by AI Text Detector v2.0 | Processing Time: {detection_dict.get('performance', {}).get('total_time', 0):.2f}s",
595
- ParagraphStyle('Footer', parent=styles['Normal'], fontSize=8, textColor=colors.gray)))
 
 
 
 
 
 
596
 
597
  # Build PDF
598
  doc.build(elements)
599
 
600
- logger.info(f"PDF report saved: {output_path}")
601
  return output_path
602
 
603
 
 
82
  # Convert DetectionResult to dict for consistent access
83
  detection_dict = detection_result.to_dict() if hasattr(detection_result, 'to_dict') else detection_result
84
 
85
+ # DEBUG: Check structure
86
+ logger.debug(f"detection_dict keys: {list(detection_dict.keys())}")
87
+
88
+ # Extract the actual detection data from the structure: The full response has 'detection_result' key, but we need the inner data
89
+ if ("detection_result" in detection_dict):
90
+ detection_data = detection_dict["detection_result"]
91
+ logger.debug("Extracted detection_result from outer dict")
92
+
93
+ else:
94
+ detection_data = detection_dict
95
+ logger.debug("Using detection_dict directly")
96
+
97
  # Generate detailed reasoning
98
  reasoning = self.reasoning_generator.generate(ensemble_result = detection_result.ensemble_result,
99
  metric_results = detection_result.metric_results,
 
103
  )
104
 
105
  # Extract detailed metrics from ACTUAL detection results
106
+ detailed_metrics = self._extract_detailed_metrics(detection_data)
107
 
108
  # Timestamp for filenames
109
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 
112
 
113
  # Generate requested formats
114
  if ("json" in formats):
115
+ json_path = self._generate_json_report(detection_data = detection_data,
116
+ detection_dict_full = detection_dict,
117
  reasoning = reasoning,
118
  detailed_metrics = detailed_metrics,
119
  attribution_result = attribution_result,
 
124
 
125
  if ("pdf" in formats):
126
  try:
127
+ pdf_path = self._generate_pdf_report(detection_data = detection_data,
128
+ detection_dict_full = detection_dict,
129
  reasoning = reasoning,
130
  detailed_metrics = detailed_metrics,
131
  attribution_result = attribution_result,
 
143
  return generated_files
144
 
145
 
146
+ def _extract_detailed_metrics(self, detection_data: Dict) -> List[DetailedMetric]:
147
  """
148
  Extract detailed metrics with sub-metrics from ACTUAL detection result
149
  """
150
  detailed_metrics = list()
151
+ metrics_data = detection_data.get("metrics", {})
152
+ ensemble_data = detection_data.get("ensemble", {})
153
 
154
  # Get actual metric weights from ensemble
155
  metric_weights = ensemble_data.get("metric_contributions", {})
156
 
157
+ # Log what we're working with
158
+ logger.debug(f"Extracting metrics from {len(metrics_data)} metrics")
159
+ logger.debug(f"Metric names: {list(metrics_data.keys())}")
160
+
161
  # Extract actual metric data
162
  for metric_name, metric_result in metrics_data.items():
163
+ if (not isinstance(metric_result, dict)):
164
+ logger.warning(f"Metric {metric_name} is not a dict: {type(metric_result)}")
165
  continue
166
 
167
+ if (metric_result.get("error") is not None):
168
+ logger.warning(f"Metric {metric_name} has error: {metric_result.get('error')}")
169
  continue
170
 
171
  # Get actual probabilities and confidence
172
+ ai_prob = metric_result.get("ai_probability", 0)
173
+ human_prob = metric_result.get("human_probability", 0)
174
+ confidence = metric_result.get("confidence", 0)
175
+
176
+ # DEBUG: Log extracted values
177
+ logger.debug(f"Metric {metric_name}: AI={ai_prob}, Human={human_prob}, Confidence={confidence}")
178
 
179
  # Determine verdict based on actual probability
180
+ # 60% threshold in decimal
181
+ if (ai_prob >= 0.6):
182
  verdict = "AI"
183
+
184
+ # 40% threshold in decimal
185
+ elif (ai_prob <= 0.4):
186
  verdict = "HUMAN"
187
 
188
  else:
189
+ verdict = "MIXED"
190
 
191
  # Get actual weight or use default
192
  weight = 0.0
193
+
194
  if metric_name in metric_weights:
195
+ weight = metric_weights[metric_name].get("weight", 0.0)
196
 
197
  # Extract actual detailed metrics from metric result
198
  detailed_metrics_data = self._extract_metric_details(metric_name = metric_name,
 
203
  description = self._get_metric_description(metric_name = metric_name)
204
 
205
  detailed_metrics.append(DetailedMetric(name = metric_name,
206
+ ai_probability = ai_prob * 100, # Convert to percentage
207
+ human_probability = human_prob * 100, # Convert to percentage
208
+ confidence = confidence * 100, # Convert to percentage
209
  verdict = verdict,
210
  description = description,
211
  detailed_metrics = detailed_metrics_data,
212
+ weight = weight * 100, # Convert to percentage
213
  )
214
  )
215
 
216
+ logger.debug(f"Extracted {len(detailed_metrics)} detailed metrics")
217
  return detailed_metrics
218
 
219
 
 
253
  return descriptions.get(metric_name, "Advanced text analysis metric.")
254
 
255
 
256
+ def _generate_json_report(self, detection_data: Dict, detection_dict_full: Dict, reasoning: DetailedReasoning, detailed_metrics: List[DetailedMetric],
257
  attribution_result: Optional[AttributionResult], highlighted_sentences: Optional[List] = None, filename: str = None) -> Path:
258
  """
259
  Generate JSON format report with detailed metrics
 
300
  }
301
 
302
  # Use ACTUAL detection results from dictionary
303
+ ensemble_data = detection_data.get("ensemble", {})
304
+ analysis_data = detection_data.get("analysis", {})
305
+ metrics_data_dict = detection_data.get("metrics", {})
306
+ performance_data = detection_data.get("performance", {})
307
+
308
+ report_data = {"report_metadata" : {"generated_at" : datetime.now().isoformat(),
309
+ "version" : "1.0.0",
310
+ "format" : "json",
311
+ "report_id" : filename.replace('.json', ''),
312
+ },
313
+ "overall_results" : {"final_verdict" : ensemble_data.get("final_verdict", "Unknown"),
314
+ "ai_probability" : ensemble_data.get("ai_probability", 0),
315
+ "human_probability" : ensemble_data.get("human_probability", 0),
316
+ "mixed_probability" : ensemble_data.get("mixed_probability", 0),
317
+ "overall_confidence" : ensemble_data.get("overall_confidence", 0),
318
+ "uncertainty_score" : ensemble_data.get("uncertainty_score", 0),
319
+ "consensus_level" : ensemble_data.get("consensus_level", 0),
320
+ "domain" : analysis_data.get("domain", "general"),
321
+ "domain_confidence" : analysis_data.get("domain_confidence", 0),
322
+ "text_length" : analysis_data.get("text_length", 0),
323
+ "sentence_count" : analysis_data.get("sentence_count", 0),
324
+ },
325
+ "ensemble_analysis" : {"method_used" : "confidence_calibrated",
326
+ "metric_weights" : ensemble_data.get("metric_contributions", {}),
327
+ "reasoning" : ensemble_data.get("reasoning", []),
328
+ },
329
+ "detailed_metrics" : metrics_data,
330
+ "detection_reasoning" : {"summary" : reasoning.summary,
331
+ "key_indicators" : reasoning.key_indicators,
332
+ "metric_explanations" : reasoning.metric_explanations,
333
+ "supporting_evidence" : reasoning.supporting_evidence,
334
+ "contradicting_evidence" : reasoning.contradicting_evidence,
335
+ "confidence_explanation" : reasoning.confidence_explanation,
336
+ "domain_analysis" : reasoning.domain_analysis,
337
+ "ensemble_analysis" : reasoning.ensemble_analysis,
338
+ "uncertainty_analysis" : reasoning.uncertainty_analysis,
339
+ "recommendations" : reasoning.recommendations,
340
+ },
341
+ "highlighted_text" : highlighted_data,
342
+ "model_attribution" : attribution_data,
343
+ "performance_metrics" : {"total_processing_time" : performance_data.get("total_time", 0),
344
+ "metrics_execution_time" : performance_data.get("metrics_time", {}),
345
+ "warnings" : detection_data.get("warnings", []),
346
+ "errors" : detection_data.get("errors", []),
347
+ }
348
+ }
349
+
350
+ output_path = self.output_dir / filename
351
 
352
  with open(output_path, 'w', encoding='utf-8') as f:
353
  json.dump(obj = report_data,
 
360
  return output_path
361
 
362
 
363
+ def _generate_pdf_report(self, detection_data: Dict, detection_dict_full: Dict, reasoning: DetailedReasoning, detailed_metrics: List[DetailedMetric],
364
  attribution_result: Optional[AttributionResult], highlighted_sentences: Optional[List] = None, filename: str = None) -> Path:
365
  """
366
  Generate PDF format report with detailed metrics
367
  """
368
  try:
369
  from reportlab.lib import colors
370
+ from reportlab.lib.units import cm
371
+ from reportlab.platypus import Table
372
  from reportlab.lib.units import inch
373
+ from reportlab.platypus import Spacer
374
+ from reportlab.lib.pagesizes import A4
375
+ from reportlab.lib.enums import TA_LEFT
376
+ from reportlab.platypus import PageBreak
377
+ from reportlab.platypus import Paragraph
378
+ from reportlab.lib.enums import TA_RIGHT
379
+ from reportlab.graphics import renderPDF
380
+ from reportlab.lib.enums import TA_CENTER
381
+ from reportlab.platypus import TableStyle
382
+ from reportlab.pdfgen.canvas import Canvas
383
+ from reportlab.lib.enums import TA_JUSTIFY
384
+ from reportlab.lib.pagesizes import letter
385
+ from reportlab.graphics.shapes import Line
386
+ from reportlab.graphics.shapes import Rect
387
+ from reportlab.platypus import KeepTogether
388
+ from reportlab.graphics.shapes import Circle
389
+ from reportlab.graphics.shapes import Drawing
390
+ from reportlab.lib.styles import ParagraphStyle
391
+ from reportlab.platypus import SimpleDocTemplate
392
+ from reportlab.graphics.charts.piecharts import Pie
393
+ from reportlab.platypus.flowables import HRFlowable
394
+ from reportlab.lib.styles import getSampleStyleSheet
395
+ from reportlab.graphics.charts.textlabels import Label
396
+ from reportlab.graphics.widgets.markers import makeMarker
397
+
398
  except ImportError:
399
  raise ImportError("reportlab is required for PDF generation. Install: pip install reportlab")
400
 
401
  output_path = self.output_dir / filename
402
 
403
+ # Create PDF with premium settings
404
  doc = SimpleDocTemplate(str(output_path),
405
+ pagesize = A4,
406
+ rightMargin = 0.75*inch,
407
+ leftMargin = 0.75*inch,
408
+ topMargin = 0.75*inch,
409
+ bottomMargin = 0.75*inch,
410
  )
411
 
412
  # Container for PDF elements
413
+ elements = list()
414
+ styles = getSampleStyleSheet()
415
+
416
+ # Premium Color Scheme
417
+ PRIMARY_COLOR = colors.HexColor('#3b82f6') # Blue-600
418
+ SUCCESS_COLOR = colors.HexColor('#10b981') # Emerald-500
419
+ WARNING_COLOR = colors.HexColor('#f59e0b') # Amber-500
420
+ DANGER_COLOR = colors.HexColor('#ef4444') # Red-500
421
+ INFO_COLOR = colors.HexColor('#8b5cf6') # Violet-500
422
+ GRAY_LIGHT = colors.HexColor('#f8fafc') # Gray-50
423
+ GRAY_MEDIUM = colors.HexColor('#e2e8f0') # Gray-200
424
+ GRAY_DARK = colors.HexColor('#334155') # Gray-700
425
+ TEXT_COLOR = colors.HexColor('#1e293b') # Gray-800
426
+
427
+ # Premium Custom Styles
428
+ title_style = ParagraphStyle('PremiumTitle',
429
+ parent = styles['Heading1'],
430
+ fontName = 'Helvetica-Bold',
431
+ fontSize = 28,
432
+ textColor = PRIMARY_COLOR,
433
+ spaceAfter = 20,
434
+ alignment = TA_CENTER,
435
+ )
436
+
437
+ subtitle_style = ParagraphStyle('PremiumSubtitle',
438
+ parent = styles['Normal'],
439
+ fontName = 'Helvetica',
440
+ fontSize = 12,
441
+ textColor = GRAY_DARK,
442
+ spaceAfter = 30,
443
+ alignment = TA_CENTER,
444
+ )
445
 
446
+ section_style = ParagraphStyle('PremiumSection',
447
+ parent = styles['Heading2'],
448
+ fontName = 'Helvetica-Bold',
449
+ fontSize = 18,
450
+ textColor = TEXT_COLOR,
451
+ spaceAfter = 12,
452
+ spaceBefore = 20,
453
+ underlineWidth = 1,
454
+ underlineColor = PRIMARY_COLOR,
455
+ )
456
+
457
+ subsection_style = ParagraphStyle('PremiumSubSection',
458
+ parent = styles['Heading3'],
459
+ fontName = 'Helvetica-Bold',
460
+ fontSize = 14,
461
+ textColor = GRAY_DARK,
462
+ spaceAfter = 8,
463
+ spaceBefore = 16,
464
+ )
465
+
466
+ body_style = ParagraphStyle('PremiumBody',
467
+ parent = styles['BodyText'],
468
+ fontName = 'Helvetica',
469
+ fontSize = 11,
470
+ textColor = TEXT_COLOR,
471
+ alignment = TA_JUSTIFY,
472
+ spaceAfter = 8,
473
+ )
474
+
475
+ verdict_style = ParagraphStyle('VerdictStyle',
476
+ parent = styles['Heading2'],
477
+ fontName = 'Helvetica-Bold',
478
+ fontSize = 22,
479
+ spaceAfter = 5,
480
+ )
481
+
482
+ metric_name_style = ParagraphStyle('MetricNameStyle',
483
+ parent = styles['Heading3'],
484
+ fontName = 'Helvetica-Bold',
485
+ fontSize = 13,
486
+ textColor = GRAY_DARK,
487
+ spaceAfter = 4,
488
+ )
489
 
490
+ # Use detection results from detection_data
491
+ ensemble_data = detection_data.get("ensemble", {})
492
+ analysis_data = detection_data.get("analysis", {})
493
+ performance_data = detection_data.get("performance", {})
494
+
495
+ # Extract values
496
+ ai_prob = ensemble_data.get("ai_probability", 0)
497
+ human_prob = ensemble_data.get("human_probability", 0)
498
+ mixed_prob = ensemble_data.get("mixed_probability", 0)
499
+ confidence = ensemble_data.get("overall_confidence", 0)
500
+ uncertainty = ensemble_data.get("uncertainty_score", 0)
501
+ consensus = ensemble_data.get("consensus_level", 0)
502
+ final_verdict = ensemble_data.get("final_verdict", "Unknown")
503
+
504
+ # Determine colors based on verdict
505
+ if ("Human".lower() in final_verdict.lower()):
506
+ verdict_color = SUCCESS_COLOR
507
+
508
+ elif ("AI".lower() in final_verdict.lower()):
509
+ verdict_color = DANGER_COLOR
510
+
511
+ elif ("Mixed".lower() in final_verdict.lower()):
512
+ verdict_color = WARNING_COLOR
513
+
514
+ else:
515
+ verdict_color = PRIMARY_COLOR
516
+
517
+ # Create header with logo/company name
518
+ header_style = ParagraphStyle('HeaderStyle',
519
+ parent = styles['Normal'],
520
+ fontName = 'Helvetica-Bold',
521
+ fontSize = 10,
522
+ textColor = GRAY_DARK,
523
+ alignment = TA_RIGHT,
524
+ )
525
+
526
+ # Header
527
+ elements.append(Paragraph("AI DETECTION ANALYTICS", header_style))
528
+ elements.append(HRFlowable(width = "100%",
529
+ thickness = 1,
530
+ color = PRIMARY_COLOR,
531
+ spaceAfter = 20,
532
+ )
533
+ )
534
 
535
  # Title and main sections
536
  elements.append(Paragraph("AI Text Detection Analysis Report", title_style))
537
+ elements.append(Paragraph(f"Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", subtitle_style))
538
+
539
+ # Add decorative line
540
+ elements.append(HRFlowable(width = "80%",
541
+ thickness = 2,
542
+ color = PRIMARY_COLOR,
543
+ spaceBefore = 10,
544
+ spaceAfter = 30,
545
+ hAlign = 'CENTER',
546
+ )
547
+ )
548
+
549
+ # Quick Stats Banner
550
+ stats_data = [['', 'AI', 'HUMAN', 'MIXED'],
551
+ ['Probability', f"{ai_prob:.1%}", f"{human_prob:.1%}", f"{mixed_prob:.1%}"]
552
+ ]
553
+
554
+ stats_table = Table(stats_data, colWidths = [1.5*inch, 1*inch, 1*inch, 1*inch])
555
+ stats_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), PRIMARY_COLOR),
556
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
557
+ ('BACKGROUND', (1, 1), (1, 1), DANGER_COLOR),
558
+ ('BACKGROUND', (2, 1), (2, 1), SUCCESS_COLOR),
559
+ ('BACKGROUND', (3, 1), (3, 1), WARNING_COLOR),
560
+ ('TEXTCOLOR', (1, 1), (-1, 1), colors.white),
561
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
562
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
563
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
564
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
565
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
566
+ ('GRID', (0, 0), (-1, -1), 0.5, colors.white),
567
+ ('BOX', (0, 0), (-1, -1), 1, PRIMARY_COLOR),
568
+ ])
569
+ )
570
+
571
+ elements.append(stats_table)
572
  elements.append(Spacer(1, 0.3*inch))
573
 
574
+ # Main Verdict Section with colored badge
575
+ elements.append(Paragraph("DETECTION VERDICT", section_style))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
+ verdict_box_data = [[Paragraph(f"<font size=18 color='{colors.toHex(verdict_color)}'><b>{final_verdict.upper()}</b></font>", ParagraphStyle('VerdictText', alignment=TA_CENTER)),
578
+ Paragraph(f"<font size=12>Confidence: <b>{confidence:.1%}</b></font><br/>" f"<font size=10>Uncertainty: {uncertainty:.1%} | Consensus: {consensus:.1%}</font>", ParagraphStyle('VerdictDetails', alignment=TA_CENTER))
579
+ ]]
580
 
581
+ verdict_box = Table(verdict_box_data, colWidths=[2.5*inch, 3*inch])
582
+
583
+ verdict_box.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, 0), GRAY_LIGHT),
584
+ ('BACKGROUND', (1, 0), (1, 0), GRAY_LIGHT),
585
+ ('BOX', (0, 0), (-1, -1), 1, verdict_color),
586
+ ('ROUNDEDCORNERS', [10, 10, 10, 10]),
587
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
588
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
589
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 15),
590
+ ('TOPPADDING', (0, 0), (-1, -1), 15),
591
+ ])
592
+ )
593
+
594
+ elements.append(verdict_box)
595
+ elements.append(Spacer(1, 0.3*inch))
596
+
597
+ # Content Analysis in a sleek table
598
+ elements.append(Paragraph("CONTENT ANALYSIS", section_style))
599
+
600
+ domain = analysis_data.get("domain", "general").title().replace('_', ' ')
601
+ domain_confidence = analysis_data.get("domain_confidence", 0)
602
+ text_length = analysis_data.get("text_length", 0)
603
+ sentence_count = analysis_data.get("sentence_count", 0)
604
+ total_time = performance_data.get("total_time", 0)
605
+
606
+ # Create two-column layout for content analysis
607
+ content_data = [[Paragraph("<b>Content Domain</b>", body_style), Paragraph(f"<font color='{colors.toHex(INFO_COLOR)}'><b>{domain}</b></font> ({domain_confidence:.1%} confidence)", body_style)],
608
+ [Paragraph("<b>Text Statistics</b>", body_style), Paragraph(f"{text_length:,} words | {sentence_count:,} sentences", body_style)],
609
+ [Paragraph("<b>Processing Time</b>", body_style), Paragraph(f"{total_time:.2f} seconds", body_style)],
610
+ [Paragraph("<b>Analysis Method</b>", body_style), Paragraph("Confidence-Weighted Ensemble Aggregation", body_style)],
611
+ ]
612
+
613
+ content_table = Table(content_data, colWidths = [2*inch, 4*inch])
614
+
615
+ content_table.setStyle(TableStyle([('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
616
+ ('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
617
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
618
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
619
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
620
+ ('GRID', (0, 0), (-1, -1), 0.25, GRAY_MEDIUM),
621
+ ('BACKGROUND', (0, 0), (0, -1), GRAY_LIGHT),
622
  ])
623
  )
624
+
625
  elements.append(content_table)
626
+ elements.append(Spacer(1, 0.3*inch))
627
 
628
+ # Metric Weights Visualization
629
+ elements.append(Paragraph("METRIC CONTRIBUTIONS", section_style))
 
 
630
 
 
631
  metric_contributions = ensemble_data.get("metric_contributions", {})
632
+
633
+ if metric_contributions and len(metric_contributions) > 0:
634
+ # Create horizontal bar chart effect with table
635
+ weight_data = [['METRIC', 'WEIGHT', '']]
636
+
637
+ for metric_name, contribution in metric_contributions.items():
638
+ weight = contribution.get("weight", 0)
639
+ display_name = metric_name.title().replace('_', ' ')
640
+
641
+ # Create visual bar representation
642
+ bar_width = int(weight * 100)
643
+ bar_cell = f"[{'β–ˆ' * bar_width}{'β–‘' * (100-bar_width)}] {weight:.1%}"
644
+
645
+ weight_data.append([display_name, f"{weight:.1%}", bar_cell])
646
 
647
+ weight_table = Table(weight_data, colWidths=[2*inch, 1*inch, 3*inch])
648
+ weight_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), PRIMARY_COLOR),
649
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
650
  ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
651
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
652
  ('FONTSIZE', (0, 0), (-1, -1), 9),
653
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
654
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
655
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
656
+ ('TEXTCOLOR', (2, 1), (2, -1), PRIMARY_COLOR),
657
+ ('FONTNAME', (2, 1), (2, -1), 'Courier'),
658
  ])
659
+ )
660
+
661
  elements.append(weight_table)
662
+ elements.append(Spacer(1, 0.3*inch))
663
 
664
+ # Detailed Metric Analysis with colored cards
665
+ elements.append(Paragraph("DETAILED METRIC ANALYSIS", section_style))
666
+
667
+ if detailed_metrics:
668
+ for metric in detailed_metrics:
669
+ # Determine metric color based on verdict
670
+ if (metric.verdict == "HUMAN"):
671
+ metric_color = SUCCESS_COLOR
672
+ prob_color = SUCCESS_COLOR
673
+
674
+ elif( metric.verdict == "AI"):
675
+ metric_color = DANGER_COLOR
676
+ prob_color = DANGER_COLOR
677
+
678
+ else:
679
+ metric_color = WARNING_COLOR
680
+ prob_color = WARNING_COLOR
 
 
 
 
 
 
 
 
 
681
 
682
+ # Create metric card
683
+ metric_card_data = [[Paragraph(f"<font color='{colors.toHex(metric_color)}' size=12><b>{metric.name.upper().replace('_', ' ')}</b></font><br/>"
684
+ f"<font size=9>{metric.description}</font>",
685
+ ParagraphStyle('MetricTitle', alignment=TA_LEFT)),
686
+
687
+ Paragraph(f"<font size=11><b>VERDICT</b></font><br/>"
688
+ f"<font color='{colors.toHex(metric_color)}' size=12><b>{metric.verdict}</b></font>",
689
+ ParagraphStyle('MetricVerdict', alignment=TA_CENTER)),
690
+
691
+ Paragraph(f"<font size=11><b>AI PROBABILITY</b></font><br/>"
692
+ f"<font color='{colors.toHex(prob_color)}' size=12><b>{metric.ai_probability:.1f}%</b></font>",
693
+ ParagraphStyle('MetricProbability', alignment=TA_CENTER)),
694
+
695
+ Paragraph(f"<font size=11><b>WEIGHT</b></font><br/>"
696
+ f"<font size=12><b>{metric.weight:.1f}%</b></font>",
697
+ ParagraphStyle('MetricWeight', alignment=TA_CENTER)),
698
+
699
+ Paragraph(f"<font size=11><b>CONFIDENCE</b></font><br/>"
700
+ f"<font size=12><b>{metric.confidence:.1f}%</b></font>",
701
+ ParagraphStyle('MetricConfidence', alignment=TA_CENTER)),
702
+ ]]
703
+
704
+ metric_table = Table(metric_card_data, colWidths = [2.5*inch, 1*inch, 1*inch, 0.8*inch, 0.8*inch])
705
+
706
+ metric_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), GRAY_LIGHT),
707
+ ('BOX', (0, 0), (-1, 0), 1, metric_color),
708
+ ('LINEABOVE', (0, 0), (-1, 0), 2, metric_color),
709
+ ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
710
+ ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),
711
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
712
+ ('TOPPADDING', (0, 0), (-1, 0), 10),
713
+ ])
714
+ )
715
+
716
+ elements.append(metric_table)
717
+
718
+ # Add detailed sub-metrics if available
719
+ if metric.detailed_metrics:
720
+ elements.append(Spacer(1, 0.1*inch))
721
+
722
+ # Create a grid of sub-metrics
723
+ sub_items = list(metric.detailed_metrics.items())[:6]
724
+ sub_data = list()
725
+
726
+ for i in range(0, len(sub_items), 3):
727
+ row = list()
728
+ for j in range(3):
729
+ if (i + j < len(sub_items)):
730
+ sub_name, sub_value = sub_items[i + j]
731
+
732
+ # Format the value
733
+ if isinstance(sub_value, (int, float)):
734
+ if (sub_name.endswith('_score') or sub_name.endswith('_probability')):
735
+ formatted_value = f"{sub_value:.1f}%"
736
 
737
+ elif (sub_name.endswith('_ratio') or sub_name.endswith('_frequency')):
738
+ formatted_value = f"{sub_value:.3f}"
739
+
740
+ elif (sub_name.endswith('_entropy') or sub_name.endswith('_perplexity')):
741
+ formatted_value = f"{sub_value:.2f}"
742
+
743
+ else:
744
+ formatted_value = f"{sub_value:.2f}"
745
+
746
+ else:
747
+ formatted_value = str(sub_value)
748
+
749
+ row.append(f"<b>{sub_name.replace('_', ' ').title()}:</b> {formatted_value}")
750
+
751
+ else:
752
+ row.append("")
753
+
754
+ sub_data.append(row)
755
+
756
+ if sub_data:
757
+ sub_table = Table(sub_data, colWidths = [1.8*inch, 1.8*inch, 1.8*inch])
758
+
759
+ sub_table.setStyle(TableStyle([('FONTSIZE', (0, 0), (-1, -1), 8),
760
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
761
+ ('TOPPADDING', (0, 0), (-1, -1), 4),
762
+ ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
763
+ ])
764
+ )
765
+ elements.append(sub_table)
766
+
767
+ elements.append(Spacer(1, 0.2*inch))
768
 
769
  # Detection Reasoning
770
+ elements.append(Paragraph("DETECTION REASONING", section_style))
771
+
772
+ # Summary in a colored box
773
+ summary_box = Table([[Paragraph(f"<font size=11>{reasoning.summary}</font>", body_style)]], colWidths = [6.5*inch])
774
+ summary_box.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, -1), GRAY_LIGHT),
775
+ ('BOX', (0, 0), (-1, -1), 1, PRIMARY_COLOR),
776
+ ('PADDING', (0, 0), (-1, -1), 10),
777
+ ])
778
+ )
779
+
780
+ elements.append(summary_box)
781
+ elements.append(Spacer(1, 0.2*inch))
782
 
783
  # Key Indicators
784
+ if reasoning.key_indicators:
785
+ elements.append(Paragraph("KEY INDICATORS", subsection_style))
786
+
787
+ indicators_data = list()
788
+
789
+ for i in range(0, len(reasoning.key_indicators), 2):
790
+ row = list()
791
+
792
+ for j in range(2):
793
+ if (i + j < len(reasoning.key_indicators)):
794
+ indicator = reasoning.key_indicators[i + j]
795
+ # Add checkmark for positive indicators
796
+ if (indicator.startswith("βœ…") or indicator.startswith("βœ“")):
797
+ icon_color = SUCCESS_COLOR
798
+
799
+ elif (indicator.startswith("⚠️") or indicator.startswith("❌")):
800
+ icon_color = WARNING_COLOR
801
+
802
+ else:
803
+ icon_color = PRIMARY_COLOR
804
+
805
+ row.append(Paragraph(f"<font color='{colors.toHex(icon_color)}'>β€’</font> {indicator}", body_style))
806
+
807
+ else:
808
+ row.append("")
809
+ indicators_data.append(row)
810
+
811
+ indicators_table = Table(indicators_data, colWidths=[3*inch, 3*inch])
812
+ indicators_table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'),
813
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
814
+ ])
815
+ )
816
+
817
+ elements.append(indicators_table)
818
+ elements.append(Spacer(1, 0.2*inch))
819
 
820
+ # Page break for attribution section
821
+ elements.append(PageBreak())
 
822
 
823
  # Model Attribution Section
824
  if attribution_result:
825
+ elements.append(Paragraph("AI MODEL ATTRIBUTION", section_style))
826
+
827
+ predicted_model = attribution_result.predicted_model.value.replace("_", " ").title()
828
+ attribution_confidence = attribution_result.confidence * 100
829
 
830
+ attribution_card_data = [[Paragraph("<b>PREDICTED MODEL</b>", subsection_style), Paragraph(f"<font size=14 color='{colors.toHex(INFO_COLOR)}'><b>{predicted_model}</b></font>", subsection_style)],
831
+ [Paragraph("<b>ATTRIBUTION CONFIDENCE</b>", subsection_style), Paragraph(f"<font size=14><b>{attribution_confidence:.1f}%</b></font>", subsection_style)],
832
+ [Paragraph("<b>DOMAIN USED</b>", subsection_style), Paragraph(f"<b>{attribution_result.domain_used.value.title()}</b>", subsection_style)],
833
+ ]
834
 
835
+ attribution_table = Table(attribution_card_data, colWidths = [2.5*inch, 3.5*inch])
 
 
 
836
 
837
+ attribution_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, -1), GRAY_LIGHT),
 
838
  ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
839
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
840
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
841
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
842
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
843
  ])
844
  )
845
+
846
  elements.append(attribution_table)
847
+ elements.append(Spacer(1, 0.3*inch))
848
 
849
  # Model probabilities table
850
  if attribution_result.model_probabilities:
851
+ elements.append(Paragraph("MODEL PROBABILITY DISTRIBUTION", subsection_style))
852
 
853
+ prob_data = [['MODEL', 'PROBABILITY', '']]
854
 
855
+ # Show top 8 models
856
+ sorted_models = sorted(attribution_result.model_probabilities.items(), key = lambda x: x[1], reverse=True)[:8]
 
 
857
 
858
  for model_name, probability in sorted_models:
859
  display_name = model_name.replace("_", " ").replace("-", " ").title()
860
+ bar_width = int(probability * 100)
861
+
862
+ prob_data.append([display_name,
863
+ f"{probability:.1%}",
864
+ f"[{'β–ˆ' * bar_width}{'β–‘' * (100-bar_width)}]"
865
+ ])
866
+
867
+ prob_table = Table(prob_data, colWidths = [2.5*inch, 1*inch, 2.5*inch])
868
+
869
+ prob_table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), INFO_COLOR),
870
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
871
  ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
872
+ ('ALIGN', (1, 1), (1, -1), 'RIGHT'),
873
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
874
  ('FONTSIZE', (0, 0), (-1, -1), 9),
875
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
876
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
877
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
878
+ ('FONTNAME', (2, 1), (2, -1), 'Courier'),
879
+ ('TEXTCOLOR', (2, 1), (2, -1), INFO_COLOR),
880
  ])
881
  )
882
+
883
  elements.append(prob_table)
884
+ elements.append(Spacer(1, 0.3*inch))
885
+
886
+ # Recommendations in colored boxes
887
+ if reasoning.recommendations:
888
+ elements.append(Paragraph("RECOMMENDATIONS", section_style))
889
 
890
+ for i, recommendation in enumerate(reasoning.recommendations):
891
+ # Alternate colors for visual interest
892
+ if (i % 3 == 0):
893
+ rec_color = SUCCESS_COLOR
894
+
895
+ elif (i % 3 == 1):
896
+ rec_color = INFO_COLOR
897
+
898
+ else:
899
+ rec_color = WARNING_COLOR
900
+
901
+ rec_box = Table([[Paragraph(f"<font color='{colors.toHex(rec_color)}'>βœ“</font> {recommendation}", body_style)]], colWidths=[6.5*inch])
902
+
903
+ rec_box.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, -1), GRAY_LIGHT),
904
+ ('BOX', (0, 0), (-1, -1), 1, rec_color),
905
+ ('PADDING', (0, 0), (-1, -1), 8),
906
+ ('BOTTOMMARGIN', (0, 0), (-1, -1), 5),
907
+ ])
908
+ )
909
+
910
+ elements.append(rec_box)
911
+ elements.append(Spacer(1, 0.1*inch))
912
 
913
+ # Footer with watermark
914
+ footer_style = ParagraphStyle('FooterStyle',
915
+ parent = styles['Normal'],
916
+ fontName = 'Helvetica',
917
+ fontSize = 9,
918
+ textColor = GRAY_DARK,
919
+ alignment = TA_CENTER,
920
+ )
921
 
922
+ elements.append(Spacer(1, 0.5*inch))
923
+ elements.append(HRFlowable(width="100%", thickness=0.5, color=GRAY_MEDIUM, spaceAfter=10))
924
+
925
+ footer_text = (f"Generated by AI Text Detector v2.0 | "
926
+ f"Processing Time: {total_time:.2f}s | "
927
+ f"Report ID: {filename.replace('.pdf', '')}")
928
+
929
+ elements.append(Paragraph(footer_text, footer_style))
930
+ elements.append(Paragraph("Confidential Analysis Report β€’ Β© 2025 AI Detection Analytics",
931
+ ParagraphStyle('Copyright', parent=footer_style, fontSize=8, textColor=GRAY_MEDIUM)))
932
 
933
  # Build PDF
934
  doc.build(elements)
935
 
936
+ logger.info(f"Premium PDF report saved: {output_path}")
937
  return output_path
938
 
939