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

pdf_generator function fixed

Browse files
Files changed (1) hide show
  1. reporter/report_generator.py +258 -271
reporter/report_generator.py CHANGED
@@ -84,15 +84,30 @@ class ReportGenerator:
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,
@@ -164,8 +179,9 @@ class ReportGenerator:
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
@@ -424,6 +440,17 @@ class ReportGenerator:
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'],
@@ -450,8 +477,6 @@ class ReportGenerator:
450
  textColor = TEXT_COLOR,
451
  spaceAfter = 12,
452
  spaceBefore = 20,
453
- underlineWidth = 1,
454
- underlineColor = PRIMARY_COLOR,
455
  )
456
 
457
  subsection_style = ParagraphStyle('PremiumSubSection',
@@ -472,21 +497,6 @@ class ReportGenerator:
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", {})
@@ -504,124 +514,115 @@ class ReportGenerator:
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
 
@@ -629,35 +630,34 @@ class ReportGenerator:
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
 
@@ -667,51 +667,48 @@ class ReportGenerator:
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
 
@@ -721,62 +718,58 @@ class ReportGenerator:
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
 
@@ -784,36 +777,31 @@ class ReportGenerator:
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
 
@@ -824,25 +812,28 @@ class ReportGenerator:
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
 
@@ -850,36 +841,36 @@ class ReportGenerator:
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
 
@@ -889,42 +880,38 @@ class ReportGenerator:
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",
 
84
 
85
  # DEBUG: Check structure
86
  logger.debug(f"detection_dict keys: {list(detection_dict.keys())}")
87
+ logger.debug(f"detection_dict type: {type(detection_dict)}")
88
 
89
+ # Check if this is the full analysis result or just partial data
90
+ # From your logs, it seems like during report generation, you're getting
91
+ # a different/shorter text than the original analysis
92
+
93
+ # Extract the actual detection data from the structure
94
  if ("detection_result" in detection_dict):
95
  detection_data = detection_dict["detection_result"]
96
  logger.debug("Extracted detection_result from outer dict")
97
+ logger.debug(f"Detection data has analysis keys: {list(detection_data.get('analysis', {}).keys())}")
98
+ logger.debug(f"Text length in analysis: {detection_data.get('analysis', {}).get('text_length', 'Unknown')}")
99
  else:
100
  detection_data = detection_dict
101
  logger.debug("Using detection_dict directly")
102
+ logger.debug(f"Detection data has analysis keys: {list(detection_data.get('analysis', {}).keys())}")
103
+
104
+ # Validate we have the correct data (not the short text)
105
+ analysis_data = detection_data.get("analysis", {})
106
+ text_length = analysis_data.get("text_length", 0)
107
+
108
+ if text_length < 50: # Less than minimum
109
+ logger.warning(f"WARNING: Report generation received short text ({text_length} chars). "
110
+ f"This might be partial data instead of full analysis results.")
111
 
112
  # Generate detailed reasoning
113
  reasoning = self.reasoning_generator.generate(ensemble_result = detection_result.ensemble_result,
 
179
  logger.warning(f"Metric {metric_name} is not a dict: {type(metric_result)}")
180
  continue
181
 
182
+ error_msg = metric_result.get("error")
183
+ if (error_msg is not None):
184
+ logger.warning(f"Metric {metric_name} has error: {error_msg}")
185
  continue
186
 
187
  # Get actual probabilities and confidence
 
440
  GRAY_DARK = colors.HexColor('#334155') # Gray-700
441
  TEXT_COLOR = colors.HexColor('#1e293b') # Gray-800
442
 
443
+ # Helper function to get hex color string
444
+ def get_hex_color(color_obj):
445
+ """Convert color object to hex string"""
446
+ if hasattr(color_obj, 'hexval'):
447
+ return color_obj.hexval()
448
+ elif hasattr(color_obj, 'rgb'):
449
+ r, g, b = color_obj.rgb()
450
+ return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
451
+ else:
452
+ return "#3b82f6" # Default blue
453
+
454
  # Premium Custom Styles
455
  title_style = ParagraphStyle('PremiumTitle',
456
  parent = styles['Heading1'],
 
477
  textColor = TEXT_COLOR,
478
  spaceAfter = 12,
479
  spaceBefore = 20,
 
 
480
  )
481
 
482
  subsection_style = ParagraphStyle('PremiumSubSection',
 
497
  spaceAfter = 8,
498
  )
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  # Use detection results from detection_data
501
  ensemble_data = detection_data.get("ensemble", {})
502
  analysis_data = detection_data.get("analysis", {})
 
514
  # Determine colors based on verdict
515
  if ("Human".lower() in final_verdict.lower()):
516
  verdict_color = SUCCESS_COLOR
 
517
  elif ("AI".lower() in final_verdict.lower()):
518
  verdict_color = DANGER_COLOR
 
519
  elif ("Mixed".lower() in final_verdict.lower()):
520
  verdict_color = WARNING_COLOR
 
521
  else:
522
  verdict_color = PRIMARY_COLOR
523
 
524
+ # Get hex strings for colors
525
+ primary_hex = get_hex_color(PRIMARY_COLOR)
526
+ success_hex = get_hex_color(SUCCESS_COLOR)
527
+ warning_hex = get_hex_color(WARNING_COLOR)
528
+ danger_hex = get_hex_color(DANGER_COLOR)
529
+ info_hex = get_hex_color(INFO_COLOR)
530
+ verdict_hex = get_hex_color(verdict_color)
 
531
 
532
  # Header
533
+ elements.append(Paragraph("AI DETECTION ANALYTICS",
534
+ ParagraphStyle('HeaderStyle', parent=styles['Normal'], fontName='Helvetica-Bold', fontSize=10, textColor=GRAY_DARK, alignment=TA_RIGHT)))
535
+
536
+ elements.append(HRFlowable(width="100%", thickness=1, color=PRIMARY_COLOR, spaceAfter=20))
 
 
 
537
 
538
  # Title and main sections
539
  elements.append(Paragraph("AI Text Detection Analysis Report", title_style))
540
  elements.append(Paragraph(f"Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", subtitle_style))
541
 
542
  # Add decorative line
543
+ elements.append(HRFlowable(width="80%", thickness=2, color=PRIMARY_COLOR, spaceBefore=10, spaceAfter=30, hAlign='CENTER'))
 
 
 
 
 
 
 
544
 
545
  # Quick Stats Banner
546
  stats_data = [['', 'AI', 'HUMAN', 'MIXED'],
547
+ ['Probability', f"{ai_prob:.1%}", f"{human_prob:.1%}", f"{mixed_prob:.1%}"]]
548
+
549
+ stats_table = Table(stats_data, colWidths=[1.5*inch, 1*inch, 1*inch, 1*inch])
550
+ stats_table.setStyle(TableStyle([
551
+ ('BACKGROUND', (0, 0), (-1, 0), PRIMARY_COLOR),
552
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
553
+ ('BACKGROUND', (1, 1), (1, 1), DANGER_COLOR),
554
+ ('BACKGROUND', (2, 1), (2, 1), SUCCESS_COLOR),
555
+ ('BACKGROUND', (3, 1), (3, 1), WARNING_COLOR),
556
+ ('TEXTCOLOR', (1, 1), (-1, 1), colors.white),
557
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
558
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
559
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
560
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
561
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
562
+ ('GRID', (0, 0), (-1, -1), 0.5, colors.white),
563
+ ('BOX', (0, 0), (-1, -1), 1, PRIMARY_COLOR),
564
+ ]))
565
+
 
566
  elements.append(stats_table)
567
  elements.append(Spacer(1, 0.3*inch))
568
 
569
  # Main Verdict Section with colored badge
570
  elements.append(Paragraph("DETECTION VERDICT", section_style))
571
 
572
+ verdict_box_data = [[
573
+ Paragraph(f"<font size=18 color='{verdict_hex}'><b>{final_verdict.upper()}</b></font>",
574
+ ParagraphStyle('VerdictText', alignment=TA_CENTER)),
575
+ Paragraph(f"<font size=12>Confidence: <b>{confidence:.1%}</b></font><br/>"
576
+ f"<font size=10>Uncertainty: {uncertainty:.1%} | Consensus: {consensus:.1%}</font>",
577
+ ParagraphStyle('VerdictDetails', alignment=TA_CENTER))
578
+ ]]
579
+
580
+ verdict_box = Table(verdict_box_data, colWidths=[2.5*inch, 3*inch])
581
+ verdict_box.setStyle(TableStyle([
582
+ ('BACKGROUND', (0, 0), (0, 0), GRAY_LIGHT),
583
+ ('BACKGROUND', (1, 0), (1, 0), GRAY_LIGHT),
584
+ ('BOX', (0, 0), (-1, -1), 1, verdict_color),
585
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
586
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
587
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 15),
588
+ ('TOPPADDING', (0, 0), (-1, -1), 15),
589
+ ]))
590
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  elements.append(verdict_box)
592
  elements.append(Spacer(1, 0.3*inch))
593
 
594
  # Content Analysis in a sleek table
595
  elements.append(Paragraph("CONTENT ANALYSIS", section_style))
596
 
597
+ domain = analysis_data.get("domain", "general").title().replace('_', ' ')
598
  domain_confidence = analysis_data.get("domain_confidence", 0)
599
+ text_length = analysis_data.get("text_length", 0)
600
+ sentence_count = analysis_data.get("sentence_count", 0)
601
+ total_time = performance_data.get("total_time", 0)
602
 
603
  # Create two-column layout for content analysis
604
+ content_data = [
605
+ [Paragraph("<b>Content Domain</b>", body_style),
606
+ Paragraph(f"<font color='{info_hex}'><b>{domain}</b></font> ({domain_confidence:.1%} confidence)", body_style)],
607
+ [Paragraph("<b>Text Statistics</b>", body_style),
608
+ Paragraph(f"{text_length:,} words | {sentence_count:,} sentences", body_style)],
609
+ [Paragraph("<b>Processing Time</b>", body_style),
610
+ Paragraph(f"{total_time:.2f} seconds", body_style)],
611
+ [Paragraph("<b>Analysis Method</b>", body_style),
612
+ Paragraph("Confidence-Weighted Ensemble Aggregation", body_style)],
613
+ ]
614
+
615
+ content_table = Table(content_data, colWidths=[2*inch, 4*inch])
616
+ content_table.setStyle(TableStyle([
617
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
618
+ ('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
619
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
620
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
621
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
622
+ ('GRID', (0, 0), (-1, -1), 0.25, GRAY_MEDIUM),
623
+ ('BACKGROUND', (0, 0), (0, -1), GRAY_LIGHT),
624
+ ]))
625
+
626
  elements.append(content_table)
627
  elements.append(Spacer(1, 0.3*inch))
628
 
 
630
  elements.append(Paragraph("METRIC CONTRIBUTIONS", section_style))
631
 
632
  metric_contributions = ensemble_data.get("metric_contributions", {})
 
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 * 50) # Use 50 chars max for better fit
643
+ bar_cell = f"[{'β–ˆ' * bar_width}{'β–‘' * (50-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, 2.5*inch])
648
+ weight_table.setStyle(TableStyle([
649
+ ('BACKGROUND', (0, 0), (-1, 0), PRIMARY_COLOR),
650
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
651
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
652
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
653
+ ('FONTSIZE', (0, 0), (-1, -1), 9),
654
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
655
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
656
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
657
+ ('TEXTCOLOR', (2, 1), (2, -1), PRIMARY_COLOR),
658
+ ('FONTNAME', (2, 1), (2, -1), 'Courier'),
659
+ ]))
660
+
661
  elements.append(weight_table)
662
  elements.append(Spacer(1, 0.3*inch))
663
 
 
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
+ elif metric.verdict == "AI":
 
674
  metric_color = DANGER_COLOR
675
+ prob_color = DANGER_COLOR
 
676
  else:
677
  metric_color = WARNING_COLOR
678
+ prob_color = WARNING_COLOR
679
+
680
+ metric_color_hex = get_hex_color(metric_color)
681
+ prob_color_hex = get_hex_color(prob_color)
682
 
683
  # Create metric card
684
+ metric_card_data = [[
685
+ Paragraph(f"<font color='{metric_color_hex}' size=12><b>{metric.name.upper().replace('_', ' ')}</b></font><br/>"
686
+ f"<font size=9>{metric.description}</font>",
687
+ ParagraphStyle('MetricTitle', alignment=TA_LEFT)),
688
+
689
+ Paragraph(f"<font size=11><b>VERDICT</b></font><br/>"
690
+ f"<font color='{metric_color_hex}' size=12><b>{metric.verdict}</b></font>",
691
+ ParagraphStyle('MetricVerdict', alignment=TA_CENTER)),
692
+
693
+ Paragraph(f"<font size=11><b>AI PROBABILITY</b></font><br/>"
694
+ f"<font color='{prob_color_hex}' size=12><b>{metric.ai_probability:.1f}%</b></font>",
695
+ ParagraphStyle('MetricProbability', alignment=TA_CENTER)),
696
+
697
+ Paragraph(f"<font size=11><b>WEIGHT</b></font><br/>"
698
+ f"<font size=12><b>{metric.weight:.1f}%</b></font>",
699
+ ParagraphStyle('MetricWeight', alignment=TA_CENTER)),
700
+ ]]
 
 
 
 
 
701
 
702
+ metric_table = Table(metric_card_data, colWidths=[2.5*inch, 1.2*inch, 1.2*inch, 1*inch])
703
+ metric_table.setStyle(TableStyle([
704
+ ('BACKGROUND', (0, 0), (-1, 0), GRAY_LIGHT),
705
+ ('BOX', (0, 0), (-1, 0), 1, metric_color),
706
+ ('LINEABOVE', (0, 0), (-1, 0), 2, metric_color),
707
+ ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
708
+ ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),
709
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
710
+ ('TOPPADDING', (0, 0), (-1, 0), 10),
711
+ ]))
712
 
