olivas/backend/app/api/endpoints/reports.py
DJP 3467dbcf03 Initial commit — OliVAS visual attention analysis platform
Full-stack application for predicting where humans look in images using
DeepGaze saliency models. Includes heatmap overlays, gaze sequence prediction,
hotspot detection, AOI analysis, rule-based insights, optional Claude AI
design analysis, and professional PDF report generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:20:58 -05:00

89 lines
3.2 KiB
Python

import io
import logging
from fastapi import APIRouter, Depends, Header, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.dependencies import get_user_id
from app.models.analysis import Analysis
from app.services.storage import storage
router = APIRouter(tags=["reports"])
logger = logging.getLogger("olivas.reports")
@router.get("/analyses/{analysis_id}/report")
async def download_report(
analysis_id: str,
db: AsyncSession = Depends(get_db),
x_user_id: str | None = Header(None),
):
user_id = get_user_id(x_user_id)
stmt = (
select(Analysis)
.options(selectinload(Analysis.aois))
.where(Analysis.id == analysis_id, Analysis.user_id == user_id)
)
result = await db.execute(stmt)
analysis = result.scalar_one_or_none()
if not analysis:
raise HTTPException(status_code=404, detail="Analysis not found")
if analysis.status != "completed":
raise HTTPException(status_code=400, detail="Analysis not yet completed")
# Check if PDF already cached
if storage.exists(analysis_id, "report.pdf"):
data = await storage.load_bytes(analysis_id, "report.pdf")
else:
from app.services.report_generator import generate_report
# Load images for the report
try:
original_data = await storage.load_bytes(analysis_id, "original.png")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Original image not found")
try:
heatmap_data = await storage.load_bytes(analysis_id, "heatmap_overlay.png")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Heatmap image not yet generated")
try:
gaze_data = await storage.load_bytes(analysis_id, "gaze_sequence.png")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Gaze sequence image not yet generated")
# Generate rule-based insights
from app.services.insights import generate_insights
rule_insights = generate_insights(analysis) if analysis.status == "completed" else []
try:
data = generate_report(
analysis=analysis,
original_image=original_data,
heatmap_image=heatmap_data,
gaze_image=gaze_data,
aois=list(analysis.aois),
rule_insights=rule_insights,
ai_insights=analysis.ai_insights,
ai_cost_usd=analysis.ai_cost_usd,
)
except Exception as e:
logger.error(f"Report generation failed for {analysis_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
await storage.save_bytes(data, analysis_id, "report.pdf")
safe_name = analysis.name.replace(" ", "_").replace("/", "_")
return StreamingResponse(
io.BytesIO(data),
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="olivas-report-{safe_name}.pdf"'
},
)