|
|
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 |
|
|
import secrets |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
|
|
|
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'} |
|
|
|
|
|
|
|
|
|
|
|
model = None |
|
|
|
|
|
|
|
|
try: |
|
|
from huggingface_hub import hf_hub_download |
|
|
|
|
|
|
|
|
HF_TOKEN_2 = os.environ.get("HF_TOKEN_2") |
|
|
|
|
|
if HF_TOKEN_2: |
|
|
print("π Attempting to download model from private HF repository...") |
|
|
|
|
|
|
|
|
os.environ['HF_HUB_CACHE'] = '/tmp/hf_cache' |
|
|
os.environ['HUGGINGFACE_HUB_CACHE'] = '/tmp/hf_cache' |
|
|
|
|
|
|
|
|
os.makedirs('/tmp/hf_cache', exist_ok=True) |
|
|
os.makedirs('/tmp/model', exist_ok=True) |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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!") |
|
|
|
|
|
|
|
|
labels = ['PNEUMONIA', 'NORMAL'] |
|
|
img_size = 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
user_name = request.form.get('user_name', '').strip() |
|
|
user_email = request.form.get('user_email', '').strip() |
|
|
user_message = request.form.get('user_message', '').strip() |
|
|
|
|
|
|
|
|
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.') |
|
|
|
|
|
|
|
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if not file_exists: |
|
|
writer.writeheader() |
|
|
|
|
|
|
|
|
writer.writerow({ |
|
|
'timestamp': timestamp, |
|
|
'name': user_name, |
|
|
'email': user_email, |
|
|
'message': user_message |
|
|
}) |
|
|
|
|
|
print(f"β
Interest submitted locally: {user_name} ({user_email})") |
|
|
|
|
|
|
|
|
if HF_TOKEN: |
|
|
try: |
|
|
from huggingface_hub import HfApi |
|
|
hf_api = HfApi() |
|
|
|
|
|
|
|
|
hf_api.upload_file( |
|
|
path_or_fileobj=interest_file, |
|
|
path_in_repo='user_interests.csv', |
|
|
repo_id='Arihant0008/AI-interests', |
|
|
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}") |
|
|
|
|
|
else: |
|
|
print("β οΈ HF_TOKEN not found - skipping dataset upload") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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 = [ |
|
|
|
|
|
"qwen/qwen2.5-vl-32b-instruct:free", |
|
|
"google/gemini-2.0-flash-exp:free", |
|
|
"meta-llama/llama-4-maverick:free", |
|
|
|
|
|
|
|
|
"x-ai/grok-4-fast:free", |
|
|
"meta-llama/llama-4-scout:free", |
|
|
|
|
|
|
|
|
"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: |
|
|
|
|
|
timeout = 60 + (i * 15) |
|
|
|
|
|
print(f"π Generating report with model {i+1}/{len(report_models)}: {model_name}") |
|
|
|
|
|
time.sleep(1) |
|
|
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: |
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/download_report', methods=['POST']) |
|
|
@limiter.limit("1 per day") |
|
|
def download_report(): |
|
|
print("π DEBUG: Processing download request.") |
|
|
|
|
|
|
|
|
raw_report = request.form.get('raw_report_content', session.get('last_report', 'API service unavailable')) |
|
|
|
|
|
|
|
|
if raw_report == 'API service unavailable' or not raw_report.strip(): |
|
|
print("β οΈ Failed to retrieve full report content (API error). Generating minimal fallback PDF.") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
report_html_content = markdown.markdown(raw_report) |
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
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' |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
@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.') |
|
|
|
|
|
|
|
|
prediction = model.predict(processed_image) |
|
|
score = float(prediction[0][0]) |
|
|
|
|
|
|
|
|
if score > 0.5: |
|
|
label = labels[1] |
|
|
confidence = score * 100 |
|
|
else: |
|
|
label = labels[0] |
|
|
confidence = (1 - score) * 100 |
|
|
|
|
|
|
|
|
confidence_formatted = f"{confidence:.1f}" |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
raw_report, html_report, success_flag = get_ai_synthesis_report(base64_image, label, confidence_formatted, today_date) |
|
|
|
|
|
|
|
|
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, |
|
|
llm_explanation=html_report, |
|
|
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') |
|
|
|
|
|
|
|
|
|
|
|
@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 |
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
app.run(debug=True, host='0.0.0.0', port=port) |
|
|
|