713
  elements.append(metric_table)
714
 
 
718
 
719
  # Create a grid of sub-metrics
720
  sub_items = list(metric.detailed_metrics.items())[:6]
721
+ sub_data = []
722
 
723
  for i in range(0, len(sub_items), 3):
724
+ row = []
725
  for j in range(3):
726
+ if i + j < len(sub_items):
727
  sub_name, sub_value = sub_items[i + j]
 
728
  # Format the value
729
  if isinstance(sub_value, (int, float)):
730
+ if sub_name.endswith('_score') or sub_name.endswith('_probability'):
731
  formatted_value = f"{sub_value:.1f}%"
732
+ elif sub_name.endswith('_ratio') or sub_name.endswith('_frequency'):
 
733
  formatted_value = f"{sub_value:.3f}"
734
+ elif sub_name.endswith('_entropy') or sub_name.endswith('_perplexity'):
 
735
  formatted_value = f"{sub_value:.2f}"
 
736
  else:
737
  formatted_value = f"{sub_value:.2f}"
 
738
  else:
739
  formatted_value = str(sub_value)
740
 
741
  row.append(f"<b>{sub_name.replace('_', ' ').title()}:</b> {formatted_value}")
 
742
  else:
743
  row.append("")
744
 
745
  sub_data.append(row)
