Arihant0008's picture
Update app.py
1480e06 verified
raw
history blame
26.7 kB
import os
import base64
import json
import requests
import re
import numpy as np
import tensorflow as tf
from flask import Flask, request, render_template, make_response, session
from werkzeug.utils import secure_filename
import cv2
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from weasyprint import HTML
import datetime
import time
import csv
import traceback
import markdown # REQUIRED: For robust Markdown to HTML conversion
import secrets
# --- Initialization & Configuration ---
app = Flask(__name__)
# IMPORTANT: Replace "your-secret-key-here" with a long, random string.
app.secret_key = secrets.token_hex(32)
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
YOUR_SITE_URL = "https://huggingface.co/spaces/Arihant0008/Pneumonia-Detector-App"
YOUR_SITE_NAME = "Pneumonia Detection AI"
HF_TOKEN = os.environ.get("HF_TOKEN")
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["1 per day"],
storage_uri="memory://"
)
UPLOAD_FOLDER = 'static/uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
# --- CNN Model Loading FROM PRIVATE HF REPO ---
# --- CNN Model Loading (HYBRID WITH FALLBACK) ---
model = None
# Try HF first with proper /tmp/ paths
try:
from huggingface_hub import hf_hub_download
# CHANGED: Use HF_TOKEN_2 instead of HF_TOKEN
HF_TOKEN_2 = os.environ.get("HF_TOKEN_2")
if HF_TOKEN_2:
print("πŸ”„ Attempting to download model from private HF repository...")
# CRITICAL: Set environment variables for /tmp/ cache
os.environ['HF_HUB_CACHE'] = '/tmp/hf_cache'
os.environ['HUGGINGFACE_HUB_CACHE'] = '/tmp/hf_cache'
# Create directories
os.makedirs('/tmp/hf_cache', exist_ok=True)
os.makedirs('/tmp/model', exist_ok=True)
# Download with proper paths
MODEL_PATH = hf_hub_download(
repo_id="Arihant0008/pneumonia-resnet50-model",
filename="ResNet50_Pneumonia_model.keras",
token=HF_TOKEN_2,
local_dir="/tmp/model",
local_dir_use_symlinks=False # CRITICAL for Spaces
)
print(f"πŸ“₯ Model downloaded to: {MODEL_PATH}")
model = tf.keras.models.load_model(MODEL_PATH, compile=False)
print(f"βœ… CNN Model loaded from HF repo")
else:
print("⚠️ HF_TOKEN_2 not found - trying local model")
except Exception as hf_error:
print(f"⚠️ HF download failed: {str(hf_error)}")
print("πŸ”„ Falling back to local model...")
model = None
# Fallback to local model if HF failed
if model is None:
try:
LOCAL_PATH = 'model_weights/ResNet50_Pneumonia_model.keras'
if os.path.exists(LOCAL_PATH):
print(f"πŸ”„ Loading local model from {LOCAL_PATH}")
model = tf.keras.models.load_model(LOCAL_PATH, compile=False)
print(f"βœ… CNN Model loaded from local backup")
else:
print(f"❌ Local model not found at {LOCAL_PATH}")
model = None
except Exception as local_error:
print(f"❌ Local model loading failed: {str(local_error)}")
import traceback
traceback.print_exc()
model = None
if model is None:
print("❌❌❌ CRITICAL: Model not loaded from any source!")
else:
print("βœ…βœ…βœ… Model ready!")
# Model configuration
labels = ['PNEUMONIA', 'NORMAL']
img_size = 128
# --- Helper Functions ---
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def image_to_base64(filepath):
try:
with open(filepath, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
except Exception as e:
print(f"❌ Error encoding image to Base64: {e}")
return None
def preprocess_image(image_path):
try:
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img is None:
return None
img = cv2.resize(img, (img_size, img_size))
img = img / 255.0
return np.expand_dims(img, axis=0)
except Exception as e:
print(f"❌ Error preprocessing image: {e}")
return None
@app.route('/submit_interest', methods=['POST'])
@limiter.limit("10 per hour")
def submit_interest():
try:
# Get form data
user_name = request.form.get('user_name', '').strip()
user_email = request.form.get('user_email', '').strip()
user_message = request.form.get('user_message', '').strip()
# Validate inputs
if not user_name or not user_email or not user_message:
return render_template('index.html', error='All fields are required in the interest form.')
# Create timestamp
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Save to local CSV file (temporary)
interest_file = '/tmp/user_interests.csv'
file_exists = os.path.exists(interest_file)
with open(interest_file, 'a', newline='', encoding='utf-8') as csvfile:
fieldnames = ['timestamp', 'name', 'email', 'message']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write header if file is new
if not file_exists:
writer.writeheader()
# Write user data
writer.writerow({
'timestamp': timestamp,
'name': user_name,
'email': user_email,
'message': user_message
})
print(f"βœ… Interest submitted locally: {user_name} ({user_email})")
# ========== NEW: Upload to Hugging Face Dataset ==========
if HF_TOKEN:
try:
from huggingface_hub import HfApi
hf_api = HfApi()
# Upload the CSV to your dataset repository
hf_api.upload_file(
path_or_fileobj=interest_file,
path_in_repo='user_interests.csv',
repo_id='Arihant0008/AI-interests', # ⬅️ Change if you used different name
repo_type='dataset',
token=HF_TOKEN,
commit_message=f'New submission from {user_name}'
)
print(f"βœ… Uploaded to HF Dataset: {user_name}")
except Exception as upload_error:
print(f"⚠️ HF Dataset upload failed (data still saved locally): {upload_error}")
# Don't fail the whole request - data is still saved locally
else:
print("⚠️ HF_TOKEN not found - skipping dataset upload")
# ========== END NEW CODE ==========
return render_template('index.html', interest_success=True)
except Exception as e:
print(f"❌ Error saving interest: {e}")
traceback.print_exc()
return render_template('index.html', error=f'Submission error: {str(e)}')
@app.route('/check_csv')
def check_csv():
"""Debug route to check CSV status"""
interest_file = '/tmp/user_interests.csv'
if os.path.exists(interest_file):
with open(interest_file, 'r', encoding='utf-8') as f:
content = f.read()
return f"<h2>CSV Found! βœ…</h2><pre>{content}</pre>"
else:
return f"<h2>File not found ❌</h2><p>Looking for: {os.path.abspath(interest_file)}</p><p>Files in /tmp: {os.listdir('/tmp')}</p>"
@app.route('/download_csv')
def download_csv():
"""Download user interests CSV"""
from flask import send_file
interest_file = '/tmp/user_interests.csv'
if os.path.exists(interest_file):
return send_file(
interest_file,
mimetype='text/csv',
as_attachment=True,
download_name=f'user_interests_{datetime.datetime.now().strftime("%Y%m%d")}.csv'
)
else:
return "No submissions yet.", 404
# --- AI Report Generation ---
def get_ai_synthesis_report(image_base64, resnet_label, resnet_confidence, today_date):
if not OPENROUTER_API_KEY:
return "API service unavailable", "<h3>API Configuration Error</h3><p>OpenRouter API key not configured.</p>", False
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"HTTP-Referer": YOUR_SITE_URL,
"X-Title": YOUR_SITE_NAME
}
# FIXED: Enhanced prompt with prominent DATE display
user_prompt = f"""
You are a board-certified radiologist and AI medical imaging specialist with over 15 years of experience in chest X-ray interpretation and pneumonia diagnosis.
**GENERATE A COMPREHENSIVE RADIOLOGY REPORT WITH THE FOLLOWING EXACT STRUCTURE AND FORMATTING:**
# CHEST X-RAY ANALYSIS REPORT
**Report Date: {today_date}**
**Model: ResNet50 Deep Learning Architecture**
---
## PATIENT INFORMATION & STUDY DETAILS
- **Study Date:** {today_date}
- **Imaging Modality:** Posterior-Anterior (PA) Chest X-ray
- **AI Classification Result:** {resnet_label}
- **Model Confidence Level:** {resnet_confidence}
- **Analysis System:** ResNet50 Convolutional Neural Network
---
## MEDICAL DISCLAIMER
- This AI-generated report is for educational and informational purposes only
- This analysis does NOT constitute a clinical diagnosis or medical advice
- All findings require confirmation by qualified healthcare professionals
- Seek immediate medical attention for concerning symptoms
- This report should be used as a supplementary tool alongside clinical evaluation
---
## EXECUTIVE SUMMARY
Provide a concise 2-3 sentence clinical summary incorporating the CNN prediction ({resnet_label} at {resnet_confidence} confidence) and your comprehensive visual analysis of the chest radiograph.
---
## SYSTEMATIC RADIOLOGICAL ANALYSIS
---
### A. Technical Assessment
- **Image Quality:** Assess penetration, inspiration depth, patient positioning, and overall technical adequacy
- **Anatomical Coverage:** Evaluate whether all relevant chest structures are adequately visualized
- **Artifacts:** Identify any technical artifacts, overlapping structures, or positioning issues
### B. Cardiac Assessment
- **Cardiac Silhouette:** Analyze heart size, shape, and cardiothoracic ratio
- **Cardiac Borders:** Evaluate right heart border, left heart border, and aortic knob
- **Mediastinal Structures:** Assess mediastinal width, tracheal position, and hilar anatomy
- **Cardiovascular Abnormalities:** Note any cardiomegaly, mediastinal shift, or vascular congestion
### C. Pulmonary Parenchymal Analysis
- **Lung Fields:** Systematically evaluate both upper, middle, and lower lung zones
- **Aeration Pattern:** Assess lung expansion, symmetry, and overall aeration
- **Opacities:** Identify and characterize any consolidations, ground-glass opacities, or infiltrates
- **Pneumonia-Specific Findings:**
* Alveolar consolidation patterns
* Air bronchograms presence/absence
* Lobar vs bronchopneumonia distribution
* Pleural involvement assessment
- **Other Parenchymal Abnormalities:** Nodules, masses, cavitations, or interstitial changes
### D. Pleural Space Evaluation
- **Pleural Effusions:** Assess for fluid collections, quantify if present
- **Pneumothorax:** Evaluate for air in pleural space
- **Pleural Thickening:** Note any pleural abnormalities or calcifications
### E. Skeletal and Soft Tissue Assessment
- **Chest Wall:** Examine ribs, clavicles, and thoracic spine for fractures or abnormalities
- **Soft Tissues:** Evaluate for subcutaneous emphysema or masses
- **Diaphragm:** Assess diaphragmatic contours and position
## CLINICAL CORRELATION AND INTERPRETATION
---
### Primary Findings Discussion
Based on the CNN analysis showing **{resnet_label}** with **{resnet_confidence}** confidence:
- **If PNEUMONIA detected:** Provide detailed analysis of consolidation patterns, distribution (lobar/bronchopneumonia), severity assessment, and potential complications
- **If NORMAL detected:** Confirm normal anatomical structures, clear lung fields, and absence of pathological findings
### Differential Diagnosis Considerations
List 3-5 relevant differential diagnoses based on imaging findings, including:
- Most likely diagnosis based on imaging pattern
- Alternative diagnoses to consider
- Clinical correlation needed for definitive diagnosis
### Severity Assessment
- **Mild:** Limited involvement, normal cardiac silhouette
- **Moderate:** More extensive involvement, possible complications
- **Severe:** Extensive bilateral involvement, signs of respiratory compromise
## CLINICAL RECOMMENDATIONS
---
### Immediate Actions
- Urgency level based on findings (routine follow-up vs urgent evaluation)
- Need for supplemental oxygen or respiratory support
- Isolation precautions if infectious pneumonia suspected
### Diagnostic Workup
- **Laboratory Tests:** CBC with differential, blood cultures, sputum culture, inflammatory markers (CRP, procalcitonin)
- **Additional Imaging:** Consider CT chest if findings unclear, ultrasound for pleural effusions
- **Microbiological Studies:** Sputum gram stain and culture, blood cultures, urinary antigens
### Treatment Considerations
- **Outpatient vs Inpatient:** Management setting recommendations
- **Antibiotic Therapy:** Empirical treatment suggestions based on pattern
- **Supportive Care:** Oxygen therapy, fluid management, bronchodilators if indicated
- **Monitoring Parameters:** Vital signs, oxygen saturation, clinical response
### Follow-up Recommendations
- **Timeline:** When to repeat imaging (typically 6-8 weeks post-treatment for pneumonia)
- **Clinical Monitoring:** Symptom resolution, functional improvement
- **Red Flag Symptoms:** When to seek immediate medical attention
## RISK STRATIFICATION AND PROGNOSIS
- **Risk Factors:** Age, comorbidities, extent of disease
- **Prognostic Indicators:** Based on imaging extent and pattern
- **Expected Recovery Timeline:** Typical resolution patterns
## QUALITY ASSURANCE NOTES
- **AI Model Limitations:** Acknowledge CNN model constraints and potential false positives/negatives
- **Correlation Needed:** Emphasize importance of clinical correlation with symptoms, vital signs, and laboratory data
- **Second Opinion:** Recommend radiologist review for complex cases
---
## SIMPLIFIED PATIENT-FRIENDLY SUMMARY
**Plain Language Explanation for Non-Medical Readers:**
- This chest X-ray was analyzed using an advanced AI system called ResNet50.
- The system predicts: **{resnet_label}** with **{resnet_confidence}% confidence**.
- If pneumonia is detected, it means there may be an infection in the lungs causing cloudy areas on the X-ray.
- If the result is normal, it means the lungs, heart, and chest structures look healthy.
- This report is not a final diagnosis. A doctor must review the findings and decide the next steps.
- If you feel unwellβ€”especially with fever, cough, or breathing problemsβ€”please see a healthcare provider immediately.
Generate a comprehensive, detailed report following this structure while maintaining the highest standards of medical accuracy and clinical utility.
"""
report_models = [
# Tier 1: Best for medical analysis (primary)
"qwen/qwen2.5-vl-32b-instruct:free",
"google/gemini-2.0-flash-exp:free",
"meta-llama/llama-4-maverick:free",
# Tier 2: Good general performance (secondary)
"x-ai/grok-4-fast:free",
"meta-llama/llama-4-scout:free",
# Tier 3: Reliable fallbacks (tertiary)
"mistralai/mistral-small-3.2-24b-instruct:free",
"google/gemma-3-12b-it:free",
"qwen/qwen2.5-vl-72b-instruct:free"
]
for i, model_name in enumerate(report_models):
payload = {
"model": model_name,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
]
}],
"temperature": 0.1,
"max_tokens": 4000
}
try:
# Progressive timeout strategy
timeout = 60 + (i * 15) # 60s, 75s, 90s, etc.
print(f"πŸ“Š Generating report with model {i+1}/{len(report_models)}: {model_name}")
time.sleep(1) # Rate limiting courtesy
response = requests.post(OPENROUTER_API_URL, headers=headers,
data=json.dumps(payload), timeout=timeout)
if response.status_code == 200:
json_response = response.json()
raw_content = json_response.get('choices', [{}])[0].get('message', {}).get('content', '')
if raw_content and len(raw_content) > 200: # Ensure substantial content
html_content = markdown.markdown(raw_content)
print(f"βœ… Report generated successfully with {model_name}")
return raw_content, f"<div>{html_content}</div>", True
else:
print(f"⚠️ Model {model_name} returned insufficient content, trying next...")
continue
elif response.status_code == 429:
print(f"⏳ Model {model_name} rate limited, waiting and trying next...")
time.sleep(5)
continue
elif response.status_code in [503, 502, 500]:
print(f"πŸ”§ Model {model_name} service unavailable, trying next...")
continue
else:
print(f"❌ Model {model_name} failed with status {response.status_code}")
continue
except requests.exceptions.Timeout:
print(f"⏰ Model {model_name} timed out, trying next...")
continue
except requests.exceptions.RequestException as req_e:
print(f"πŸ’₯ Model {model_name} request failed: {str(req_e)}")
continue
# Fallback if all models fail
print("⚠️ All report generation models failed")
return "API service unavailable", "<h3>AI Service Unavailable</h3><p>Unable to generate detailed report - all models failed.</p>", False
# --- Download Report Route (CRITICAL FIXES HERE) ---
@app.route('/download_report', methods=['POST'])
@limiter.limit("1 per day")
def download_report():
print("πŸ” DEBUG: Processing download request.")
# 1. CRITICAL: Try to get the raw report content directly from the POST form data, then fall back to session.
raw_report = request.form.get('raw_report_content', session.get('last_report', 'API service unavailable'))
# 2. Check if we are truly in fallback mode
if raw_report == 'API service unavailable' or not raw_report.strip():
print("⚠️ Failed to retrieve full report content (API error). Generating minimal fallback PDF.")
# Fallback logic: Retrieves minimal data from session
cnn_label = session.get('last_cnn_label', 'UNKNOWN')
cnn_confidence = session.get('last_cnn_confidence', 'N/A')
today_date = datetime.datetime.now().strftime("%d-%m-%Y")
# Fallback content in Markdown format
raw_report = f"""
# Medical Analysis Report (Fallback Mode)
Date: {today_date}
## AI Service Unavailable
The detailed AI synthesis service is currently unreachable. This report contains only the raw machine learning prediction results.
## CNN Analysis Results
- **Classification:** {cnn_label}
- **Confidence Level:** {cnn_confidence}
- **Important Notice:** This is an automated prediction, not a clinical diagnosis. Consult a qualified medical professional for definitive evaluation and treatment recommendations.
"""
filename_label = cnn_label
is_fallback = True
else:
print("βœ… Full report content successfully retrieved for PDF generation.")
filename_label = session.get('last_cnn_label', 'AI_Report')
is_fallback = False
try:
# 3. Use markdown library to convert the RAW report content to HTML for the PDF
report_html_content = markdown.markdown(raw_report)
# Consistent PDF Styling for professional look
pdf_style = f"""
<style>
body {{ font-family: 'Segoe UI', Arial, sans-serif; font-size: 11pt; line-height: 1.6; color: #333; margin: 40px; }}
h1 {{ font-size: 20pt; color: #2c3e50; text-align: center; margin-bottom: 30px; border-bottom: 3px solid #3498db; padding-bottom: 15px; }}
h2 {{ font-size: 16pt; color: #34495e; margin-top: 25px; margin-bottom: 12px; border-bottom: 1px solid #eee; padding-bottom: 5px; }}
h3 {{ font-size: 14pt; color: #555; margin-top: 20px; margin-bottom: 10px; }}
ul, ol {{ padding-left: 25px; margin-bottom: 15px; }}
li {{ margin-bottom: 8px; }}
strong {{ font-weight: bold; color: #2c3e50; }}
p {{ margin: 8px 0; text-align: justify; }}
.disclaimer {{ background-color: {'#fff3cd' if is_fallback else '#e8f5e8'}; border-left: 5px solid {'#f39c12' if is_fallback else '#28a745'}; padding: 15px; margin: 20px 0; }}
</style>
"""
# 4. Construct the full HTML for WeasyPrint
full_html = f"<html><head><title>Medical Analysis Report</title>{pdf_style}</head><body>{report_html_content}</body></html>"
pdf = HTML(string=full_html).write_pdf()
response = make_response(pdf)
response.headers['Content-Type'] = 'application/pdf'
# 5. Set dynamic filename
today_date = datetime.datetime.now().strftime('%d-%m-%Y')
if is_fallback:
filename = f"Medical_Analysis_Fallback_{filename_label}_{today_date}.pdf"
else:
filename = f"AI_Medical_Report_{filename_label}_{today_date}.pdf"
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
print(f"βœ… PDF generated successfully: {filename}")
return response
except Exception as e:
print(f"❌ PDF generation failed: {e}")
traceback.print_exc()
return render_template('index.html', error="PDF generation failed. Please try again."), 500
# --- Index Route ---
@app.route('/', methods=['GET', 'POST'])
@limiter.limit("1 per day")
def index():
if request.method == 'POST':
if 'file' not in request.files or request.files['file'].filename == '':
return render_template('index.html', error='Please select a file to upload.')
file = request.files['file']
if not allowed_file(file.filename):
return render_template('index.html', error='Invalid file type. Only PNG, JPG, JPEG allowed.')
if model is None:
return render_template('index.html', error='CNN model not loaded.')
try:
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
base64_image = image_to_base64(filepath)
processed_image = preprocess_image(filepath)
if processed_image is None:
return render_template('index.html', error='Failed to preprocess image.')
# CORRECTED: CNN Prediction using ORIGINAL sigmoid logic from base code
prediction = model.predict(processed_image)
score = float(prediction[0][0])
# Get the class label and confidence (RESTORED FROM BASE CODE)
if score > 0.5:
label = labels[1] # NORMAL
confidence = score * 100
else:
label = labels[0] # PNEUMONIA
confidence = (1 - score) * 100
# Format confidence with percentage
confidence_formatted = f"{confidence:.1f}"
# Store session info (Crucial for fallback if direct form post fails)
session['last_cnn_label'] = label
session['last_cnn_confidence'] = confidence_formatted
session['last_image_path'] = filepath
today_date = datetime.datetime.now().strftime("%d-%m-%Y")
# AI Report
raw_report, html_report, success_flag = get_ai_synthesis_report(base64_image, label, confidence_formatted, today_date)
# CRITICAL: Store the raw Markdown string for PDF generation.
session['last_report'] = raw_report
return render_template('index.html',
prediction_text=label,
confidence_score=confidence_formatted,
image_path=filepath,
raw_report_text=raw_report, # Pass raw Markdown for download button hidden field
llm_explanation=html_report, # Pass converted HTML for display
success=success_flag)
except Exception as e:
print(f"❌ Error during processing: {e}")
traceback.print_exc()
return render_template('index.html', error='An error occurred during image analysis.')
return render_template('index.html')
# --- Error Handlers ---
@app.errorhandler(500)
def internal_server_error(e):
traceback.print_exc()
return render_template('index.html', error='Internal server error occurred.'), 500
@app.errorhandler(413)
def request_entity_too_large(e):
return render_template('index.html', error='File too large. Max 50 MB.'), 413
@app.errorhandler(429)
def ratelimit_handler(e):
return render_template('index.html', error=f'Rate limit exceeded: {e.description}'), 429
# --- Main ---
if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
if OPENROUTER_API_KEY:
print(f"πŸ”‘ OpenRouter API key configured: {OPENROUTER_API_KEY[:10]}...")
else:
print("⚠️ OpenRouter API key not found. AI reports will be unavailable.")
port = int(os.environ.get('PORT', 7860))
# Note: Flask's built-in server is used here for simplicity.
# For production, use Gunicorn/Waitress and remove debug=True.
app.run(debug=True, host='0.0.0.0', port=port)