ubden commited on
Commit
2eac65d
·
verified ·
1 Parent(s): 72455a1

e-mail entegration

Browse files
Dockerfile.email ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PULSE-7B Email ECG Processor Docker Container
2
+ # Ubden® Team - Email-based ECG Analysis Service
3
+
4
+ FROM python:3.9-slim
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Install system dependencies
10
+ RUN apt-get update && apt-get install -y \
11
+ gcc \
12
+ g++ \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy requirements
16
+ COPY requirements.txt .
17
+
18
+ # Install Python dependencies
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy application files
22
+ COPY email_ecg_processor.py .
23
+ COPY start_email_processor.py .
24
+ COPY utils.py .
25
+
26
+ # Create logs directory
27
+ RUN mkdir -p /app/logs
28
+
29
+ # Set environment variables
30
+ ENV PYTHONPATH=/app
31
+ ENV PYTHONUNBUFFERED=1
32
+
33
+ # Create non-root user
34
+ RUN useradd --create-home --shell /bin/bash app
35
+ RUN chown -R app:app /app
36
+ USER app
37
+
38
+ # Health check
39
+ HEALTHCHECK --interval=5m --timeout=30s --start-period=1m --retries=3 \
40
+ CMD python -c "import email_ecg_processor; print('Service healthy')" || exit 1
41
+
42
+ # Default command
43
+ CMD ["python", "start_email_processor.py", "300"]
README.md CHANGED
@@ -23,6 +23,8 @@ This repository provides a custom handler for deploying the **PULSE-7B** ECG ana
23
 
24
  **🚀 Enhanced with DeepSeek Integration**: This handler automatically translates PULSE-7B's English medical analysis into patient-friendly Turkish commentary using DeepSeek AI, providing bilingual ECG interpretation for Turkish healthcare professionals and patients.
25
 
 
 
26
  ## 🚀 Quick Start
27
 
28
  ### Prerequisites
@@ -34,8 +36,9 @@ This repository provides a custom handler for deploying the **PULSE-7B** ECG ana
34
 
35
  ```
36
  pulse-hf/
37
- ├── handler.py # Custom inference handler with DeepSeek integration
38
  ├── utils.py # Performance monitoring and DeepSeek client
 
39
  ├── requirements.txt # Python dependencies
40
  ├── generation_config.json # Model generation configuration
41
  ├── test_requests.json # Example request templates
@@ -68,17 +71,26 @@ pulse-hf/
68
  4. Click **"Create Endpoint"**
69
  5. Wait for the status to change from `Building` → `Initializing` → `Running`
70
 
71
- ### Step 3: Configure DeepSeek API Key (Optional)
72
-
73
- To enable Turkish commentary feature:
74
 
 
75
  1. Go to your endpoint's **"Environment"** tab
76
  2. In **"Secret Env"** section, add:
77
  - **Key**: `deep_key`
78
  - **Value**: Your DeepSeek API key
 
 
 
 
 
 
 
 
79
  3. Click **"Update Endpoint"**
80
 
81
- **Note**: Without this configuration, the endpoint will work but without Turkish commentary.
 
 
82
 
83
  ### Step 4: Get Your Endpoint URL
84
 
 
23
 
24
  **🚀 Enhanced with DeepSeek Integration**: This handler automatically translates PULSE-7B's English medical analysis into patient-friendly Turkish commentary using DeepSeek AI, providing bilingual ECG interpretation for Turkish healthcare professionals and patients.
25
 
26
+ **📧 Automatic Email Processing**: When email configuration is provided, the system automatically processes ECG images sent via email and responds with analysis results, making it a complete email-to-analysis solution.
27
+
28
  ## 🚀 Quick Start
29
 
30
  ### Prerequisites
 
36
 
37
  ```
38
  pulse-hf/
39
+ ├── handler.py # Custom inference handler with auto email processing
40
  ├── utils.py # Performance monitoring and DeepSeek client
41
+ ├── email_ecg_processor.py # Email processing module (auto-loaded by handler)
42
  ├── requirements.txt # Python dependencies
43
  ├── generation_config.json # Model generation configuration
44
  ├── test_requests.json # Example request templates
 
71
  4. Click **"Create Endpoint"**
72
  5. Wait for the status to change from `Building` → `Initializing` → `Running`
73
 
74
+ ### Step 3: Configure Environment Variables
 
 
75
 
76
+ #### 3.1 DeepSeek API Key (Optional - for Turkish commentary)
77
  1. Go to your endpoint's **"Environment"** tab
78
  2. In **"Secret Env"** section, add:
79
  - **Key**: `deep_key`
80
  - **Value**: Your DeepSeek API key
81
+
82
+ #### 3.2 Email Processing (Optional - for automatic email processing)
83
+ Add these additional environment variables:
84
+ - **Key**: `mail_host` | **Value**: `imap.gmail.com` (or your email provider)
85
+ - **Key**: `mail_username` | **Value**: Your email address
86
+ - **Key**: `mail_pw` | **Value**: Your email app password
87
+ - **Key**: `hf_key` | **Value**: Your HuggingFace token
88
+
89
  3. Click **"Update Endpoint"**
90
 
91
+ **Note**:
92
+ - Without DeepSeek key: Works but no Turkish commentary
93
+ - Without email config: Works but no automatic email processing
94
 