746
 
747
  if sub_data:
748
+ sub_table = Table(sub_data, colWidths=[1.8*inch, 1.8*inch, 1.8*inch])
749
+ sub_table.setStyle(TableStyle([
750
+ ('FONTSIZE', (0, 0), (-1, -1), 8),
751
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
752
+ ('TOPPADDING', (0, 0), (-1, -1), 4),
753
+ ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
754
+ ]))
 
755
  elements.append(sub_table)
756
 
757
  elements.append(Spacer(1, 0.2*inch))
758
+ else:
759
+ elements.append(Paragraph("No detailed metrics available for this analysis.", body_style))
760
+ elements.append(Spacer(1, 0.2*inch))
761
 
762
  # Detection Reasoning
763
  elements.append(Paragraph("DETECTION REASONING", section_style))
764
 
765
  # Summary in a colored box
766
+ summary_box = Table([[Paragraph(f"<font size=11>{reasoning.summary}</font>", body_style)]], colWidths=[6.5*inch])
767
+ summary_box.setStyle(TableStyle([
768
+ ('BACKGROUND', (0, 0), (-1, -1), GRAY_LIGHT),
769
+ ('BOX', (0, 0), (-1, -1), 1, PRIMARY_COLOR),
770
+ ('PADDING', (0, 0), (-1, -1), 10),
771
+ ]))
772
+
773
  elements.append(summary_box)
