Skip to content
Open

Pdf #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions Dockerfile.patched
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@ FROM my-fastapi-app:latest

# Install only the missing packages
RUN pip install --no-cache-dir \
openai

# docker build -f Dockerfile.patched -t my-fastapi-app:patched .
# docker tag my-fastapi-app:patched my-fastapi-app:latest
lime
1 change: 1 addition & 0 deletions backend/api/report/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# backend/api/report/__init__.py
204 changes: 204 additions & 0 deletions backend/api/report/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# backend/api/report/routes.py
"""
Synaptic Shield — Report Generation API
POST /report/generate — generate a PDF for image, video, or audio analysis
GET /report/download/{filename} — download a previously generated PDF
"""

import os
import uuid
import logging
from datetime import datetime

from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, JSONResponse

from .schemas import GenerateReportRequest, GenerateReportResponse

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/report", tags=["Report Generation"])

REPORTS_DIR = "/app/backend/reports"
os.makedirs(REPORTS_DIR, exist_ok=True)


# ─── POST /report/generate ────────────────────────────────────────────────────

@router.post("/generate", response_model=GenerateReportResponse)
async def generate_report(payload: GenerateReportRequest):
"""
Generate a professional forensic PDF report.

module_type:
- "image" → Module A: Single-image deepfake analysis report
- "video" → Module B: Video frame-by-frame analysis report
- "audio" → Module C: Acoustic deepfake analysis report

Returns the file path and report ID. Use GET /report/download/{filename}
to retrieve the actual PDF binary.
"""
case_id = payload.case_id or f"CASE-{datetime.now().strftime('%Y%m%d%H%M%S')}"
module = payload.module_type

logger.info(f"[ReportAPI] Generating {module.upper()} report for case: {case_id}")

try:
if module == "image":
file_path, report_id = _generate_image_report(payload, case_id)
elif module == "video":
file_path, report_id = _generate_video_report(payload, case_id)
elif module == "audio":
file_path, report_id = _generate_audio_report(payload, case_id)
else:
raise HTTPException(status_code=400, detail=f"Unknown module_type: {module}")

logger.info(f"[ReportAPI] ✅ Generated: {file_path}")
return GenerateReportResponse(
report_id=report_id,
file_path=file_path,
module_type=module,
message=f"{module.upper()} forensic report generated successfully",
)

except HTTPException:
raise
except Exception as e:
logger.error(f"[ReportAPI] ❌ Report generation failed: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Report generation failed: {str(e)}"
)


# ─── GET /report/download/{filename} ─────────────────────────────────────────

@router.get("/download/{filename}")
async def download_report(filename: str):
"""
Download a previously generated forensic PDF report by filename.
Security: only alphanumeric, dash, underscore, dot allowed in filename.
"""
import re
if not re.match(r'^[\w\-. ]+\.pdf$', filename, re.IGNORECASE):
raise HTTPException(status_code=400, detail="Invalid filename format")

file_path = os.path.join(REPORTS_DIR, filename)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"Report not found: {filename}")

return FileResponse(
path=file_path,
media_type="application/pdf",
filename=filename,
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"X-Report-Source": "Synaptic Shield XAI Platform",
}
)


# ─── GET /report/list ─────────────────────────────────────────────────────────