95
  ### Step 4: Get Your Endpoint URL
96
 
docker-compose.email.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ pulse-email-processor:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile.email
8
+ container_name: pulse-ecg-email-processor
9
+ restart: unless-stopped
10
+
11
+ environment:
12
+ # Email Configuration (Required)
13
+ - mail_host=${MAIL_HOST:-imap.gmail.com}
14
+ - mail_username=${MAIL_USERNAME}
15
+ - mail_pw=${MAIL_PASSWORD}
16
+
17
+ # HuggingFace Configuration (Required)
18
+ - hf_key=${HF_TOKEN}
19
+ - PULSE_ENDPOINT_URL=${PULSE_ENDPOINT_URL}
20
+
21
+ # DeepSeek Configuration (Optional)
22
+ - deep_key=${DEEPSEEK_API_KEY}
23
+
24
+ # Processing Configuration
25
+ - CHECK_INTERVAL=${CHECK_INTERVAL:-300}
26
+ - LOG_LEVEL=${LOG_LEVEL:-INFO}
27
+
28
+ volumes:
29
+ # Persist logs
30
+ - ./logs:/app/logs
31
+
32
+ # Optional: Mount config files
33
+ - ./config:/app/config:ro
34
+
35
+ # Resource limits
36
+ deploy:
37
+ resources:
38
+ limits:
39
+ cpus: '0.5'
40
+ memory: 512M
41
+ reservations:
42
+ cpus: '0.1'
43
+ memory: 128M
44
+
45
+ # Health check
46
+ healthcheck:
47
+ test: ["CMD", "python", "-c", "import email_ecg_processor; print('healthy')"]
48
+ interval: 5m
49
+ timeout: 30s
50
+ retries: 3
51
+ start_period: 1m
52
+
53
+ # Logging
54
+ logging:
55
+ driver: "json-file"
56
+ options:
57
+ max-size: "10m"
58
+ max-file: "3"
59
+
60
+ # Optional: Add monitoring service
61
+ pulse-monitor:
62
+ image: prom/prometheus:latest
63
+ container_name: pulse-ecg-monitor
64
+ restart: unless-stopped
65
+ ports:
66
+ - "9090:9090"
67
+ volumes:
68
+ - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
69
+ profiles:
70
+ - monitoring
71
+
72
+ volumes:
73
+ logs:
74
+ driver: local
75
+
76
+ networks:
77
+ default:
78
+ name: pulse-ecg-network
email_ecg_processor.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PULSE-7B Email ECG Processor
3
+ Ubden® Team - Email-based ECG Analysis System
4
+ Automatically processes ECG images from emails and sends Turkish commentary
5
+ """
6
+
7
+ import imaplib
8
+ import smtplib
9
+ import email
10
+ import base64
11
+ import os
12
+ import time
13
+ import re
14
+ import requests
15
+ from datetime import datetime
16
+ from email.mime.text import MIMEText
17
+ from email.mime.multipart import MIMEMultipart
18
+ from email.mime.image import MIMEImage
19
+ from email.header import decode_header
20
+ from typing import Dict, List, Any, Optional, Tuple
21
+ import logging
22
+
23
+ # Configure logging
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.FileHandler('ecg_email_processor.log'),
29
+ logging.StreamHandler()
30
+ ]
31
+ )
32
+ logger = logging.getLogger(__name__)
33
+
34
+ class EmailECGProcessor:
35
+ """Email-based ECG analysis processor with retry logic and error handling"""
36
+
37
+ def __init__(self):
38
+ """Initialize with environment variables"""
39
+ self.mail_host = os.getenv('mail_host', 'imap.gmail.com')
40
+ self.mail_username = os.getenv('mail_username')
41
+ self.mail_password = os.getenv('mail_pw')
42
+ self.hf_token = os.getenv('hf_key')
43
+ self.deepseek_key = os.getenv('deep_key')
44
+
45
+ # PULSE endpoint configuration
46
+ self.endpoint_url = self._get_endpoint_url()
47
+
48
+ # Email configuration
49
+ self.smtp_host = self.mail_host.replace('imap', 'smtp')
50
+ self.smtp_port = 587
51
+ self.imap_port = 993
52
+
53
+ # Processing configuration
54
+ self.max_retries = 3
55
+ self.retry_delay = 60 # 1 minute
56
+ self.max_wait_time = 300 # 5 minutes for cold start
57
+
58
+ # Supported image formats for ECG detection
59
+ self.supported_image_formats = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']
60
+
61
+ self._validate_configuration()
62
+ logger.info("📧 Email ECG Processor initialized successfully")
63
+
64
+ def _get_endpoint_url(self) -> str:
65
+ """Get endpoint URL from environment or construct from HF space"""
66
+ # Try direct endpoint URL first
67
+ endpoint_url = os.getenv('PULSE_ENDPOINT_URL')
68
+ if endpoint_url:
69
+ return endpoint_url
70
+
71
+ # Try to construct from HF space name
72
+ hf_space = os.getenv('HF_SPACE_NAME')
73
+ if hf_space:
74
+ return f"https://{hf_space}.hf.space"
75
+
76
+ # If running as part of HF Inference Endpoint, try to detect automatically
77
+ hf_endpoint_name = os.getenv('ENDPOINT_NAME')
78
+ hf_region = os.getenv('AWS_DEFAULT_REGION', 'us-east-1')
79
+ if hf_endpoint_name:
80
+ return f"https://{hf_endpoint_name}.{hf_region}.aws.endpoints.huggingface.cloud"
81
+
82
+ # Last resort - try localhost (for development)
83
+ if os.getenv('DEVELOPMENT_MODE'):
84
+ return "http://localhost:8000"
85
+
86
+ raise ValueError("PULSE_ENDPOINT_URL, HF_SPACE_NAME, or ENDPOINT_NAME must be set in environment variables")
87
+
88
+ def _validate_configuration(self):
89
+ """Validate required environment variables"""
90
+ required_vars = {
91
+ 'mail_username': self.mail_username,
92
+ 'mail_pw': self.mail_password,
93
+ 'hf_key': self.hf_token
94
+ }
95
+
96
+ missing_vars = [var for var, value in required_vars.items() if not value]
97
+ if missing_vars:
98
+ raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
99
+
100
+ logger.info("✅ Configuration validation passed")
101
+
102
+ def connect_to_email(self) -> imaplib.IMAP4_SSL:
103
+ """Connect to email server"""
104
+ try:
105
+ mail = imaplib.IMAP4_SSL(self.mail_host, self.imap_port)
106
+ mail.login(self.mail_username, self.mail_password)
107
+ mail.select('INBOX')
108
+ logger.info(f"📧 Connected to email server: {self.mail_host}")
109
+ return mail
110
+ except Exception as e:
111
+ logger.error(f"❌ Failed to connect to email server: {e}")
112
+ raise
113
+
114
+ def get_unread_emails(self, mail: imaplib.IMAP4_SSL) -> List[str]:
115
+ """Get list of unread email IDs"""
116
+ try:
117
+ # Search for unread emails
118
+ _, messages = mail.search(None, 'UNSEEN')
119
+ message_ids = messages[0].split() if messages[0] else []
120
+ logger.info(f"📬 Found {len(message_ids)} unread emails")
121
+ return message_ids
122
+ except Exception as e:
123
+ logger.error(f"❌ Failed to get unread emails: {e}")
124
+ return []
125
+
126
+ def parse_email(self, mail: imaplib.IMAP4_SSL, message_id: str) -> Optional[Dict[str, Any]]:
127
+ """Parse email and extract relevant information"""
128
+ try:
129
+ _, msg_data = mail.fetch(message_id, '(RFC822)')
130
+ email_body = msg_data[0][1]
131
+ email_message = email.message_from_bytes(email_body)
132
+
133
+ # Extract email headers
134
+ subject = decode_header(email_message['subject'])[0][0]
135
+ if isinstance(subject, bytes):
136
+ subject = subject.decode()
137
+
138
+ sender = email_message['from']
139
+ date = email_message['date']
140
+
141
+ # Extract body and attachments
142
+ body_text = ""
143
+ attachments = []
144
+
145
+ if email_message.is_multipart():
146
+ for part in email_message.walk():
147
+ if part.get_content_maintype() == 'multipart':
148
+ continue
149
+ elif part.get_content_maintype() == 'text':
150
+ charset = part.get_content_charset() or 'utf-8'
151
+ try:
152
+ body_text += part.get_payload(decode=True).decode(charset)
153
+ except:
154
+ body_text += part.get_payload(decode=True).decode('utf-8', errors='ignore')
155
+ elif part.get_content_maintype() == 'image':
156
+ filename = part.get_filename()
157
+ if filename and self._is_supported_image(filename):
158
+ image_data = part.get_payload(decode=True)
159
+ attachments.append({
160
+ 'filename': filename,
161
+ 'data': image_data,
162
+ 'content_type': part.get_content_type()
163
+ })
164
+ else:
165
+ charset = email_message.get_content_charset() or 'utf-8'
166
+ try:
167
+ body_text = email_message.get_payload(decode=True).decode(charset)
168
+ except:
169
+ body_text = email_message.get_payload(decode=True).decode('utf-8', errors='ignore')
170
+
171
+ return {
172
+ 'message_id': message_id,
173
+ 'subject': subject,
174
+ 'sender': sender,
175
+ 'date': date,
176
+ 'body': body_text,
177
+ 'attachments': attachments
178
+ }
179
+
180
+ except Exception as e:
181
+ logger.error(f"❌ Failed to parse email {message_id}: {e}")
182
+ return None
183
+
184
+ def _is_supported_image(self, filename: str) -> bool:
185
+ """Check if the image has a supported format"""
186
+ filename_lower = filename.lower()
187
+ is_supported = any(filename_lower.endswith(ext) for ext in self.supported_image_formats)
188
+
189
+ logger.info(f"🔍 Image format check for '{filename}': {is_supported}")
190
+ return is_supported
191
+
192
+ def analyze_ecg_with_retry(self, image_data: bytes, query: str, content_type: str) -> Dict[str, Any]:
193
+ """Analyze ECG image with retry logic for cold start handling"""
194
+ start_time = time.time()
195
+
196
+ for attempt in range(self.max_retries):
197
+ try:
198
+ logger.info(f"🔄 Analysis attempt {attempt + 1}/{self.max_retries}")
199
+
200
+ # Convert image to base64
201
+ image_base64 = base64.b64encode(image_data).decode('utf-8')
202
+ mime_type = content_type or 'image/jpeg'
203
+ base64_string = f"data:{mime_type};base64,{image_base64}"
204
+
205
+ # Prepare payload
206
+ payload = {
207
+ "inputs": {
208
+ "query": query,
209
+ "image": base64_string
210
+ },
211
+ "parameters": {
212
+ "max_new_tokens": 512,
213
+ "temperature": 0.2,
214
+ "top_p": 0.9,
215
+ "repetition_penalty": 1.05,
216
+ "enable_turkish_commentary": True,
217
+ "deepseek_timeout": 30
218
+ }
219
+ }
220
+
221
+ headers = {
222
+ "Authorization": f"Bearer {self.hf_token}",
223
+ "Content-Type": "application/json"
224
+ }
225
+
226
+ # Make request
227
+ response = requests.post(
228
+ self.endpoint_url,
229
+ headers=headers,
230
+ json=payload,
231
+ timeout=120
232
+ )
233
+
234
+ if response.status_code == 200:
235
+ result = response.json()[0]
236
+ elapsed_time = time.time() - start_time
237
+ logger.info(f"✅ ECG analysis completed in {elapsed_time:.1f} seconds")
238
+ return result
239
+
240
+ elif response.status_code == 503:
241
+ # Service Unavailable - Cold start
242
+ elapsed_time = time.time() - start_time
243
+ if elapsed_time < self.max_wait_time and attempt < self.max_retries - 1:
244
+ wait_time = min(self.retry_delay, self.max_wait_time - elapsed_time)
245
+ logger.info(f"⏳ Endpoint cold start detected. Waiting {wait_time} seconds...")
246
+ time.sleep(wait_time)
247
+ continue
248
+ else:
249
+ raise Exception("Endpoint startup timeout")
250
+
251
+ else:
252
+ raise Exception(f"HTTP {response.status_code}: {response.text}")
253
+
254
+ except requests.exceptions.Timeout:
255
+ logger.warning(f"⏰ Request timeout on attempt {attempt + 1}")
256
+ if attempt < self.max_retries - 1:
257
+ time.sleep(self.retry_delay)
258
+ continue
259
+ else:
260
+ raise Exception("Analysis timeout after all retries")
261
+
262
+ except Exception as e:
263
+ logger.error(f"❌ Analysis error on attempt {attempt + 1}: {e}")
264
+ if attempt < self.max_retries - 1:
265
+ time.sleep(self.retry_delay)
266
+ continue
267
+ else:
268
+ raise
269
+
270
+ raise Exception("Analysis failed after all retry attempts")
271
+
272
+ def send_analysis_result(self, recipient: str, original_subject: str,
273
+ filename: str, analysis_result: Dict[str, Any]) -> bool:
274
+ """Send analysis result via email"""
275
+ try:
276
+ msg = MIMEMultipart()
277
+ msg['From'] = self.mail_username
278
+ msg['To'] = recipient
279
+ msg['Subject'] = f"🏥 EKG Sonucunuz: {filename}"
280
+
281
+ # Extract sender name for personalization
282
+ sender_name = recipient.split('@')[0].title()
283
+
284
+ # Prepare email body
285
+ english_analysis = analysis_result.get('generated_text', 'Analysis not available')
286
+ turkish_commentary = analysis_result.get('comment_text', 'Türkçe yorum mevcut değil')
287
+
288
+ # Format the email body
289
+ body = f"""
290
+ Sayın {sender_name},
291
+
292
+ EKG görüntünüzün analizi tamamlanmıştır. Aşağıda detaylı sonuçları bulabilirsiniz:
293
+
294
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
295
+
296
+ 🔬 **İNGİLİZCE TEKNİK ANALİZ:**
297
+ {english_analysis}
298
+
299
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
300
+
301
+ {turkish_commentary}
302
+
303
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
304
+
305
+ ⚠️ **ÖNEMLİ UYARI:**
306
+ Bu analiz sadece bilgilendirme amaçlıdır ve kesinlikle tıbbi tanı yerine geçmez.
307
+ Kesin tanı ve tedavi için mutlaka bir kardiyoloji uzmanına başvurunuz.
308
+
309
+ 🏥 **ACİL DURUMLARDA:**
310
+ Göğüs ağrısı, nefes darlığı, çarpıntı gibi şikayetleriniz varsa
311
+ derhal en yakın acil servise başvurunuz veya 112'yi arayınız.
312
+
313
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
314
+
315
+ 📊 **Analiz Detayları:**
316
+ • Dosya: {filename}
317
+ • Tarih: {datetime.now().strftime('%d.%m.%Y %H:%M')}
318
+ • Model: PULSE-7B + DeepSeek AI
319
+ • İşlem Durumu: {analysis_result.get('commentary_status', 'Başarılı')}
320
+
321
+ Geçmiş olsun! 🙏
322
+
323
+ ---
324
+ 🤖 Bu e-posta PULSE-7B EKG Analiz Sistemi tarafından otomatik olarak gönderilmiştir.
325
+ 💡 Ubden® Team - AI-Powered Healthcare Solutions
326
+ """
327
+
328
+ msg.attach(MIMEText(body, 'plain', 'utf-8'))
329
+
330
+ # Send email
331
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
332
+ server.starttls()
333
+ server.login(self.mail_username, self.mail_password)
334
+ server.send_message(msg)
335
+ server.quit()
336
+
337
+ logger.info(f"✅ Analysis result sent to {recipient}")
338
+ return True
339
+
340
+ except Exception as e:
341
+ logger.error(f"❌ Failed to send analysis result to {recipient}: {e}")
342
+ return False
343
+
344
+ def send_error_notification(self, recipient: str, original_subject: str,
345
+ filename: str, error_message: str) -> bool:
346
+ """Send error notification via email"""
347
+ try:
348
+ msg = MIMEMultipart()
349
+ msg['From'] = self.mail_username
350
+ msg['To'] = recipient
351
+ msg['Subject'] = f"⚠️ EKG Analizi Hatası: {filename}"
352
+
353
+ sender_name = recipient.split('@')[0].title()
354
+
355
+ body = f"""
356
+ Sayın {sender_name},
357
+
358
+ Maalesef EKG görüntünüzün analizi sırasında bir hata oluştu.
359
+
360
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
361
+
362
+ 📋 **Hata Detayları:**
363
+ • Dosya: {filename}
364
+ • Hata: {error_message}
365
+ • Tarih: {datetime.now().strftime('%d.%m.%Y %H:%M')}
366
+
367
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
368
+
369
+ 🔧 **Olası Çözümler:**
370
+ 1. EKG görüntüsünün net ve okunaklı olduğundan emin olun
371
+ 2. Desteklenen dosya formatlarını kullanın (JPG, PNG, GIF, BMP)
372
+ 3. Dosya boyutunun 10MB'dan küçük olduğundan emin olun
373
+ 4. Birkaç dakika sonra tekrar deneyin (sistem başlatılıyor olabilir)
374
+
375
+ 🔄 **Tekrar Deneme:**
376
+ EKG görüntünüzü yeniden gönderebilirsiniz. Sistem otomatik olarak işleme alacaktır.
377
+
378
+ 📞 **Destek:**
379
+ Problem devam ederse lütfen sistem yöneticisi ile iletişime geçin.
380
+
381
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
382
+
383
+ Özür dileriz! 🙏
384
+
385
+ ---
386
+ 🤖 Bu e-posta PULSE-7B EKG Analiz Sistemi tarafından otomatik olarak gönderilmiştir.
387
+ 💡 Ubden® Team - AI-Powered Healthcare Solutions
388
+ """
389
+
390
+ msg.attach(MIMEText(body, 'plain', 'utf-8'))
391
+
392
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
393
+ server.starttls()
394
+ server.login(self.mail_username, self.mail_password)
395
+ server.send_message(msg)
396
+ server.quit()
397
+
398
+ logger.info(f"📧 Error notification sent to {recipient}")
399
+ return True
400
+
401
+ except Exception as e:
402
+ logger.error(f"❌ Failed to send error notification to {recipient}: {e}")
403
+ return False
404
+
405
+ def mark_as_read(self, mail: imaplib.IMAP4_SSL, message_id: str) -> bool:
406
+ """Mark email as read"""
407
+ try:
408
+ mail.store(message_id, '+FLAGS', '\\Seen')
409
+ logger.info(f"📖 Email {message_id} marked as read")
410
+ return True
411
+ except Exception as e:
412
+ logger.error(f"❌ Failed to mark email {message_id} as read: {e}")
413
+ return False
414
+
415
+ def process_single_email(self, mail: imaplib.IMAP4_SSL, message_id: str) -> bool:
416
+ """Process a single email"""
417
+ logger.info(f"📧 Processing email {message_id}")
418
+
419
+ try:
420
+ # Parse email
421
+ email_data = self.parse_email(mail, message_id)
422
+ if not email_data:
423
+ logger.warning(f"⚠️ Failed to parse email {message_id}")
424
+ return False
425
+
426
+ logger.info(f"📬 Email from: {email_data['sender']}, Subject: {email_data['subject']}")
427
+
428
+ # Process image attachments (assuming they are ECG images)
429
+ processed_any = False
430
+ for attachment in email_data['attachments']:
431
+ logger.info(f"🖼️ Processing image attachment: {attachment['filename']}")
432
+
433
+ try:
434
+ # Analyze image as ECG
435
+ query = f"Analyze this ECG image from email. Subject: {email_data['subject']}. Context: {email_data['body'][:200]}..."
436
+
437
+ analysis_result = self.analyze_ecg_with_retry(
438
+ attachment['data'],
439
+ query,
440
+ attachment['content_type']
441
+ )
442
+
443
+ # Send result
444
+ success = self.send_analysis_result(
445
+ email_data['sender'],
446
+ email_data['subject'],
447
+ attachment['filename'],
448
+ analysis_result
449
+ )
450
+
451
+ if success:
452
+ processed_any = True
453
+ logger.info(f"✅ Successfully processed {attachment['filename']}")
454
+
455
+ except Exception as e:
456
+ logger.error(f"❌ Failed to analyze {attachment['filename']}: {e}")
457
+
458
+ # Send error notification
459
+ self.send_error_notification(
460
+ email_data['sender'],
461
+ email_data['subject'],
462
+ attachment['filename'],
463
+ str(e)
464
+ )
465
+
466
+ # Mark as read if we processed any attachments
467
+ if processed_any:
468
+ self.mark_as_read(mail, message_id)
469
+ return True
470
+ elif not email_data['attachments']:
471
+ # No image attachments found, mark as read to avoid reprocessing
472
+ logger.info(f"📭 No image attachments found in email {message_id}")
473
+ self.mark_as_read(mail, message_id)
474
+ return True
475
+ else:
476
+ logger.warning(f"⚠️ Failed to process any attachments in email {message_id}")
477
+ return False
478
+
479
+ except Exception as e:
480
+ logger.error(f"❌ Error processing email {message_id}: {e}")
481
+ return False
482
+
483
+ def run_email_processor(self, check_interval: int = 300) -> None:
484
+ """Main email processing loop"""
485
+ logger.info(f"🚀 Starting Email ECG Processor (check interval: {check_interval}s)")
486
+
487
+ while True:
488
+ try:
489
+ logger.info("🔄 Checking for new emails...")
490
+
491
+ # Connect to email
492
+ mail = self.connect_to_email()
493
+
494
+ # Get unread emails
495
+ unread_emails = self.get_unread_emails(mail)
496
+
497
+ if unread_emails:
498
+ logger.info(f"📬 Processing {len(unread_emails)} unread emails")
499
+
500
+ for message_id in unread_emails:
501
+ self.process_single_email(mail, message_id)
502
+ time.sleep(2) # Brief pause between emails
503
+ else:
504
+ logger.info("📭 No unread emails found")
505
+
506
+ # Close connection
507
+ mail.close()
508
+ mail.logout()
509
+
510
+ # Wait for next check
511
+ logger.info(f"⏰ Waiting {check_interval} seconds until next check...")
512
+ time.sleep(check_interval)
513
+
514
+ except KeyboardInterrupt:
515
+ logger.info("🛑 Email processor stopped by user")
516
+ break
517
+ except Exception as e:
518
+ logger.error(f"❌ Error in main loop: {e}")
519
+ logger.info(f"⏰ Waiting {check_interval} seconds before retry...")
520
+ time.sleep(check_interval)
521
+
522
+ def main():
523
+ """Main function to run the email processor"""
524
+ try:
525
+ processor = EmailECGProcessor()
526
+ processor.run_email_processor(check_interval=300) # Check every 5 minutes
527
+ except Exception as e:
528
+ logger.error(f"❌ Failed to start email processor: {e}")
529
+ raise
530
+
531
+ if __name__ == "__main__":
532
+ main()
handler.py CHANGED
@@ -11,6 +11,8 @@ from io import BytesIO
11
  from PIL import Image
12
  import requests
13
  import time
 
 
14
 
15
  # Import utilities if available
16
  try:
@@ -28,6 +30,16 @@ except ImportError:
28
  deepseek_client = None
29
  print("⚠️ Utils module not found - performance monitoring and DeepSeek integration disabled")
30
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  class EndpointHandler:
33
  def __init__(self, path=""):
@@ -45,6 +57,11 @@ class EndpointHandler:
45
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
46
  print(f"🖥️ Running on: {self.device}")
47
 
 
 
 
 
 
48
  try:
49
  # First attempt - using pipeline (easiest and most stable way)
50
  from transformers import pipeline
@@ -107,6 +124,47 @@ class EndpointHandler:
107
  else:
108
  self.use_pipeline = True
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  def process_image_input(self, image_input):
111
  """