774
  elements.append(Spacer(1, 0.2*inch))
775
 
 
777
  if reasoning.key_indicators:
778
  elements.append(Paragraph("KEY INDICATORS", subsection_style))
779
 
780
+ indicators_data = []
 
781
  for i in range(0, len(reasoning.key_indicators), 2):
782
+ row = []
 
783
  for j in range(2):
784
+ if i + j < len(reasoning.key_indicators):
785
  indicator = reasoning.key_indicators[i + j]
786
  # Add checkmark for positive indicators
787
+ if indicator.startswith("βœ…") or indicator.startswith("βœ“"):
788
+ icon_color = success_hex
789
+ elif indicator.startswith("⚠️") or indicator.startswith("❌"):
790
+ icon_color = warning_hex
 
 
791
  else:
792
+ icon_color = primary_hex
793
 
794
+ row.append(Paragraph(f"<font color='{icon_color}'>β€’</font> {indicator}", body_style))
 
795
  else:
796
  row.append("")
797
  indicators_data.append(row)
798
 
799
  indicators_table = Table(indicators_data, colWidths=[3*inch, 3*inch])
800
+ indicators_table.setStyle(TableStyle([
801
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
802
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
803
+ ]))
804
+
805
  elements.append(indicators_table)
806
  elements.append(Spacer(1, 0.2*inch))
