feat: Enhance medical report generation with new features and improved data handling
- Added body fat percentage input and optional muscle oxygenation CSV upload in the upload form. - Implemented TSI chart generation based on muscle oxygenation data. - Updated report generation to include metabolism and fuel source charts. - Refactored context generation to eliminate reliance on SECA data, using patient info directly instead. - Improved error handling and logging for graph generation processes. - Enhanced HTML templates for better user experience and functionality.
This commit is contained in:
@@ -151,7 +151,7 @@ class ReportGeneratorService:
|
||||
}
|
||||
|
||||
def generate_html(
|
||||
self, patient_info: Dict[str, Any], context_list: List[Dict[str, Any]]
|
||||
self, patient_info: Dict[str, Any], contexts: Dict[str, Dict[str, Any]]
|
||||
) -> str:
|
||||
"""
|
||||
Generate HTML content for the report.
|
||||
@@ -159,7 +159,7 @@ class ReportGeneratorService:
|
||||
Args:
|
||||
patient_info: Dictionary containing patient information
|
||||
(patient_name, age, height, weight, focus)
|
||||
context_list: List of context dictionaries for each page
|
||||
contexts: Dictionary with keys 'page_1', 'page_2', etc., each containing context data
|
||||
|
||||
Returns:
|
||||
Complete HTML document as string
|
||||
@@ -175,6 +175,9 @@ class ReportGeneratorService:
|
||||
"focus": patient_info.get("focus", "Endurance"),
|
||||
}
|
||||
|
||||
# Get total number of pages
|
||||
num_pages = len(contexts)
|
||||
|
||||
# Footer context
|
||||
footer_context = [
|
||||
{
|
||||
@@ -183,7 +186,7 @@ class ReportGeneratorService:
|
||||
"social": "@ishplabs",
|
||||
"page_number": i + 1,
|
||||
}
|
||||
for i in range(len(context_list))
|
||||
for i in range(num_pages)
|
||||
]
|
||||
|
||||
# Render header
|
||||
@@ -195,11 +198,13 @@ class ReportGeneratorService:
|
||||
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)
|
||||
# Render pages - iterate through pages in order
|
||||
for i in range(1, num_pages + 1):
|
||||
page_key = f"page_{i}"
|
||||
context = contexts.get(page_key, {})
|
||||
template = self.env.get_template(f"page_{i}.html").render(context)
|
||||
|
||||
if (i + 1) > 2:
|
||||
if i > 2:
|
||||
full_html = f"""
|
||||
<div class="page flex flex-col justify-between">
|
||||
<div>
|
||||
@@ -209,7 +214,7 @@ class ReportGeneratorService:
|
||||
{template}
|
||||
</main>
|
||||
<div class="border-t text-center text-sm text-gray-600">
|
||||
{footer_html_list[i]}
|
||||
{footer_html_list[i - 1]}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@@ -284,10 +289,10 @@ class ReportGeneratorService:
|
||||
self,
|
||||
spirometry_pdf_path: str,
|
||||
pnoe_csv_path: str,
|
||||
seca_excel_path: str,
|
||||
patient_info: Dict[str, Any],
|
||||
output_filename: str = None,
|
||||
metric_overrides: Optional[Dict[str, Any]] = None,
|
||||
oxygenation_csv_path: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate complete medical report from uploaded files.
|
||||
@@ -325,69 +330,165 @@ class ReportGeneratorService:
|
||||
graphs_generated = self.generate_graphs(df)
|
||||
|
||||
# Create graph dictionary with base64 encoded images
|
||||
import base64
|
||||
|
||||
graphs_dict = {}
|
||||
for graph in graphs_generated:
|
||||
# Read the graph file and convert to base64
|
||||
graph_path = Path(graph["path"])
|
||||
if graph_path.exists():
|
||||
import base64
|
||||
|
||||
with open(graph_path, "rb") as f:
|
||||
graphs_dict[graph["name"]] = base64.b64encode(f.read()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
# Also generate body composition charts
|
||||
# Extract patient data for these charts
|
||||
patient_name = patient_info.get("patient_name", "").split()[-1] # Get last name
|
||||
# Use patient info directly (no SECA file needed)
|
||||
fat_pct = patient_info.get("fat_percentage", 0)
|
||||
age = patient_info.get("age", 25)
|
||||
gender = patient_info.get("gender", "female").lower()
|
||||
|
||||
# Load SECA data to get body composition info
|
||||
seca_df = pd.read_excel(seca_excel_path)
|
||||
patient_data = seca_df[
|
||||
seca_df["LastName"].str.contains(patient_name, case=False, na=False)
|
||||
]
|
||||
# Convert weight to kg if needed
|
||||
weight_str = str(patient_info.get("weight", "0"))
|
||||
# Extract numeric value and unit
|
||||
weight_str_clean = (
|
||||
weight_str.replace("lbs", "").replace("kg", "").replace(" ", "").strip()
|
||||
)
|
||||
try:
|
||||
weight_value = float(weight_str_clean)
|
||||
except ValueError:
|
||||
print(f"Warning: Could not parse weight '{weight_str}', using default 0")
|
||||
weight_value = 0.0
|
||||
|
||||
if not patient_data.empty:
|
||||
row = patient_data.iloc[0]
|
||||
weight_kg = float(row.get("Weight", 0))
|
||||
fat_pct = float(row.get("Adult_FMP", 0))
|
||||
age = int(row.get("Age", patient_info.get("age", 25)))
|
||||
gender = row.get("Gender", "female").lower()
|
||||
# Convert to kg if weight is in lbs
|
||||
if "lbs" in weight_str.lower():
|
||||
weight_kg = weight_value / 2.20462 # Convert lbs to kg
|
||||
else:
|
||||
weight_kg = weight_value # Already in kg or assume kg if no unit specified
|
||||
|
||||
fat_mass_lbs = weight_kg * fat_pct / 100 * 2.20462
|
||||
lean_mass_lbs = weight_kg * (1 - fat_pct / 100) * 2.20462
|
||||
# Calculate fat and lean mass in pounds
|
||||
fat_mass_lbs = weight_kg * fat_pct / 100 * 2.20462
|
||||
lean_mass_lbs = weight_kg * (1 - fat_pct / 100) * 2.20462
|
||||
|
||||
# Generate body composition chart
|
||||
body_comp_b64 = self.graph_generator.generate_body_composition_chart(
|
||||
fat_mass_lbs, lean_mass_lbs, save_as_base64=True
|
||||
# Generate body composition chart (save as file first, then convert to base64)
|
||||
try:
|
||||
body_comp_path = self.graph_generator.generate_body_composition_chart(
|
||||
fat_mass_lbs, lean_mass_lbs, save_as_base64=False
|
||||
)
|
||||
graphs_dict["body_composition"] = body_comp_b64
|
||||
|
||||
# Generate body fat percent chart
|
||||
body_fat_b64 = self.graph_generator.generate_body_fat_percent_chart(
|
||||
fat_pct, age, gender, save_as_base64=True
|
||||
graphs_generated.append(
|
||||
{"name": "body_composition", "path": str(body_comp_path)}
|
||||
)
|
||||
graphs_dict["body_fat_percent"] = body_fat_b64
|
||||
# Convert to base64 for graphs_dict
|
||||
with open(body_comp_path, "rb") as f:
|
||||
graphs_dict["body_composition"] = base64.b64encode(f.read()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate body composition chart: {e}")
|
||||
graphs_dict["body_composition"] = ""
|
||||
|
||||
# Generate body fat percent chart (save as file first, then convert to base64)
|
||||
try:
|
||||
body_fat_path = self.graph_generator.generate_body_fat_percent_chart(
|
||||
fat_pct, age, gender, save_as_base64=False
|
||||
)
|
||||
graphs_generated.append(
|
||||
{"name": "body_fat_percent", "path": str(body_fat_path)}
|
||||
)
|
||||
# Convert to base64 for graphs_dict
|
||||
with open(body_fat_path, "rb") as f:
|
||||
graphs_dict["body_fat_percent"] = base64.b64encode(f.read()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate body fat percent chart: {e}")
|
||||
graphs_dict["body_fat_percent"] = ""
|
||||
|
||||
# Generate spirometry chart
|
||||
print("Step 4: Generating spirometry chart...")
|
||||
try:
|
||||
spirometry_df = pd.read_csv(spirometry_csv_path)
|
||||
print(f"Spirometry data loaded: {len(spirometry_df)} rows")
|
||||
print(f"Spirometry columns: {spirometry_df.columns.tolist()}")
|
||||
if "Parameters" in spirometry_df.columns:
|
||||
print(f"Available parameters: {spirometry_df['Parameters'].tolist()}")
|
||||
spirometry_chart_b64 = self.graph_generator.generate_spirometry_chart(
|
||||
spirometry_df, save_as_base64=True
|
||||
)
|
||||
graphs_dict["spirometry_chart"] = spirometry_chart_b64
|
||||
print("Spirometry chart generated successfully")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Warning: Could not generate spirometry chart: {e}")
|
||||
print(f"Error details: {error_details}")
|
||||
graphs_dict["spirometry_chart"] = ""
|
||||
|
||||
# Generate TSI chart if oxygenation CSV is provided
|
||||
if oxygenation_csv_path:
|
||||
print("Step 4.5: Generating TSI chart...")
|
||||
try:
|
||||
oxygenation_df = pd.read_csv(oxygenation_csv_path)
|
||||
tsi_chart_b64 = self.graph_generator.generate_tsi_chart(
|
||||
oxygenation_df, save_as_base64=True
|
||||
)
|
||||
graphs_dict["tsi_chart"] = tsi_chart_b64
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate TSI chart: {e}")
|
||||
graphs_dict["tsi_chart"] = ""
|
||||
|
||||
# Generate metabolism and fuel source charts for page 5
|
||||
print("Step 4.6: Generating metabolism and fuel source charts...")
|
||||
try:
|
||||
# Calculate RMR and fuel source from pnoe data
|
||||
from services.context_generator import ContextGenerator
|
||||
|
||||
temp_context_gen = ContextGenerator()
|
||||
temp_context_gen.load_data(pnoe_csv_path, str(spirometry_csv_path), None)
|
||||
temp_context_gen.patient_info = {
|
||||
"name": patient_info.get("first_name", ""),
|
||||
"last_name": patient_info.get("last_name", ""),
|
||||
"age": patient_info.get("age", 25),
|
||||
"weight": weight_kg,
|
||||
"fat_percentage": fat_pct,
|
||||
"gender": gender,
|
||||
}
|
||||
rmr_metrics = temp_context_gen.calculate_rmr_and_fuel_source()
|
||||
|
||||
# Generate metabolism chart
|
||||
metabolism_chart_b64 = self.graph_generator.generate_metabolism_chart(
|
||||
rmr_metrics["rmr_kcal"], save_as_base64=True
|
||||
)
|
||||
graphs_dict["metabolism_chart"] = metabolism_chart_b64
|
||||
|
||||
# Generate fuel source chart
|
||||
fuel_source_chart_b64 = self.graph_generator.generate_fuel_source_chart(
|
||||
rmr_metrics["rest_fat_percentage"], save_as_base64=True
|
||||
)
|
||||
graphs_dict["fuel_source_chart"] = fuel_source_chart_b64
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate metabolism/fuel source charts: {e}")
|
||||
graphs_dict["metabolism_chart"] = ""
|
||||
graphs_dict["fuel_source_chart"] = ""
|
||||
|
||||
# Step 5: Generate context for all pages
|
||||
print("Step 5: Generating page contexts...")
|
||||
patient_name = patient_info.get("patient_name", "")
|
||||
self.context_generator.load_data(
|
||||
pnoe_csv_path, str(spirometry_csv_path), seca_excel_path
|
||||
pnoe_csv_path,
|
||||
str(spirometry_csv_path),
|
||||
None, # No SECA file
|
||||
)
|
||||
context_list = self.context_generator.generate_all_contexts(
|
||||
# Set patient info manually
|
||||
self.context_generator.patient_info = {
|
||||
"name": patient_info.get("first_name", ""),
|
||||
"last_name": patient_info.get("last_name", ""),
|
||||
"age": patient_info.get("age", 25),
|
||||
"weight": weight_kg,
|
||||
"fat_percentage": fat_pct,
|
||||
"gender": gender,
|
||||
}
|
||||
contexts = self.context_generator.generate_all_contexts(
|
||||
patient_name, graphs_dict, metric_overrides=metric_overrides
|
||||
)
|
||||
|
||||
@@ -396,7 +497,7 @@ class ReportGeneratorService:
|
||||
analysis_data["graphs_count"] = len(graphs_generated)
|
||||
|
||||
# Step 6: Generate HTML
|
||||
html_content = self.generate_html(patient_info, context_list)
|
||||
html_content = self.generate_html(patient_info, contexts)
|
||||
|
||||
# Step 7: Generate PDF
|
||||
if output_filename is None:
|
||||
|
||||
Reference in New Issue
Block a user