Pratik0112 commited on
Commit
43353f5
·
1 Parent(s): ab341b8

Initial commit

Browse files
Files changed (4) hide show
  1. Dockerfile +31 -0
  2. app.py +213 -0
  3. requirements.txt +8 -0
  4. templates/index.html +174 -0
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+ FROM python:3.9-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y \
6
+ tesseract-ocr \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set working directory
10
+ WORKDIR /app
11
+
12
+ # Copy requirements and install dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Create uploads directory
17
+ RUN mkdir -p uploads && chmod -R 777 uploads
18
+
19
+ # Copy application files
20
+ COPY app.py .
21
+ COPY templates/index.html templates/
22
+
23
+ # Set environment variables
24
+ ENV PYTHONUNBUFFERED=1
25
+ ENV PORT=7860
26
+
27
+ # Expose port
28
+ EXPOSE 7860
29
+
30
+ # Command to run the application
31
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860","--timeout", "240", "app:app"]
app.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from flask import Flask, request, render_template, redirect, flash, url_for
4
+ from werkzeug.utils import secure_filename
5
+ import PyPDF2
6
+ from PIL import Image
7
+ import io
8
+ import base64
9
+ import google.generativeai as genai
10
+ import pytesseract
11
+ import markdown
12
+
13
+ app = Flask(__name__)
14
+ app.secret_key = 'your_secret_key_here'
15
+ logging.basicConfig(level=logging.DEBUG)
16
+
17
+ # Configure upload settings
18
+ UPLOAD_FOLDER = 'uploads'
19
+ ALLOWED_EXTENSIONS = {'pdf', 'jpg', 'jpeg', 'png'}
20
+ MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB max file size
21
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
22
+ app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE
23
+
24
+ # Configure Gemini API
25
+ GEMINI_MODEL = "gemini-2.0-flash" # Updated to use the correct model name
26
+ genai.configure(api_key="AIzaSyArihOGcyK5KcQR4ntIqNga6bSoq7kM7Yo")
27
+
28
+ # Ensure the upload folder exists
29
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
30
+
31
+ def setup_gemini():
32
+ """Initialize Gemini API with error handling"""
33
+ try:
34
+ genai.configure(api_key="AIzaSyArihOGcyK5KcQR4ntIqNga6bSoq7kM7Yo")
35
+ return True
36
+ except Exception as e:
37
+ logging.error(f"Failed to configure Gemini API: {str(e)}")
38
+ return False
39
+
40
+ def allowed_file(filename):
41
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
42
+
43
+ def extract_images_from_pdf(pdf_path):
44
+ """Extract images from PDF file and convert to base64"""
45
+ images_data = []
46
+ try:
47
+ with open(pdf_path, "rb") as pdf_file:
48
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
49
+ for page in pdf_reader.pages:
50
+ if hasattr(page, 'images'):
51
+ for image_file_object in page.images:
52
+ try:
53
+ image = Image.open(io.BytesIO(image_file_object.data))
54
+ buffered = io.BytesIO()
55
+ image.save(buffered, format="PNG")
56
+ img_str = base64.b64encode(buffered.getvalue()).decode()
57
+ images_data.append(img_str)
58
+ except Exception as e:
59
+ logging.warning(f"Failed to process image: {str(e)}")
60
+ except Exception as e:
61
+ logging.error(f"Error extracting images: {str(e)}")
62
+ return images_data
63
+
64
+ def extract_text_from_image(image_path):
65
+ """Extract text from image using OCR"""
66
+ try:
67
+ image = Image.open(image_path)
68
+ text = pytesseract.image_to_string(image)
69
+ return text
70
+ except Exception as e:
71
+ logging.error(f"Error extracting text from image: {str(e)}")
72
+ return ""
73
+
74
+ def get_scan_type(text):
75
+ """Determine the type of scan from the text content"""
76
+ text_lower = text.lower()
77
+ scan_types = {
78
+ 'electrocardiogram': 'ECG'
79
+ }
80
+
81
+ for key, value in scan_types.items():
82
+ if key in text_lower:
83
+ return value
84
+ return 'Unknown Scan Type'
85
+
86
+ def analyze_medical_scan(scan_type, combined_text, image_count):
87
+ """Generate analysis """
88
+ try:
89
+ logging.debug(f"Initializing Gemini model with type: {GEMINI_MODEL}")
90
+ model = genai.GenerativeModel(GEMINI_MODEL)
91
+
92
+ prompt = f"""
93
+ As a professional medical imaging specialist, analyze this {scan_type} report.
94
+ Please provide a clear analysis of the following points:
95
+ 1. Key findings and observations
96
+ 2. Any significant abnormalities or concerns
97
+ 3. Technical quality of the scan
98
+ 4. Recommendations for follow-up (if any)
99
+
100
+ Report details:
101
+ - Number of images: {image_count}
102
+ - Text content: {combined_text}
103
+
104
+ Note any limitations if image quality or content is unclear.
105
+ """
106
+
107
+ logging.debug("Sending request to Gemini API")
108
+ response = model.generate_content(prompt)
109
+ logging.debug("Received response from Gemini API")
110
+
111
+ if not response or not hasattr(response, 'text'):
112
+ raise ValueError("Invalid response from Gemini API")
113
+
114
+ return response.text
115
+ except Exception as e:
116
+ logging.error(f"Error generating analysis: {str(e)}")
117
+ raise
118
+
119
+ @app.route('/', methods=['GET', 'POST'])
120
+ def index():
121
+ if not setup_gemini():
122
+ flash('Failed to initialize AI service. Please try again later.', 'error')
123
+ return render_template("index.html", result=None)
124
+
125
+ result = None
126
+ if request.method == 'POST':
127
+ logging.debug("Received a POST request.")
128
+
129
+ # Check if the post request has the file part
130
+ if 'file' not in request.files:
131
+ logging.debug("No file part in request")
132
+ flash('No file selected', 'error')
133
+ return redirect(request.url)
134
+
135
+ file = request.files['file']
136
+
137
+ # If user does not select file, browser also
138
+ # submit an empty part without filename
139
+ if file.filename == '':
140
+ logging.debug("No selected file")
141
+ flash('No file selected', 'error')
142
+ return redirect(request.url)
143
+
144
+ # Check file size
145
+ if request.content_length > MAX_FILE_SIZE:
146
+ logging.debug("File too large")
147
+ flash(f'File size exceeds {MAX_FILE_SIZE // (1024 * 1024)}MB limit', 'error')
148
+ return redirect(request.url)
149
+
150
+ if file and allowed_file(file.filename):
151
+ file_path = None
152
+ try:
153
+ filename = secure_filename(file.filename)
154
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
155
+ logging.debug(f"Saving file to: {file_path}")
156
+ file.save(file_path)
157
+
158
+ # Process text and images
159
+ combined_text = ""
160
+ if filename.lower().endswith('.pdf'):
161
+ logging.debug("Processing PDF file")
162
+ with open(file_path, "rb") as pdf_file:
163
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
164
+ for page_num, page in enumerate(pdf_reader.pages):
165
+ page_text = page.extract_text()
166
+ if page_text:
167
+ combined_text += page_text
168
+ logging.debug(f"Extracted text from page {page_num}")
169
+ images_data = extract_images_from_pdf(file_path)
170
+ else: # Image file
171
+ logging.debug("Processing image file")
172
+ combined_text = extract_text_from_image(file_path)
173
+ with open(file_path, "rb") as img_file:
174
+ img_data = base64.b64encode(img_file.read()).decode()
175
+ images_data = [img_data]
176
+
177
+ logging.debug(f"Extracted text length: {len(combined_text)}")
178
+ scan_type = get_scan_type(combined_text)
179
+ image_count = len(images_data)
180
+ logging.debug(f"Detected scan type: {scan_type}, Image count: {image_count}")
181
+
182
+ # Generate analysis
183
+ logging.debug("Generating analysis")
184
+ analysis_text = analyze_medical_scan(scan_type, combined_text, image_count)
185
+ analysis_html = markdown.markdown(analysis_text)
186
+
187
+ result = {
188
+ 'analysis': analysis_text,
189
+ 'analysis_html': analysis_html,
190
+ 'scan_type': scan_type,
191
+ 'image_count': image_count,
192
+ 'images': images_data
193
+ }
194
+ logging.debug("Analysis complete")
195
+
196
+ except Exception as e:
197
+ logging.exception("Error processing file:")
198
+ flash(f"Error processing file: {str(e)}", 'error')
199
+ return redirect(request.url)
200
+ finally:
201
+ # Clean up uploaded file
202
+ if file_path and os.path.exists(file_path):
203
+ os.remove(file_path)
204
+ logging.debug("Temporary file removed after processing.")
205
+ else:
206
+ logging.debug("Invalid file type")
207
+ flash('Only PDF and image files are allowed', 'error')
208
+ return redirect(request.url)
209
+
210
+ return render_template("index.html", result=result)
211
+
212
+ if __name__ == '__main__':
213
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ flask==2.0.1
2
+ Werkzeug==2.0.1
3
+ PyPDF2==3.0.1
4
+ Pillow==9.5.0
5
+ Markdown
6
+ google-generativeai==0.3.0
7
+ pytesseract==0.3.10
8
+ gunicorn==20.1.0
templates/index.html ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ECG Scan Analyzer</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/2.3.0/alpine.js"></script>
9
+ </head>
10
+ <body class="bg-gray-50 min-h-screen">
11
+ <div x-data="{
12
+ isDragging: false,
13
+ isAnalyzing: false,
14
+ fileName: '',
15
+ selectedScan: 'ECG',
16
+ handleFile(event) {
17
+ const file = event.target.files[0];
18
+ if (file) {
19
+ this.fileName = file.name;
20
+ document.querySelector('form').submit();
21
+ this.isAnalyzing = true;
22
+ }
23
+ }
24
+ }"
25
+ class="container mx-auto px-4 py-8 max-w-4xl">
26
+
27
+ <!-- Header -->
28
+ <header class="text-center mb-12">
29
+ <h1 class="text-4xl font-bold text-gray-800 mb-4">ECG Scan Analyzer</h1>
30
+ <p class="text-gray-600 max-w-2xl mx-auto">Upload your ECG scan reports in PDF format and receive an instant professional analysis powered by advanced AI.</p>
31
+ </header>
32
+
33
+ {% if result %}
34
+ <!-- Results Section -->
35
+ <div class="bg-white rounded-lg shadow-lg p-6 mb-8">
36
+ <div class="flex items-center justify-between mb-4">
37
+ <h2 class="text-2xl font-semibold text-gray-800">Analysis Results</h2>
38
+ <span class="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
39
+ {{ result.scan_type }} Scan
40
+ </span>
41
+ </div>
42
+
43
+ <div class="bg-gray-50 rounded-lg p-6 mb-6">
44
+ <!-- Render the converted HTML analysis -->
45
+ <div class="text-gray-700 whitespace-pre-wrap">
46
+ {{ result.analysis_html | safe }}
47
+ </div>
48
+ </div>
49
+
50
+ {% if result.images %}
51
+ <div class="mt-6">
52
+ <h3 class="text-lg font-medium text-gray-800 mb-4">Scan Images</h3>
53
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
54
+ {% for image in result.images %}
55
+ <div class="border rounded-lg p-2">
56
+ <img src="data:image/png;base64,{{ image }}" alt="Scan image" class="w-full h-auto">
57
+ </div>
58
+ {% endfor %}
59
+ </div>
60
+ </div>
61
+ {% endif %}
62
+
63
+
64
+ <a href="{{ url_for('index') }}"
65
+ class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 mt-6">
66
+ Analyze Another Report
67
+ </a>
68
+ </div>
69
+ {% else %}
70
+ <!-- Upload Section -->
71
+ <div class="bg-white rounded-lg shadow-lg p-8"
72
+ x-on:dragover.prevent="isDragging = true"
73
+ x-on:dragleave.prevent="isDragging = false"
74
+ x-on:drop.prevent="isDragging = false; handleFile($event)">
75
+
76
+ <form action="{{ url_for('index') }}" method="POST" enctype="multipart/form-data"
77
+ class="relative">
78
+
79
+ <!-- Scan Type Selection -->
80
+ <div class="mb-6">
81
+ <label class="block text-sm font-medium text-gray-700 mb-2">Select Scan Type</label>
82
+ <div class="grid grid-cols-2 gap-4">
83
+ <label class="relative flex cursor-pointer">
84
+ <input type="radio" name="scan_type" value="ECG" class="sr-only" x-model="selectedScan">
85
+ <div class="flex items-center justify-center w-full p-4 border rounded-lg"
86
+ :class="{ 'border-blue-500 bg-blue-50': selectedScan === 'ECG', 'border-gray-200': selectedScan !== 'ECG' }">
87
+ <span class="text-sm font-medium" :class="{ 'text-blue-600': selectedScan === 'ECG', 'text-gray-900': selectedScan !== 'ECG' }">
88
+ ECG Scan
89
+ </span>
90
+ </div>
91
+ </label>
92
+ <!-- MRI option removed -->
93
+ </div>
94
+ </div>
95
+
96
+ <div class="flex flex-col items-center justify-center space-y-6"
97
+ :class="{ 'bg-blue-50 border-2 border-dashed border-blue-400': isDragging,
98
+ 'border-2 border-dashed border-gray-300': !isDragging }"
99
+ class="rounded-lg p-8 transition-all duration-200">
100
+
101
+ <div class="text-center" x-show="!isAnalyzing">
102
+ <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
103
+ <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4-4m4-4h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
104
+ </svg>
105
+ <div class="mt-4">
106
+ <label for="file" class="cursor-pointer">
107
+ <span class="mt-2 block text-sm font-medium text-gray-900">
108
+ Drop your PDF here, or
109
+ <span class="text-blue-600 hover:text-blue-500">browse</span>
110
+ </span>
111
+ </label>
112
+ <input type="file" id="file" name="file" accept=".pdf" required
113
+ class="hidden"
114
+ @change="handleFile($event)">
115
+ <p class="text-xs text-gray-500 mt-2">PDF files only, up to 20MB</p>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Loading State -->
120
+ <div x-show="isAnalyzing" class="text-center">
121
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
122
+ <p class="mt-4 text-sm text-gray-600">Analyzing your <span x-text="selectedScan"></span> report...</p>
123
+ </div>
124
+ </div>
125
+ </form>
126
+
127
+ <!-- Error Messages -->
128
+ <div class="mt-4">
129
+ {% with messages = get_flashed_messages(with_categories=true) %}
130
+ {% if messages %}
131
+ {% for category, message in messages %}
132
+ <div class="rounded-md p-4 {% if category == 'error' %}bg-red-50 text-red-700{% else %}bg-green-50 text-green-700{% endif %}">
133
+ {{ message }}
134
+ </div>
135
+ {% endfor %}
136
+ {% endif %}
137
+ {% endwith %}
138
+ </div>
139
+ </div>
140
+ {% endif %}
141
+
142
+ <!-- Features Section -->
143
+ <div class="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
144
+ <div class="text-center">
145
+ <div class="rounded-lg bg-blue-50 p-6">
146
+ <svg class="h-8 w-8 text-blue-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
148
+ </svg>
149
+ <h3 class="mt-4 text-lg font-medium text-gray-900">Instant Analysis</h3>
150
+ <p class="mt-2 text-sm text-gray-500">Get quick insights from your medical scans within seconds</p>
151
+ </div>
152
+ </div>
153
+ <div class="text-center">
154
+ <div class="rounded-lg bg-blue-50 p-6">
155
+ <svg class="h-8 w-8 text-blue-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
157
+ </svg>
158
+ <h3 class="mt-4 text-lg font-medium text-gray-900">Secure Processing</h3>
159
+ <p class="mt-2 text-sm text-gray-500">Your medical data is processed securely and never stored</p>
160
+ </div>
161
+ </div>
162
+ <div class="text-center">
163
+ <div class="rounded-lg bg-blue-50 p-6">
164
+ <svg class="h-8 w-8 text-blue-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
165
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
166
+ </svg>
167
+ <h3 class="mt-4 text-lg font-medium text-gray-900">AI-Powered</h3>
168
+ <p class="mt-2 text-sm text-gray-500">Advanced AI technology for accurate medical scan analysis</p>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </body>
174
+ </html>