807
 
 
812
  if attribution_result:
813
  elements.append(Paragraph("AI MODEL ATTRIBUTION", section_style))
814
 
815
+ predicted_model = attribution_result.predicted_model.value.replace("_", " ").title()
816
  attribution_confidence = attribution_result.confidence * 100
817
 
818
+ attribution_card_data = [
819
+ [Paragraph("<b>PREDICTED MODEL</b>", subsection_style),
820
+ Paragraph(f"<font size=14 color='{info_hex}'><b>{predicted_model}</b></font>", subsection_style)],
821
+ [Paragraph("<b>ATTRIBUTION CONFIDENCE</b>", subsection_style),
822
+ Paragraph(f"<font size=14><b>{attribution_confidence:.1f}%</b></font>", subsection_style)],
823
+ [Paragraph("<b>DOMAIN USED</b>", subsection_style),
824
+ Paragraph(f"<b>{attribution_result.domain_used.value.title()}</b>", subsection_style)],
825
+ ]
826
 
827
+ attribution_table = Table(attribution_card_data, colWidths=[2.5*inch, 3.5*inch])
828
+ attribution_table.setStyle(TableStyle([
829
+ ('BACKGROUND', (0, 0), (0, -1), GRAY_LIGHT),
830
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
831
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
832
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
833
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
834
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
835
+ ]))
836
 
 
 
 
 
 
 
 
 
 
837
  elements.append(attribution_table)
