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:
bolade
2025-11-18 16:57:39 +01:00
parent 83f50882e2
commit 7e985c497e
12 changed files with 1256 additions and 262 deletions
+139 -38
View File
@@ -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: