""" Report Generator Service This service handles the generation of medical reports from uploaded files. It processes data, generates graphs, and creates PDF reports. """ from pathlib import Path from typing import Any, Dict, List, Optional import pandas as pd from jinja2 import Environment, FileSystemLoader from playwright.async_api import async_playwright from services.context_generator import ContextGenerator from services.graph_generator import GraphGenerator from services.spirometry_table_extractor import extract_spirometry_table_from_pdf class ReportGeneratorService: """Service for generating medical performance reports""" def __init__( self, template_dir: str = "app/report_gen", graphs_dir: str = "graphs", reports_dir: str = "reports", data_dir: str = "data", ): """ Initialize the report generator service. Args: template_dir: Directory containing Jinja2 templates graphs_dir: Directory to save generated graphs reports_dir: Directory to save generated reports data_dir: Directory to store extracted/processed data """ self.template_dir = template_dir self.graphs_dir = Path(graphs_dir) self.reports_dir = Path(reports_dir) self.data_dir = Path(data_dir) self.graph_generator = GraphGenerator(charts_dir=str(self.graphs_dir)) self.context_generator = ContextGenerator() self.env = Environment(loader=FileSystemLoader(template_dir)) # Ensure directories exist self.graphs_dir.mkdir(exist_ok=True) self.reports_dir.mkdir(exist_ok=True) self.data_dir.mkdir(exist_ok=True) def process_pnoe_data(self, pnoe_csv_path: str) -> pd.DataFrame: """ Load and process Pnoe CSV data. Args: pnoe_csv_path: Path to Pnoe CSV file Returns: Processed DataFrame with smoothed columns """ # Load data df = pd.read_csv(pnoe_csv_path, delimiter=";") # Convert numeric columns (updated approach) for col in df.columns: try: df[col] = pd.to_numeric(df[col]) except (ValueError, TypeError): pass # Keep as-is if not numeric # Calculate derived columns df["VO2 Pulse"] = df["VO2(ml/min)"] / df["HR(bpm)"] df["VO2 Breath"] = df["VO2(ml/min)"] / df["BF(bpm)"] df["CHO"] = df["EE(kcal/min)"] * df["CARBS(%)"] / 100 df["FAT"] = df["EE(kcal/min)"] * df["FAT(%)"] / 100 # Smooth columns window_size = 10 columns_to_smooth = [ "VO2(ml/min)", "VCO2(ml/min)", "HR(bpm)", "VT(l)", "BF(bpm)", "VE(l/min)", "VO2 Pulse", "VO2 Breath", "CHO", "FAT", ] for col in columns_to_smooth: if col in df.columns: df[f"{col}_smoothed"] = ( df[col].rolling(window=window_size, min_periods=1).mean() ) return df def generate_graphs(self, df: pd.DataFrame) -> List[Dict[str, str]]: """ Generate all required graphs from processed data. Args: df: Processed DataFrame with smoothed columns Returns: List of dictionaries containing graph names and paths """ graphs_generated = [] # List of graphs to generate graph_methods = [ ("respiratory", self.graph_generator.generate_respiratory_chart), ("fuel_utilization", self.graph_generator.generate_fuel_utilization_chart), ("vo2_pulse", self.graph_generator.generate_vo2_pulse_chart), ("vo2_breath", self.graph_generator.generate_vo2_breath_chart), ("fat_metabolism", self.graph_generator.generate_fat_metabolism_chart), ("recovery", self.graph_generator.generate_recovery_chart), ] for name, method in graph_methods: try: path = method(df, save_as_base64=False) graphs_generated.append({"name": name, "path": str(path)}) except Exception as e: print(f"Warning: Could not generate {name} chart: {e}") return graphs_generated def calculate_analysis_metrics(self, df: pd.DataFrame) -> Dict[str, Any]: """ Calculate basic analysis metrics from processed data. Args: df: Processed DataFrame with smoothed columns Returns: Dictionary containing analysis metrics """ return { "vo2_max": float(df["VO2(ml/min)_smoothed"].max()) if "VO2(ml/min)_smoothed" in df.columns else 0, "peak_vt": float(df["VT(l)_smoothed"].max()) if "VT(l)_smoothed" in df.columns else 0, "max_hr": float(df["HR(bpm)_smoothed"].max()) if "HR(bpm)_smoothed" in df.columns else 0, } def generate_html( self, patient_info: Dict[str, Any], context_list: List[Dict[str, Any]] ) -> str: """ Generate HTML content for the report. Args: patient_info: Dictionary containing patient information (patient_name, age, height, weight, focus) context_list: List of context dictionaries for each page Returns: Complete HTML document as string """ html_pages = [] # Header context header_context = { "patient_name": patient_info.get("patient_name", ""), "age": patient_info.get("age", ""), "height": patient_info.get("height", ""), "weight": patient_info.get("weight", ""), "focus": patient_info.get("focus", "Endurance"), } # Footer context footer_context = [ { "contact_email": "info@ishplabs.com", "website": "www.ishplabs.com", "social": "@ishplabs", "page_number": i + 1, } for i in range(len(context_list)) ] # Render header header_html = self.env.get_template("header.html").render(header_context) # Render footers footer_html_list = [ self.env.get_template("footer.html").render(context) for context in footer_context ] # Render pages for i, context in enumerate(context_list): template = self.env.get_template(f"page_{i + 1}.html").render(context) if (i + 1) > 2: full_html = f"""