112
  Handle both URL and base64 image inputs like a champ!
@@ -229,14 +287,36 @@ class EndpointHandler:
229
  def health_check(self) -> Dict[str, Any]:
230
  """Health check endpoint"""
231
  if UTILS_AVAILABLE:
232
- return create_health_check()
233
  else:
234
- return {
235
  'status': 'healthy',
236
  'model': 'PULSE-7B',
237
  'timestamp': time.time(),
238
  'handler_version': '2.0.0'
239
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  def __call__(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
242
  """
 
11
  from PIL import Image
12
  import requests
13
  import time
14
+ import threading
15
+ import os
16
 
17
  # Import utilities if available
18
  try:
 
30
  deepseek_client = None
31
  print("⚠️ Utils module not found - performance monitoring and DeepSeek integration disabled")
32
 
33
+ # Import email processor if available
34
+ try:
35
+ from email_ecg_processor import EmailECGProcessor
36
+ EMAIL_PROCESSOR_AVAILABLE = True
37
+ print("📧 Email ECG Processor module found - email processing will be enabled")
38
+ except ImportError:
39
+ EMAIL_PROCESSOR_AVAILABLE = False
40
+ EmailECGProcessor = None
41
+ print("📭 Email ECG Processor module not found - email processing disabled")
42
+
43
 
44
  class EndpointHandler:
45
  def __init__(self, path=""):
 
57
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
58
  print(f"🖥️ Running on: {self.device}")
59
 
60
+ # Initialize email processor
61
+ self.email_processor = None
62
+ self.email_thread = None
63
+ self._init_email_processor()
64
+
65
  try:
66
  # First attempt - using pipeline (easiest and most stable way)
67
  from transformers import pipeline
 
124
  else:
125
  self.use_pipeline = True
126
 
127
+ def _init_email_processor(self):
128
+ """Initialize email processor in background thread"""
129
+ if not EMAIL_PROCESSOR_AVAILABLE:
130
+ print("📭 Email processor not available - skipping email initialization")
131
+ return
132
+
133
+ # Check if email configuration is available
134
+ required_email_vars = ['mail_username', 'mail_pw', 'hf_key']
135
+ missing_vars = [var for var in required_email_vars if not os.getenv(var)]
136
+
137
+ if missing_vars:
138
+ print(f"📭 Email processor disabled - missing environment variables: {', '.join(missing_vars)}")
139
+ return
140
+
141
+ try:
142
+ print("📧 Initializing email processor...")
143
+ self.email_processor = EmailECGProcessor()
144
+
145
+ # Start email processor in background thread
146
+ self.email_thread = threading.Thread(
147
+ target=self._run_email_processor,
148
+ daemon=True,
149
+ name="EmailProcessor"
150
+ )
151
+ self.email_thread.start()
152
+ print("✅ Email processor started successfully in background thread")
153
+
154
+ except Exception as e:
155
+ print(f"❌ Failed to initialize email processor: {e}")
156
+ self.email_processor = None
157
+ self.email_thread = None
158
+
159
+ def _run_email_processor(self):
160
+ """Run email processor in background thread"""
161
+ try:
162
+ if self.email_processor:
163
+ print("📧 Email processor thread started - checking emails every 5 minutes")
164
+ self.email_processor.run_email_processor(check_interval=300)
165
+ except Exception as e:
166
+ print(f"❌ Email processor thread error: {e}")
167
+
168
  def process_image_input(self, image_input):
169
  """
170
  Handle both URL and base64 image inputs like a champ!
 
287
  def health_check(self) -> Dict[str, Any]:
288
  """Health check endpoint"""
289
  if UTILS_AVAILABLE:
290
+ health = create_health_check()
291
  else:
292
+ health = {
293
  'status': 'healthy',
294
  'model': 'PULSE-7B',
295
  'timestamp': time.time(),
296
  'handler_version': '2.0.0'
297
  }
298
+
299
+ # Add email processor status
300
+ if EMAIL_PROCESSOR_AVAILABLE and self.email_processor:
301
+ health['email_processor'] = {
302
+ 'status': 'running',
303
+ 'thread_alive': self.email_thread.is_alive() if self.email_thread else False,
304
+ 'configuration': 'configured'
305
+ }
306
+ elif EMAIL_PROCESSOR_AVAILABLE:
307
+ health['email_processor'] = {
308
+ 'status': 'available_but_not_configured',
309
+ 'thread_alive': False,
310
+ 'configuration': 'missing_environment_variables'
311
+ }
312
+ else:
313
+ health['email_processor'] = {
314
+ 'status': 'not_available',
315
+ 'thread_alive': False,
316
+ 'configuration': 'module_not_found'
317
+ }
318
+
319
+ return health
320
 
321
  def __call__(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
322
  """
requirements.txt CHANGED
@@ -19,4 +19,8 @@ typing-extensions>=4.0.0
19
  psutil>=5.8.0
20
 
21
  # HuggingFace Inference specific
22
- huggingface-hub>=0.16.0
 
 
 
 
 
19
  psutil>=5.8.0
20
 
21
  # HuggingFace Inference specific
22
+ huggingface-hub>=0.16.0
23
+
24
+ # Email processing dependencies
25
+ email-validator>=2.0.0
26
+ chardet>=5.0.0
start_email_processor.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PULSE-7B Email ECG Processor Starter
4
+ Ubden® Team - Email processor başlatıcı script
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import signal
10
+ import logging
11
+ from email_ecg_processor import EmailECGProcessor
12
+
13
+ # Configure logging
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format='%(asctime)s - %(levelname)s - %(message)s'
17
+ )
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class EmailProcessorService:
21
+ """Email processor service wrapper"""
22
+
23
+ def __init__(self):
24
+ self.processor = None
25
+ self.running = False
26
+
27
+ # Register signal handlers
28
+ signal.signal(signal.SIGINT, self._signal_handler)
29
+ signal.signal(signal.SIGTERM, self._signal_handler)
30
+
31
+ def _signal_handler(self, signum, frame):
32
+ """Handle shutdown signals"""
33
+ logger.info(f"📡 Received signal {signum}, shutting down gracefully...")
34
+ self.running = False
35
+ sys.exit(0)
36
+
37
+ def check_environment(self):
38
+ """Check required environment variables"""
39
+ required_vars = [
40
+ 'mail_host',
41
+ 'mail_username',
42
+ 'mail_pw',
43
+ 'hf_key'
44
+ ]
45
+
46
+ optional_vars = [
47
+ 'deep_key',
48
+ 'PULSE_ENDPOINT_URL',
49
+ 'HF_SPACE_NAME'
50
+ ]
51
+
52
+ logger.info("🔍 Checking environment configuration...")
53
+
54
+ # Check required variables
55
+ missing_required = []
56
+ for var in required_vars:
57
+ if not os.getenv(var):
58
+ missing_required.append(var)
59
+ else:
60
+ logger.info(f"✅ {var}: {'*' * len(os.getenv(var))}")
61
+
62
+ if missing_required:
63
+ logger.error(f"❌ Missing required environment variables: {', '.join(missing_required)}")
64
+ logger.info("\n📋 Required Environment Variables:")
65
+ logger.info("• mail_host: Email server (e.g., imap.gmail.com)")
66
+ logger.info("• mail_username: Email username")
67
+ logger.info("• mail_pw: Email password (App Password for Gmail)")
68
+ logger.info("• hf_key: HuggingFace API token")
69
+ logger.info("\n📋 Optional Environment Variables:")
70
+ logger.info("• deep_key: DeepSeek API key (for Turkish commentary)")
71
+ logger.info("• PULSE_ENDPOINT_URL: Direct endpoint URL")
72
+ logger.info("• HF_SPACE_NAME: HuggingFace space name")
73
+ return False
74
+
75
+ # Check optional variables
76
+ for var in optional_vars:
77
+ value = os.getenv(var)
78
+ if value:
79
+ logger.info(f"✅ {var}: {'*' * min(len(value), 10)}")
80
+ else:
81
+ logger.warning(f"⚠️ {var}: Not set (optional)")
82
+
83
+ return True
84
+
85
+ def start(self, check_interval: int = 300):
86
+ """Start the email processor service"""
87
+ logger.info("🚀 Starting PULSE-7B Email ECG Processor Service")
88
+ logger.info("━" * 60)
89
+
90
+ # Check environment
91
+ if not self.check_environment():
92
+ logger.error("❌ Environment check failed")
93
+ return False
94
+
95
+ logger.info("━" * 60)
96
+
97
+ try:
98
+ # Initialize processor
99
+ self.processor = EmailECGProcessor()
100
+ self.running = True
101
+
102
+ logger.info(f"📧 Email processor initialized successfully")
103
+ logger.info(f"⏰ Check interval: {check_interval} seconds")
104
+ logger.info("🔄 Starting email monitoring...")
105
+ logger.info("━" * 60)
106
+
107
+ # Start processing
108
+ self.processor.run_email_processor(check_interval)
109
+
110
+ except KeyboardInterrupt:
111
+ logger.info("🛑 Service stopped by user")
112
+ except Exception as e:
113
+ logger.error(f"❌ Service error: {e}")
114
+ return False
115
+ finally:
116
+ logger.info("🔚 Email processor service stopped")
117
+
118
+ return True
119
+
120
+ def print_usage():
121
+ """Print usage information"""
122
+ print("""
123
+ 🏥 PULSE-7B Email ECG Processor Service
124
+
125
+ Usage:
126
+ python start_email_processor.py [check_interval]
127
+
128
+ Arguments:
129
+ check_interval Check interval in seconds (default: 300 = 5 minutes)
130
+
131
+ Examples:
132
+ python start_email_processor.py # Check every 5 minutes
133
+ python start_email_processor.py 60 # Check every 1 minute
134
+ python start_email_processor.py 900 # Check every 15 minutes
135
+
136
+ Environment Variables Required:
137
+ mail_host Email IMAP server (e.g., imap.gmail.com)
138
+ mail_username Email username
139
+ mail_pw Email password (App Password for Gmail)
140
+ hf_key HuggingFace API token
141
+
142
+ Environment Variables Optional:
143
+ deep_key DeepSeek API key (for Turkish commentary)
144
+ PULSE_ENDPOINT_URL Direct endpoint URL
145
+ HF_SPACE_NAME HuggingFace space name
146
+
147
+ Gmail Setup:
148
+ 1. Enable 2-Factor Authentication
149
+ 2. Generate App Password: Google Account → Security → App passwords
150
+ 3. Use App Password as mail_pw (not your regular password)
151
+
152
+ HuggingFace Setup:
153
+ 1. Create API token: HuggingFace → Settings → Access Tokens
154
+ 2. Deploy PULSE-7B endpoint with this handler
155
+ 3. Add DeepSeek API key to endpoint environment (deep_key)
156
+
157
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
158
+
159
+ 💡 Ubden® Team - AI-Powered Healthcare Solutions
160
+ """)
161
+
162
+ def main():
163
+ """Main function"""
164
+ # Parse command line arguments
165
+ check_interval = 300 # Default 5 minutes
166
+
167
+ if len(sys.argv) > 1:
168
+ if sys.argv[1] in ['-h', '--help', 'help']:
169
+ print_usage()
170
+ return
171
+
172
+ try:
173
+ check_interval = int(sys.argv[1])
174
+ if check_interval < 30:
175
+ logger.warning("⚠️ Check interval too low, setting to minimum 30 seconds")
176
+ check_interval = 30
177
+ elif check_interval > 3600:
178
+ logger.warning("⚠️ Check interval too high, setting to maximum 1 hour")
179
+ check_interval = 3600
180
+ except ValueError:
181
+ logger.error("❌ Invalid check interval. Must be a number in seconds.")
182
+ print_usage()
183
+ return
184
+
185
+ # Start service
186
+ service = EmailProcessorService()
187
+ service.start(check_interval)
188
+
189
+ if __name__ == "__main__":
190
+ main()