@router.get("/list")
async def list_reports():
"""List all generated reports in the reports directory."""
try:
files = [
f for f in os.listdir(REPORTS_DIR)
if f.lower().endswith(".pdf")
]
files.sort(reverse=True) # newest first
return {
"reports": files,
"count": len(files),
"reports_dir": REPORTS_DIR,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# ─── Internal helpers ─────────────────────────────────────────────────────────

def _generate_image_report(payload: GenerateReportRequest, case_id: str):
from services.reports.image_report import ImageForensicReport

img = payload.image_data
if img is None:
img = {}
else:
img = img.model_dump()

# Resolve LLM explanation: prefer executive_summary, fall back to llm_explanation
llm_text = payload.executive_summary or payload.llm_explanation or ""

data = {
"case_id": case_id,
"executive_summary": llm_text,
"llm_explanation": llm_text,
# Flatten image_data fields into root
**img,
}

report = ImageForensicReport(data)
file_path = report.generate(REPORTS_DIR)
return file_path, report.report_id


def _generate_video_report(payload: GenerateReportRequest, case_id: str):
from services.reports.video_report import VideoForensicReport

vid = payload.video_data
vid_dict = vid.model_dump() if vid else {}

# Build flagged_frames list
flagged = []
if payload.flagged_frames:
for f in payload.flagged_frames:
fd = f.model_dump()
flagged.append(fd)

# If anomaly_count not set, derive from flagged frames
if not vid_dict.get("anomaly_count") and flagged:
vid_dict["anomaly_count"] = len([f for f in flagged if f.get("is_anomaly", True)])

# Resolve LLM explanation: prefer executive_summary, fall back to llm_explanation
llm_text = payload.executive_summary or payload.llm_explanation or ""

data = {
"case_id": case_id,
"executive_summary": llm_text,
"llm_explanation": llm_text,
"video_data": vid_dict,
"flagged_frames": flagged,
}

report = VideoForensicReport(data)
file_path = report.generate(REPORTS_DIR)
return file_path, report.report_id


def _generate_audio_report(payload: GenerateReportRequest, case_id: str):
from services.reports.audio_report import AudioForensicReport

aud = payload.audio_data
aud_dict = aud.model_dump() if aud else {}

stft_dict = payload.stft.model_dump() if payload.stft else None

# Resolve LLM explanation: prefer executive_summary, fall back to llm_explanation
llm_text = payload.executive_summary or payload.llm_explanation or ""

data = {
"case_id": case_id,
"executive_summary": llm_text,
"llm_explanation": llm_text,
"audio_data": aud_dict,
"ig_scores": payload.ig_scores or [],
"shap_scores": payload.shap_scores or [],
"stft": stft_dict,
}

report = AudioForensicReport(data)
file_path = report.generate(REPORTS_DIR)
return file_path, report.report_id
117 changes: 117 additions & 0 deletions backend/api/report/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# backend/api/report/schemas.py
"""
Pydantic request/response schemas for the forensic PDF report generation API.
Supports three module types: image, video, audio.
"""

from typing import Optional, Literal, List, Any
from pydantic import BaseModel, Field


# ─── Shared ───────────────────────────────────────────────────────────────────

class StftDataSchema(BaseModel):
"""STFT spectrogram data forwarded from the audio analysis result."""
matrix: List[List[float]]
times: List[float]
freqs: List[float]
db_min: float
db_max: float


# ─── Module A: Image ──────────────────────────────────────────────────────────

class ImageDataSchema(BaseModel):
task_id: Optional[str] = None
file_name: str = "Unknown"
verdict: Optional[str] = None
is_fake: bool = False
confidence: float = 0.0
fake_prob: float = 0.0
real_prob: float = 0.0
anomaly_type: Optional[str] = None
sha256_hash: Optional[str] = None
# Base64 images (with or without data URI prefix)
thumbnail_b64: Optional[str] = None
gradcam_b64: Optional[str] = None
ela_b64: Optional[str] = None
# JSON XAI payloads
fft_data: Optional[Any] = None
lime_data: Optional[Any] = None


# ─── Module B: Video ──────────────────────────────────────────────────────────

class FlaggedFrameSchema(BaseModel):
frame_index: int = 0
timestamp: str = "00:00:00"
is_anomaly: bool = True
confidence: float = 0.0
fake_prob: float = 0.0
real_prob: float = 0.0
anomaly_type: Optional[str] = None
# Base64 frame image data (raw base64, NO data URI prefix needed)
frame_data: Optional[str] = None
gradcam_b64: Optional[str] = None
ela_b64: Optional[str] = None
lime_b64: Optional[str] = None
fft_b64: Optional[str] = None


class VideoDataSchema(BaseModel):
task_id: Optional[str] = None
file_name: str = "Unknown"
verdict: Optional[str] = None
is_fake: bool = False
confidence: float = 0.0
fake_prob: float = 0.0
real_prob: float = 0.0
total_frames: int = 0
anomaly_count: int = 0
duration_seconds: float = 0.0
detected_type: Optional[str] = None


# ─── Module C: Audio ──────────────────────────────────────────────────────────

class AudioDataSchema(BaseModel):
task_id: Optional[str] = None
file_name: str = "Unknown"
verdict: str = "REAL"
is_fake: bool = False
confidence: float = 0.0
fake_prob: float = 0.0
real_prob: float = 0.0
duration_seconds: float = 0.0


# ─── Unified Report Request ───────────────────────────────────────────────────

class GenerateReportRequest(BaseModel):
case_id: Optional[str] = None
module_type: Literal["image", "video", "audio"]
executive_summary: Optional[str] = None
# Alias: some frontend sends llm_explanation instead of executive_summary
llm_explanation: Optional[str] = None

# Module-specific payloads (only one should be filled per call)
image_data: Optional[ImageDataSchema] = None
video_data: Optional[VideoDataSchema] = None
audio_data: Optional[AudioDataSchema] = None

# Video: list of flagged frames with base64 images
flagged_frames: Optional[List[FlaggedFrameSchema]] = None

# Audio: XAI score vectors and spectrogram
ig_scores: Optional[List[float]] = None
shap_scores: Optional[List[float]] = None
stft: Optional[StftDataSchema] = None


# ─── Response ─────────────────────────────────────────────────────────────────

class GenerateReportResponse(BaseModel):
report_id: str
file_path: str
module_type: str
message: str = "Report generated successfully"
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ async def shutdown_event():
from api.users.routes import router as users_router
from api.audio.routes import router as audio_router
from api.audio.websocket import router as audio_ws_router
from api.report.routes import router as report_router

app.include_router(video_ws_router)
app.include_router(video_router)
app.include_router(auth_router)
app.include_router(users_router)
app.include_router(audio_router)
app.include_router(audio_ws_router)
app.include_router(report_router)


@app.get("/health")
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added backend/reports/string_20260404_093452.pdf
Binary file not shown.
34 changes: 34 additions & 0 deletions backend/services/pdf_generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# backend/services/pdf_generator/__init__.py
"""
PDF Generation Module for Deepfake Detection Reports.

Supports generation of professional forensic PDF reports for:
- Video Analysis
- Image Analysis
- Audio Analysis

Each report includes:
- Executive Summary (LLM-generated)
- Technical Breakdown (anomaly scores, confidence levels)
- XAI Visualizations (Grad-CAM, ELA, spectrograms)
- Forensic Evidence (side-by-side comparisons)
"""

from .generator import PDFGenerator, generate_forensic_report
from .schemas import (
AnalysisReportData,
VideoAnalysisData,
ImageAnalysisData,
AudioAnalysisData,
XAIResultData,
)

__all__ = [
"PDFGenerator",
"generate_forensic_report",
"AnalysisReportData",
"VideoAnalysisData",
"ImageAnalysisData",
"AudioAnalysisData",
"XAIResultData",
]
6 changes: 6 additions & 0 deletions backend/services/reports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# backend/services/reports/__init__.py
from .image_report import ImageForensicReport
from .video_report import VideoForensicReport
from .audio_report import AudioForensicReport

__all__ = ["ImageForensicReport", "VideoForensicReport", "AudioForensicReport"]
Loading