Spaces:
Runtime error
Runtime error
| from fastapi import APIRouter, Depends | |
| from fastapi.responses import StreamingResponse | |
| from sqlalchemy.orm import Session | |
| from database import get_db | |
| import models.db_models as db_models | |
| import json | |
| import io | |
| import csv | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib import colors | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import mm | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable | |
| router = APIRouter(prefix="/export", tags=["Export"]) | |
| def _build_pdf_report(record) -> io.BytesIO: | |
| """Generate a professionally styled PDF report for a single scan.""" | |
| buffer = io.BytesIO() | |
| doc = SimpleDocTemplate(buffer, pagesize=A4, topMargin=20*mm, bottomMargin=20*mm, | |
| leftMargin=20*mm, rightMargin=20*mm) | |
| styles = getSampleStyleSheet() | |
| # Custom styles | |
| title_style = ParagraphStyle('Title2', parent=styles['Title'], fontSize=22, | |
| textColor=colors.HexColor('#0db9f2'), spaceAfter=6) | |
| heading_style = ParagraphStyle('Heading2Custom', parent=styles['Heading2'], fontSize=14, | |
| textColor=colors.HexColor('#1e293b'), spaceBefore=14, spaceAfter=8) | |
| body_style = ParagraphStyle('BodyCustom', parent=styles['BodyText'], fontSize=10, | |
| textColor=colors.HexColor('#475569'), leading=14) | |
| elements = [] | |
| # Header | |
| elements.append(Paragraph("ScamDetect AI — Scan Report", title_style)) | |
| elements.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#0db9f2'))) | |
| elements.append(Spacer(1, 10)) | |
| # Scan Metadata | |
| elements.append(Paragraph("Scan Details", heading_style)) | |
| risk_color = '#ef4444' if record.risk_level in ('Critical', 'High') else ( | |
| '#f59e0b' if record.risk_level == 'Medium' else '#10b981' | |
| ) | |
| meta_data = [ | |
| ['Scan ID', record.id], | |
| ['Timestamp', str(record.timestamp)], | |
| ['Input Type', (record.type or 'unknown').capitalize()], | |
| ['Risk Score', f'{record.risk_score}%'], | |
| ['Risk Level', record.risk_level], | |
| ['Threat Categories', ', '.join(json.loads(record.threat_categories)) if record.threat_categories else 'None'], | |
| ] | |
| meta_table = Table(meta_data, colWidths=[120, 350]) | |
| meta_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f1f5f9')), | |
| ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#334155')), | |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 10), | |
| ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')), | |
| ('TOPPADDING', (0, 0), (-1, -1), 6), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), | |
| ('LEFTPADDING', (0, 0), (-1, -1), 8), | |
| ])) | |
| elements.append(meta_table) | |
| elements.append(Spacer(1, 12)) | |
| # Extracted Text | |
| if record.raw_text_extracted: | |
| elements.append(Paragraph("Extracted Content", heading_style)) | |
| # Truncate very long text for the PDF | |
| text_preview = record.raw_text_extracted[:500] | |
| if len(record.raw_text_extracted) > 500: | |
| text_preview += "..." | |
| elements.append(Paragraph(text_preview, body_style)) | |
| elements.append(Spacer(1, 12)) | |
| # Explanations (AI Feedback) | |
| if record.explanations: | |
| elements.append(Paragraph("AI Explainability Analysis", heading_style)) | |
| exp_header = ['Feature', 'Description', 'Risk Contribution'] | |
| exp_data = [exp_header] | |
| for exp in record.explanations: | |
| exp_data.append([exp.feature, exp.description, f'{exp.risk_contribution}%']) | |
| exp_table = Table(exp_data, colWidths=[120, 280, 80]) | |
| exp_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#0db9f2')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 9), | |
| ('ALIGN', (2, 0), (2, -1), 'CENTER'), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8fafc')]), | |
| ('TOPPADDING', (0, 0), (-1, -1), 6), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), | |
| ('LEFTPADDING', (0, 0), (-1, -1), 6), | |
| ])) | |
| elements.append(exp_table) | |
| elements.append(Spacer(1, 16)) | |
| # Footer | |
| elements.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0'))) | |
| elements.append(Spacer(1, 6)) | |
| elements.append(Paragraph("Generated by ScamDetect AI — Multi-Modal Fraud Detection Platform", | |
| ParagraphStyle('Footer', parent=styles['Normal'], fontSize=8, | |
| textColor=colors.HexColor('#94a3b8'), alignment=1))) | |
| doc.build(elements) | |
| buffer.seek(0) | |
| return buffer | |
| async def export_pdf(scan_id: str, db: Session = Depends(get_db)): | |
| """Generate and download a PDF report for a specific scan.""" | |
| record = db.query(db_models.ScanRecord).filter( | |
| db_models.ScanRecord.id == scan_id | |
| ).first() | |
| if not record: | |
| return {"error": "Scan not found"} | |
| pdf_buffer = _build_pdf_report(record) | |
| return StreamingResponse( | |
| pdf_buffer, | |
| media_type="application/pdf", | |
| headers={"Content-Disposition": f"attachment; filename=ScamDetect_Report_{scan_id[:8]}.pdf"} | |
| ) | |
| async def export_csv(db: Session = Depends(get_db)): | |
| """Export all scan history as a CSV file.""" | |
| records = db.query(db_models.ScanRecord).order_by( | |
| db_models.ScanRecord.timestamp.desc() | |
| ).all() | |
| output = io.StringIO() | |
| writer = csv.writer(output) | |
| # Header | |
| writer.writerow(['Scan ID', 'Timestamp', 'Type', 'Risk Score', 'Risk Level', | |
| 'Threat Categories', 'Extracted Text', 'Explanations']) | |
| for record in records: | |
| categories = ', '.join(json.loads(record.threat_categories)) if record.threat_categories else '' | |
| explanations = ' | '.join( | |
| [f"{e.feature}: {e.description} ({e.risk_contribution}%)" for e in record.explanations] | |
| ) | |
| writer.writerow([ | |
| record.id, | |
| record.timestamp, | |
| record.type, | |
| record.risk_score, | |
| record.risk_level, | |
| categories, | |
| (record.raw_text_extracted or '')[:200], | |
| explanations | |
| ]) | |
| output.seek(0) | |
| return StreamingResponse( | |
| io.BytesIO(output.getvalue().encode('utf-8')), | |
| media_type="text/csv", | |
| headers={"Content-Disposition": "attachment; filename=ScamDetect_History.csv"} | |
| ) | |