838
  elements.append(Spacer(1, 0.3*inch))
839
 
 
841
  if attribution_result.model_probabilities:
842
  elements.append(Paragraph("MODEL PROBABILITY DISTRIBUTION", subsection_style))
843
 
844
+ prob_data = [['MODEL', 'PROBABILITY', '']]
845
 
846
+ # Show top 6 models
847
+ sorted_models = sorted(attribution_result.model_probabilities.items(),
848
+ key=lambda x: x[1], reverse=True)[:6]
849
 
850
  for model_name, probability in sorted_models:
851
  display_name = model_name.replace("_", " ").replace("-", " ").title()
852
+ bar_width = int(probability * 40) # 40 chars max
853
+ prob_data.append([
854
+ display_name,
855
+ f"{probability:.1%}",
856
+ f"[{'β–ˆ' * bar_width}{'β–‘' * (40-bar_width)}]"
857
+ ])
858
+
859
+ prob_table = Table(prob_data, colWidths=[2.5*inch, 1*inch, 2*inch])
860
+ prob_table.setStyle(TableStyle([
861
+ ('BACKGROUND', (0, 0), (-1, 0), INFO_COLOR),
862
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
863
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
864
+ ('ALIGN', (1, 1), (1, -1), 'RIGHT'),
865
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
866
+ ('FONTSIZE', (0, 0), (-1, -1), 9),
867
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
868
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
869
+ ('GRID', (0, 0), (-1, -1), 0.5, GRAY_MEDIUM),
870
+ ('FONTNAME', (2, 1), (2, -1), 'Courier'),
871
+ ('TEXTCOLOR', (2, 1), (2, -1), INFO_COLOR),
872
+ ]))
873
+
 
874
  elements.append(prob_table)
875
  elements.append(Spacer(1, 0.3*inch))
876
 
 
880
 
881
  for i, recommendation in enumerate(reasoning.recommendations):
882
  # Alternate colors for visual interest
883
+ if i % 3 == 0:
884
+ rec_color = success_hex
885
+ elif i % 3 == 1:
886
+ rec_color = info_hex
 
 
887
  else:
888
+ rec_color = warning_hex
889
 
890
+ rec_box = Table([[Paragraph(f"<font color='{rec_color}'>βœ“</font> {recommendation}", body_style)]], colWidths=[6.5*inch])
891
+ rec_box.setStyle(TableStyle([
892
+ ('BACKGROUND', (0, 0), (-1, -1), GRAY_LIGHT),
893
+ ('PADDING', (0, 0), (-1, -1), 8),
894
+ ('BOTTOMMARGIN', (0, 0), (-1, -1), 5),
895
+ ]))
896
 
 
 
 
 
 
 
 
897
  elements.append(rec_box)
898
  elements.append(Spacer(1, 0.1*inch))
899
 
900
  # Footer with watermark
901
  footer_style = ParagraphStyle('FooterStyle',
902
+ parent = styles['Normal'],
903
+ fontName = 'Helvetica',
904
+ fontSize = 9,
905
+ textColor = GRAY_DARK,
906
+ alignment = TA_CENTER,
907
+ )
908
 
909
  elements.append(Spacer(1, 0.5*inch))
910
  elements.append(HRFlowable(width="100%", thickness=0.5, color=GRAY_MEDIUM, spaceAfter=10))
911
 
912
  footer_text = (f"Generated by AI Text Detector v2.0 | "
913
+ f"Processing Time: {total_time:.2f}s | "
914
+ f"Report ID: {filename.replace('.pdf', '')}")
915
 
916
  elements.append(Paragraph(footer_text, footer_style))
917
  elements.append(Paragraph("Confidential Analysis Report β€’ Β© 2025 AI Detection Analytics",