| """ |
| Report Generator |
| ================ |
| Generates PDF and HTML reports for accident analysis. |
| """ |
|
|
| import os |
| from datetime import datetime |
| from typing import Dict, List, Any |
| from pathlib import Path |
|
|
| from config import REPORTS_DIR, REPORT_CONFIG, VEHICLE_TYPES |
|
|
|
|
| def generate_report( |
| results: Dict[str, Any], |
| scenarios: List[Dict[str, Any]], |
| accident_info: Dict[str, Any], |
| vehicle_1: Dict[str, Any], |
| vehicle_2: Dict[str, Any], |
| format: str = 'pdf', |
| include_maps: bool = True, |
| include_charts: bool = True, |
| include_raw_data: bool = False |
| ) -> str: |
| """ |
| Generate a comprehensive accident analysis report. |
| |
| Args: |
| results: Analysis results dictionary |
| scenarios: List of generated scenarios |
| accident_info: Accident context information |
| vehicle_1: First vehicle data |
| vehicle_2: Second vehicle data |
| format: Output format ('pdf' or 'html') |
| include_maps: Whether to include map visualizations |
| include_charts: Whether to include charts |
| include_raw_data: Whether to include raw data tables |
| |
| Returns: |
| Path to generated report file |
| """ |
| |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| |
| if format.lower() == 'html': |
| return generate_html_report( |
| results, scenarios, accident_info, vehicle_1, vehicle_2, |
| timestamp, include_maps, include_charts, include_raw_data |
| ) |
| else: |
| return generate_pdf_report( |
| results, scenarios, accident_info, vehicle_1, vehicle_2, |
| timestamp, include_maps, include_charts, include_raw_data |
| ) |
|
|
|
|
| def generate_html_report( |
| results: Dict[str, Any], |
| scenarios: List[Dict[str, Any]], |
| accident_info: Dict[str, Any], |
| vehicle_1: Dict[str, Any], |
| vehicle_2: Dict[str, Any], |
| timestamp: str, |
| include_maps: bool, |
| include_charts: bool, |
| include_raw_data: bool |
| ) -> str: |
| """Generate HTML report.""" |
| |
| |
| most_likely = results.get('most_likely_scenario', {}) |
| fault = results.get('preliminary_fault_assessment', {}) |
| |
| v1_type = VEHICLE_TYPES.get(vehicle_1.get('type', 'sedan'), {}).get('name', 'Sedan') |
| v2_type = VEHICLE_TYPES.get(vehicle_2.get('type', 'sedan'), {}).get('name', 'Sedan') |
| |
| html_content = f""" |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Traffic Accident Analysis Report</title> |
| <style> |
| * {{ |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| }} |
| |
| body {{ |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| line-height: 1.6; |
| color: #333; |
| background: #f5f5f5; |
| }} |
| |
| .container {{ |
| max-width: 1000px; |
| margin: 0 auto; |
| background: white; |
| box-shadow: 0 0 20px rgba(0,0,0,0.1); |
| }} |
| |
| .header {{ |
| background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); |
| color: white; |
| padding: 40px; |
| text-align: center; |
| }} |
| |
| .header h1 {{ |
| font-size: 2.5rem; |
| margin-bottom: 10px; |
| }} |
| |
| .header .subtitle {{ |
| opacity: 0.9; |
| font-size: 1.1rem; |
| }} |
| |
| .section {{ |
| padding: 30px 40px; |
| border-bottom: 1px solid #eee; |
| }} |
| |
| .section h2 {{ |
| color: #1e3a5f; |
| margin-bottom: 20px; |
| padding-bottom: 10px; |
| border-bottom: 2px solid #2d5a87; |
| }} |
| |
| .summary-grid {{ |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 20px; |
| margin: 20px 0; |
| }} |
| |
| .summary-card {{ |
| background: #f8f9fa; |
| padding: 20px; |
| border-radius: 10px; |
| text-align: center; |
| border-top: 4px solid #2d5a87; |
| }} |
| |
| .summary-card .label {{ |
| font-size: 0.9rem; |
| color: #666; |
| margin-bottom: 5px; |
| }} |
| |
| .summary-card .value {{ |
| font-size: 1.8rem; |
| font-weight: bold; |
| color: #1e3a5f; |
| }} |
| |
| .summary-card .delta {{ |
| font-size: 0.85rem; |
| margin-top: 5px; |
| }} |
| |
| .delta.high {{ color: #28a745; }} |
| .delta.medium {{ color: #ffc107; }} |
| .delta.low {{ color: #dc3545; }} |
| |
| .info-grid {{ |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 30px; |
| }} |
| |
| .info-box {{ |
| background: #f8f9fa; |
| padding: 20px; |
| border-radius: 10px; |
| }} |
| |
| .info-box h3 {{ |
| color: #1e3a5f; |
| margin-bottom: 15px; |
| }} |
| |
| .info-row {{ |
| display: flex; |
| justify-content: space-between; |
| padding: 8px 0; |
| border-bottom: 1px solid #eee; |
| }} |
| |
| .info-row:last-child {{ |
| border-bottom: none; |
| }} |
| |
| .vehicle-card {{ |
| background: white; |
| border-radius: 10px; |
| padding: 20px; |
| margin: 15px 0; |
| }} |
| |
| .vehicle-card.v1 {{ |
| border-left: 4px solid #FF4B4B; |
| }} |
| |
| .vehicle-card.v2 {{ |
| border-left: 4px solid #4B7BFF; |
| }} |
| |
| .scenario-card {{ |
| background: #f8f9fa; |
| border-radius: 10px; |
| padding: 20px; |
| margin: 15px 0; |
| border-left: 4px solid #2d5a87; |
| }} |
| |
| .scenario-header {{ |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 15px; |
| }} |
| |
| .probability {{ |
| font-size: 1.5rem; |
| font-weight: bold; |
| }} |
| |
| .probability.high {{ color: #28a745; }} |
| .probability.medium {{ color: #ffc107; }} |
| .probability.low {{ color: #dc3545; }} |
| |
| .progress-bar {{ |
| background: #e9ecef; |
| border-radius: 5px; |
| height: 10px; |
| margin: 10px 0; |
| overflow: hidden; |
| }} |
| |
| .progress-fill {{ |
| background: #2d5a87; |
| height: 100%; |
| border-radius: 5px; |
| }} |
| |
| .factors-list {{ |
| list-style: none; |
| padding: 0; |
| }} |
| |
| .factors-list li {{ |
| padding: 5px 0; |
| padding-left: 20px; |
| position: relative; |
| }} |
| |
| .factors-list li::before {{ |
| content: "•"; |
| color: #2d5a87; |
| position: absolute; |
| left: 0; |
| }} |
| |
| .timeline {{ |
| margin: 20px 0; |
| }} |
| |
| .timeline-item {{ |
| display: flex; |
| margin: 10px 0; |
| }} |
| |
| .timeline-time {{ |
| min-width: 80px; |
| padding: 8px 15px; |
| background: #ffc107; |
| color: white; |
| font-weight: bold; |
| text-align: center; |
| border-radius: 5px; |
| }} |
| |
| .timeline-time.impact {{ |
| background: #dc3545; |
| }} |
| |
| .timeline-time.after {{ |
| background: #28a745; |
| }} |
| |
| .timeline-event {{ |
| flex: 1; |
| padding: 8px 15px; |
| background: #f8f9fa; |
| margin-left: 10px; |
| border-radius: 5px; |
| }} |
| |
| .fault-assessment {{ |
| background: #fff3cd; |
| padding: 20px; |
| border-radius: 10px; |
| border-left: 4px solid #ffc107; |
| margin: 20px 0; |
| }} |
| |
| .footer {{ |
| background: #1e3a5f; |
| color: white; |
| padding: 30px 40px; |
| text-align: center; |
| }} |
| |
| .footer p {{ |
| opacity: 0.8; |
| font-size: 0.9rem; |
| }} |
| |
| @media print {{ |
| .container {{ |
| box-shadow: none; |
| }} |
| |
| .section {{ |
| page-break-inside: avoid; |
| }} |
| }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <!-- Header --> |
| <div class="header"> |
| <h1>🚗 Traffic Accident Analysis Report</h1> |
| <p class="subtitle">AI-Powered Analysis using Huawei MindSpore</p> |
| <p style="margin-top: 15px; opacity: 0.7;">Generated: {datetime.now().strftime("%B %d, %Y at %H:%M")}</p> |
| </div> |
| |
| <!-- Executive Summary --> |
| <div class="section"> |
| <h2>📊 Executive Summary</h2> |
| |
| <div class="summary-grid"> |
| <div class="summary-card"> |
| <div class="label">Most Likely Scenario</div> |
| <div class="value">#{most_likely.get('id', 1)}</div> |
| <div class="delta high">{most_likely.get('probability', 0)*100:.1f}% probability</div> |
| </div> |
| |
| <div class="summary-card"> |
| <div class="label">Scenarios Generated</div> |
| <div class="value">{len(scenarios)}</div> |
| <div class="delta">AI-generated</div> |
| </div> |
| |
| <div class="summary-card"> |
| <div class="label">Collision Certainty</div> |
| <div class="value">{results.get('overall_collision_probability', 0)*100:.1f}%</div> |
| <div class="delta {'high' if results.get('overall_collision_probability', 0) > 0.7 else 'medium' if results.get('overall_collision_probability', 0) > 0.4 else 'low'}"> |
| {'High' if results.get('overall_collision_probability', 0) > 0.7 else 'Medium' if results.get('overall_collision_probability', 0) > 0.4 else 'Low'} |
| </div> |
| </div> |
| |
| <div class="summary-card"> |
| <div class="label">Primary Factor</div> |
| <div class="value" style="font-size: 1.2rem;">{fault.get('primary_factor', 'N/A').replace('_', ' ').title()[:20]}</div> |
| <div class="delta">Vehicle {fault.get('likely_at_fault', '?')}</div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Accident Details --> |
| <div class="section"> |
| <h2>📍 Accident Details</h2> |
| |
| <div class="info-grid"> |
| <div class="info-box"> |
| <h3>Location Information</h3> |
| <div class="info-row"> |
| <span>Location:</span> |
| <strong>{accident_info.get('location', {}).get('name', 'Unknown')}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Coordinates:</span> |
| <strong>{accident_info.get('location', {}).get('latitude', 0):.4f}, {accident_info.get('location', {}).get('longitude', 0):.4f}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Road Type:</span> |
| <strong>{accident_info.get('road_type', 'Unknown').replace('_', ' ').title()}</strong> |
| </div> |
| </div> |
| |
| <div class="info-box"> |
| <h3>Conditions</h3> |
| <div class="info-row"> |
| <span>Date/Time:</span> |
| <strong>{accident_info.get('datetime', 'Not specified')}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Weather:</span> |
| <strong>{accident_info.get('weather', 'Unknown').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Road Condition:</span> |
| <strong>{accident_info.get('road_condition', 'Unknown').title()}</strong> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Vehicle Information --> |
| <div class="section"> |
| <h2>🚙 Vehicle Information</h2> |
| |
| <div class="info-grid"> |
| <div class="vehicle-card v1"> |
| <h3 style="color: #FF4B4B;">Vehicle 1 (Red)</h3> |
| <div class="info-row"> |
| <span>Type:</span> |
| <strong>{v1_type}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Speed:</span> |
| <strong>{vehicle_1.get('speed', 0)} km/h</strong> |
| </div> |
| <div class="info-row"> |
| <span>Direction:</span> |
| <strong>{vehicle_1.get('direction', 'Unknown').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Action:</span> |
| <strong>{vehicle_1.get('action', 'Unknown').replace('_', ' ').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Braking:</span> |
| <strong>{'Yes' if vehicle_1.get('braking', False) else 'No'}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Signaling:</span> |
| <strong>{'Yes' if vehicle_1.get('signaling', False) else 'No'}</strong> |
| </div> |
| </div> |
| |
| <div class="vehicle-card v2"> |
| <h3 style="color: #4B7BFF;">Vehicle 2 (Blue)</h3> |
| <div class="info-row"> |
| <span>Type:</span> |
| <strong>{v2_type}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Speed:</span> |
| <strong>{vehicle_2.get('speed', 0)} km/h</strong> |
| </div> |
| <div class="info-row"> |
| <span>Direction:</span> |
| <strong>{vehicle_2.get('direction', 'Unknown').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Action:</span> |
| <strong>{vehicle_2.get('action', 'Unknown').replace('_', ' ').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Braking:</span> |
| <strong>{'Yes' if vehicle_2.get('braking', False) else 'No'}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Signaling:</span> |
| <strong>{'Yes' if vehicle_2.get('signaling', False) else 'No'}</strong> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Generated Scenarios --> |
| <div class="section"> |
| <h2>🎯 AI-Generated Scenarios</h2> |
| |
| {''.join([f''' |
| <div class="scenario-card"> |
| <div class="scenario-header"> |
| <div> |
| <h3>Scenario {i+1}: {s['accident_type'].replace('_', ' ').title()}</h3> |
| </div> |
| <div class="probability {'high' if s['probability'] > 0.4 else 'medium' if s['probability'] > 0.2 else 'low'}"> |
| {s['probability']*100:.1f}% |
| </div> |
| </div> |
| |
| <p>{s['description']}</p> |
| |
| <div style="margin-top: 15px;"> |
| <strong>Analysis Metrics:</strong> |
| <div style="margin-top: 10px;"> |
| <span>Collision Probability: {s['metrics']['collision_probability']*100:.1f}%</span> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width: {s['metrics']['collision_probability']*100}%"></div> |
| </div> |
| </div> |
| <div> |
| <span>Path Overlap: {s['metrics']['path_overlap']*100:.1f}%</span> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width: {s['metrics']['path_overlap']*100}%"></div> |
| </div> |
| </div> |
| <p style="margin-top: 10px;">Speed Differential: {s['metrics']['speed_differential']:.1f} km/h | Time to Collision: {s['metrics']['time_to_collision']:.2f}s</p> |
| </div> |
| |
| <div style="margin-top: 15px;"> |
| <strong>Contributing Factors:</strong> |
| <ul class="factors-list"> |
| {''.join([f"<li>{f.replace('_', ' ').title()}</li>" for f in s['contributing_factors']])} |
| </ul> |
| </div> |
| </div> |
| ''' for i, s in enumerate(scenarios)])} |
| </div> |
| |
| <!-- Fault Assessment --> |
| <div class="section"> |
| <h2>⚖️ Preliminary Fault Assessment</h2> |
| |
| <div class="fault-assessment"> |
| <h3>⚠️ Disclaimer</h3> |
| <p>This is a preliminary AI-generated assessment for reference purposes only. Final fault determination should be made by qualified traffic authorities based on comprehensive investigation.</p> |
| </div> |
| |
| <div class="info-grid" style="margin-top: 20px;"> |
| <div class="info-box"> |
| <h3>Contribution Analysis</h3> |
| <div style="margin: 15px 0;"> |
| <span style="color: #FF4B4B;">Vehicle 1: {fault.get('vehicle_1_contribution', 50):.1f}%</span> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width: {fault.get('vehicle_1_contribution', 50)}%; background: #FF4B4B;"></div> |
| </div> |
| </div> |
| <div> |
| <span style="color: #4B7BFF;">Vehicle 2: {fault.get('vehicle_2_contribution', 50):.1f}%</span> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width: {fault.get('vehicle_2_contribution', 50)}%; background: #4B7BFF;"></div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="info-box"> |
| <h3>Assessment Summary</h3> |
| <div class="info-row"> |
| <span>Higher Contribution:</span> |
| <strong>Vehicle {fault.get('likely_at_fault', '?')}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Primary Factor:</span> |
| <strong>{fault.get('primary_factor', 'Unknown').replace('_', ' ').title()}</strong> |
| </div> |
| <div class="info-row"> |
| <span>Assessment Confidence:</span> |
| <strong>{fault.get('confidence', 0)*100:.1f}%</strong> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Timeline --> |
| <div class="section"> |
| <h2>⏱️ Estimated Accident Timeline</h2> |
| |
| <div class="timeline"> |
| {''.join([f''' |
| <div class="timeline-item"> |
| <div class="timeline-time {'impact' if e['time'] == 0 else 'after' if e['time'] > 0 else ''}">{e['time']:+.1f}s</div> |
| <div class="timeline-event">{e['event']}</div> |
| </div> |
| ''' for e in results.get('timeline', [])])} |
| </div> |
| </div> |
| |
| <!-- Footer --> |
| <div class="footer"> |
| <p><strong>Traffic Accident Reconstruction System</strong></p> |
| <p>Huawei AI Innovation Challenge 2026</p> |
| <p style="margin-top: 10px;">Powered by Huawei MindSpore AI Framework</p> |
| <p style="margin-top: 15px; font-size: 0.8rem;">Report ID: {timestamp}</p> |
| </div> |
| </div> |
| </body> |
| </html> |
| """ |
| |
| |
| report_path = REPORTS_DIR / f"accident_report_{timestamp}.html" |
| |
| with open(report_path, 'w', encoding='utf-8') as f: |
| f.write(html_content) |
| |
| return str(report_path) |
|
|
|
|
| def generate_pdf_report( |
| results: Dict[str, Any], |
| scenarios: List[Dict[str, Any]], |
| accident_info: Dict[str, Any], |
| vehicle_1: Dict[str, Any], |
| vehicle_2: Dict[str, Any], |
| timestamp: str, |
| include_maps: bool, |
| include_charts: bool, |
| include_raw_data: bool |
| ) -> str: |
| """Generate PDF report using ReportLab.""" |
| |
| try: |
| from reportlab.lib import colors |
| from reportlab.lib.pagesizes import A4 |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
| from reportlab.lib.units import inch, cm |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT |
| except ImportError: |
| |
| return generate_html_report( |
| results, scenarios, accident_info, vehicle_1, vehicle_2, |
| timestamp, include_maps, include_charts, include_raw_data |
| ) |
| |
| report_path = REPORTS_DIR / f"accident_report_{timestamp}.pdf" |
| |
| doc = SimpleDocTemplate( |
| str(report_path), |
| pagesize=A4, |
| rightMargin=72, |
| leftMargin=72, |
| topMargin=72, |
| bottomMargin=72 |
| ) |
| |
| styles = getSampleStyleSheet() |
| |
| |
| title_style = ParagraphStyle( |
| 'CustomTitle', |
| parent=styles['Heading1'], |
| fontSize=24, |
| spaceAfter=30, |
| alignment=TA_CENTER, |
| textColor=colors.HexColor('#1e3a5f') |
| ) |
| |
| heading_style = ParagraphStyle( |
| 'CustomHeading', |
| parent=styles['Heading2'], |
| fontSize=16, |
| spaceBefore=20, |
| spaceAfter=10, |
| textColor=colors.HexColor('#2d5a87') |
| ) |
| |
| story = [] |
| |
| |
| story.append(Paragraph("Traffic Accident Analysis Report", title_style)) |
| story.append(Paragraph("AI-Powered Analysis using Huawei MindSpore", styles['Normal'])) |
| story.append(Spacer(1, 30)) |
| |
| |
| story.append(Paragraph("Executive Summary", heading_style)) |
| |
| most_likely = results.get('most_likely_scenario', {}) |
| fault = results.get('preliminary_fault_assessment', {}) |
| |
| summary_data = [ |
| ['Most Likely Scenario', f"#{most_likely.get('id', 1)} ({most_likely.get('probability', 0)*100:.1f}%)"], |
| ['Scenarios Generated', str(len(scenarios))], |
| ['Collision Certainty', f"{results.get('overall_collision_probability', 0)*100:.1f}%"], |
| ['Primary Factor', fault.get('primary_factor', 'N/A').replace('_', ' ').title()] |
| ] |
| |
| summary_table = Table(summary_data, colWidths=[3*inch, 3*inch]) |
| summary_table.setStyle(TableStyle([ |
| ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8f9fa')), |
| ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), |
| ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), |
| ('FONTSIZE', (0, 0), (-1, -1), 10), |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 12), |
| ('TOPPADDING', (0, 0), (-1, -1), 12), |
| ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')) |
| ])) |
| |
| story.append(summary_table) |
| story.append(Spacer(1, 20)) |
| |
| |
| story.append(Paragraph("Accident Details", heading_style)) |
| |
| location_data = [ |
| ['Location', accident_info.get('location', {}).get('name', 'Unknown')], |
| ['Road Type', accident_info.get('road_type', 'Unknown').replace('_', ' ').title()], |
| ['Weather', accident_info.get('weather', 'Unknown').title()], |
| ['Road Condition', accident_info.get('road_condition', 'Unknown').title()] |
| ] |
| |
| location_table = Table(location_data, colWidths=[2*inch, 4*inch]) |
| location_table.setStyle(TableStyle([ |
| ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8f9fa')), |
| ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')), |
| ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), |
| ('FONTSIZE', (0, 0), (-1, -1), 10), |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 8), |
| ('TOPPADDING', (0, 0), (-1, -1), 8), |
| ])) |
| |
| story.append(location_table) |
| story.append(Spacer(1, 20)) |
| |
| |
| story.append(Paragraph("Generated Scenarios", heading_style)) |
| |
| for i, scenario in enumerate(scenarios): |
| story.append(Paragraph( |
| f"<b>Scenario {i+1}: {scenario['accident_type'].replace('_', ' ').title()}</b> - {scenario['probability']*100:.1f}% probability", |
| styles['Normal'] |
| )) |
| story.append(Paragraph(scenario['description'], styles['Normal'])) |
| story.append(Spacer(1, 10)) |
| |
| |
| doc.build(story) |
| |
| return str(report_path) |
|
|