| """ |
| FORENSIQ β Export Module |
| Generates professional forensic reports in PDF, DOCX, TXT, MD formats. |
| Uses Qwen2.5-72B-Instruct (text model) for polished formatting when available. |
| """ |
|
|
| import os |
| import io |
| import re |
| import json |
| import tempfile |
| import datetime |
| from typing import Optional |
|
|
|
|
| |
|
|
| def _format_with_llm(raw_report: str, format_type: str) -> Optional[str]: |
| """Skip LLM formatting β export instantly with raw data. |
| The reports from explanation.py are already professionally formatted.""" |
| return None |
|
|
|
|
| |
|
|
| def _strip_md(text: str) -> str: |
| """Convert markdown to plain text.""" |
| text = re.sub(r'#{1,6}\s*', '', text) |
| text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) |
| text = re.sub(r'\*([^*]+)\*', r'\1', text) |
| text = re.sub(r'`([^`]+)`', r'\1', text) |
| text = re.sub(r'```[^`]*```', '', text, flags=re.DOTALL) |
| text = re.sub(r'\|[^\n]+\|', lambda m: m.group().replace('|', ' '), text) |
| text = re.sub(r'---+', '=' * 60, text) |
| text = re.sub(r'[π¬π΄π’π‘π βͺβ
βοΈπππ³ππ]', '', text) |
| text = re.sub(r'\n{3,}', '\n\n', text) |
| return text.strip() |
|
|
|
|
| |
|
|
| def export_pdf(report_md: str, court_brief_md: str, reasoning_tree_md: str) -> str: |
| """Generate a professional PDF forensic report.""" |
| from fpdf import FPDF |
| |
| |
| formatted = _format_with_llm(report_md, "PDF document") |
| content = _strip_md(formatted if formatted else report_md) |
| court_content = _strip_md(court_brief_md) if court_brief_md else "" |
| tree_content = _strip_md(reasoning_tree_md) if reasoning_tree_md else "" |
| |
| pdf = FPDF() |
| pdf.set_auto_page_break(auto=True, margin=15) |
| pdf.set_left_margin(15) |
| pdf.set_right_margin(15) |
| |
| |
| pdf.add_page() |
| pdf.set_font("Helvetica", "B", 28) |
| pdf.cell(0, 60, "", ln=True) |
| pdf.cell(0, 15, "FORENSIQ", ln=True, align="C") |
| pdf.set_font("Helvetica", "", 14) |
| pdf.cell(0, 10, "Forensic Analysis Report", ln=True, align="C") |
| pdf.set_font("Helvetica", "", 10) |
| pdf.cell(0, 8, "", ln=True) |
| pdf.cell(0, 8, f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}", ln=True, align="C") |
| pdf.cell(0, 8, "Physics-Based Multi-Agent Forensic Framework", ln=True, align="C") |
| pdf.cell(0, 8, "for Explainable Deepfake Detection", ln=True, align="C") |
| |
| |
| pdf.add_page() |
| pdf.set_font("Helvetica", "B", 16) |
| pdf.cell(0, 12, "FORENSIC ANALYSIS REPORT", ln=True) |
| pdf.line(10, pdf.get_y(), 200, pdf.get_y()) |
| pdf.cell(0, 5, "", ln=True) |
| |
| pdf.set_font("Helvetica", "", 9) |
| for line in content.split('\n'): |
| line = line.strip() |
| if not line: |
| pdf.cell(0, 4, "", ln=True) |
| continue |
| |
| if line.startswith('Overall Verdict') or line.startswith('Key Evidence') or \ |
| line.startswith('Agent-by-Agent') or line.startswith('Bayesian') or \ |
| line.startswith('Methodology') or line.startswith('I.') or line.startswith('II.') or \ |
| line.startswith('III.') or line.startswith('IV.') or line.startswith('V.') or \ |
| line.startswith('VI.'): |
| pdf.set_font("Helvetica", "B", 11) |
| pdf.cell(0, 8, "", ln=True) |
| safe = line[:85].encode('latin-1', 'replace').decode('latin-1') |
| pdf.cell(0, 8, safe, ln=True) |
| pdf.set_font("Helvetica", "", 9) |
| else: |
| safe_line = line.encode('latin-1', 'replace').decode('latin-1') |
| |
| try: |
| pdf.multi_cell(0, 5, safe_line[:180]) |
| except Exception: |
| pdf.cell(0, 5, safe_line[:80], ln=True) |
| |
| |
| if court_content: |
| pdf.add_page() |
| pdf.set_font("Helvetica", "B", 16) |
| pdf.cell(0, 12, "EXPERT FORENSIC ANALYSIS BRIEF", ln=True) |
| pdf.line(10, pdf.get_y(), 200, pdf.get_y()) |
| pdf.cell(0, 5, "", ln=True) |
| pdf.set_font("Helvetica", "", 9) |
| for line in court_content.split('\n'): |
| line = line.strip() |
| if not line: |
| pdf.cell(0, 4, "", ln=True) |
| else: |
| safe_line = line.encode('latin-1', 'replace').decode('latin-1') |
| try: |
| pdf.multi_cell(0, 5, safe_line[:180]) |
| except Exception: |
| pdf.cell(0, 5, safe_line[:80], ln=True) |
| |
| |
| if tree_content: |
| pdf.add_page() |
| pdf.set_font("Helvetica", "B", 16) |
| pdf.cell(0, 12, "REASONING TREE", ln=True) |
| pdf.line(10, pdf.get_y(), 200, pdf.get_y()) |
| pdf.cell(0, 5, "", ln=True) |
| pdf.set_font("Courier", "", 8) |
| for line in tree_content.split('\n'): |
| safe_line = line.encode('latin-1', 'replace').decode('latin-1') |
| try: |
| pdf.multi_cell(0, 4, safe_line[:120]) |
| except Exception: |
| pdf.cell(0, 4, safe_line[:60], ln=True) |
| |
| |
| path = tempfile.mktemp(suffix=".pdf", prefix="FORENSIQ_Report_") |
| pdf.output(path) |
| return path |
|
|
|
|
| |
|
|
| def export_docx(report_md: str, court_brief_md: str, reasoning_tree_md: str) -> str: |
| """Generate a professional DOCX forensic report.""" |
| from docx import Document |
| from docx.shared import Inches, Pt, RGBColor |
| from docx.enum.text import WD_ALIGN_PARAGRAPH |
| |
| |
| formatted = _format_with_llm(report_md, "Word document") |
| content = formatted if formatted else report_md |
| |
| doc = Document() |
| |
| |
| title = doc.add_heading('FORENSIQ', level=0) |
| title.alignment = WD_ALIGN_PARAGRAPH.CENTER |
| |
| subtitle = doc.add_paragraph() |
| subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER |
| run = subtitle.add_run('Forensic Analysis Report') |
| run.font.size = Pt(14) |
| run.font.color.rgb = RGBColor(100, 100, 100) |
| |
| date_para = doc.add_paragraph() |
| date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER |
| run = date_para.add_run(f'Generated: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")}') |
| run.font.size = Pt(10) |
| run.font.color.rgb = RGBColor(150, 150, 150) |
| |
| doc.add_page_break() |
| |
| |
| def add_md_content(md_text, doc): |
| for line in md_text.split('\n'): |
| stripped = line.strip() |
| if not stripped: |
| continue |
| |
| |
| if stripped.startswith('# '): |
| doc.add_heading(re.sub(r'[π¬π΄π’π‘π βͺβ
βοΈπππ³ππ]', '', stripped[2:]).strip(), level=1) |
| elif stripped.startswith('## '): |
| doc.add_heading(re.sub(r'[π¬π΄π’π‘π βͺβ
βοΈπππ³ππ]', '', stripped[3:]).strip(), level=2) |
| elif stripped.startswith('### '): |
| doc.add_heading(re.sub(r'[π¬π΄π’π‘π βͺβ
βοΈπππ³ππ]', '', stripped[4:]).strip(), level=3) |
| elif stripped.startswith('---'): |
| doc.add_paragraph('_' * 50) |
| elif stripped.startswith('- ') or stripped.startswith('* '): |
| doc.add_paragraph(stripped[2:], style='List Bullet') |
| elif stripped.startswith('|') and '|' in stripped[1:]: |
| |
| cells = [c.strip() for c in stripped.split('|') if c.strip() and c.strip() != '---'] |
| if cells: |
| doc.add_paragraph(' | '.join(cells)) |
| else: |
| |
| p = doc.add_paragraph() |
| |
| parts = re.split(r'\*\*([^*]+)\*\*', stripped) |
| for i, part in enumerate(parts): |
| if not part: |
| continue |
| run = p.add_run(part) |
| if i % 2 == 1: |
| run.bold = True |
| run.font.size = Pt(10) |
| |
| doc.add_heading('Forensic Analysis Report', level=1) |
| add_md_content(content, doc) |
| |
| if court_brief_md: |
| doc.add_page_break() |
| doc.add_heading('Expert Forensic Analysis Brief', level=1) |
| add_md_content(court_brief_md, doc) |
| |
| if reasoning_tree_md: |
| doc.add_page_break() |
| doc.add_heading('Reasoning Tree', level=1) |
| add_md_content(reasoning_tree_md, doc) |
| |
| path = tempfile.mktemp(suffix=".docx", prefix="FORENSIQ_Report_") |
| doc.save(path) |
| return path |
|
|
|
|
| |
|
|
| def export_txt(report_md: str, court_brief_md: str, reasoning_tree_md: str) -> str: |
| """Generate a plain text forensic report.""" |
| |
| formatted = _format_with_llm(report_md, "plain text document") |
| content = _strip_md(formatted if formatted else report_md) |
| |
| full = "=" * 60 + "\n" |
| full += " FORENSIQ β FORENSIC ANALYSIS REPORT\n" |
| full += f" Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}\n" |
| full += "=" * 60 + "\n\n" |
| full += content |
| |
| if court_brief_md: |
| full += "\n\n" + "=" * 60 + "\n" |
| full += " EXPERT FORENSIC ANALYSIS BRIEF (FRE 702)\n" |
| full += "=" * 60 + "\n\n" |
| full += _strip_md(court_brief_md) |
| |
| if reasoning_tree_md: |
| full += "\n\n" + "=" * 60 + "\n" |
| full += " REASONING TREE\n" |
| full += "=" * 60 + "\n\n" |
| full += _strip_md(reasoning_tree_md) |
| |
| path = tempfile.mktemp(suffix=".txt", prefix="FORENSIQ_Report_") |
| with open(path, 'w', encoding='utf-8') as f: |
| f.write(full) |
| return path |
|
|
|
|
| |
|
|
| def export_md(report_md: str, court_brief_md: str, reasoning_tree_md: str) -> str: |
| """Export as Markdown file.""" |
| full = report_md or "" |
| if court_brief_md: |
| full += "\n\n---\n\n" + court_brief_md |
| if reasoning_tree_md: |
| full += "\n\n---\n\n" + reasoning_tree_md |
| |
| path = tempfile.mktemp(suffix=".md", prefix="FORENSIQ_Report_") |
| with open(path, 'w', encoding='utf-8') as f: |
| f.write(full) |
| return path |
|
|