GOWREESH M G commited on
Commit
3a772d8
·
verified ·
1 Parent(s): 26bf0d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -225
app.py CHANGED
@@ -3,69 +3,39 @@ import os
3
  import time
4
  import numpy as np
5
  import cv2
6
- import glob
7
-
8
- # Deep Learning Libraries
9
- import tensorflow as tf
10
- from tensorflow.keras.models import load_model, Model
11
- from tensorflow.keras.preprocessing.image import load_img, img_to_array, ImageDataGenerator
12
- from tensorflow.keras.applications import EfficientNetB0
13
- from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout, Input
14
- from tensorflow.keras.optimizers import Adam
15
- from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
16
- from sklearn.utils import class_weight
17
 
18
  app = Flask(__name__)
19
 
20
  # -------------------- CONFIG --------------------
21
  UPLOAD_FOLDER = "uploads"
22
- DATASET_PATH = os.path.join("static", "dataset", "colored_images")
23
 
24
- # 5 CLASSES (Correct Order)
25
- CLASSES = ["No_DR", "Mild", "Moderate", "Severe", "Proliferate_DR"]
 
26
 
27
- MODEL_FILE = "retina_efficientnet_v2.h5"
28
- IMG_SIZE = (224, 224)
29
- BATCH_SIZE = 16
30
 
31
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
32
- MODEL = None
33
- LOAD_ERROR = None
34
-
35
- # -------------------- COMPATIBILITY FIX --------------------
36
- def fix_layer_config(cls):
37
- class FixedLayer(cls):
38
- def __init__(self, *args, **kwargs):
39
- kwargs.pop('quantization_config', None)
40
- kwargs.pop('glitch_filter', None)
41
- super().__init__(*args, **kwargs)
42
- return FixedLayer
43
-
44
- from tensorflow.keras.layers import (
45
- Conv2D, BatchNormalization, Activation, DepthwiseConv2D,
46
- Rescaling, ZeroPadding2D, Add, Multiply, InputLayer
47
- )
48
-
49
- CUSTOM_OBJECTS = {
50
- 'Dense': fix_layer_config(Dense),
51
- 'Dropout': fix_layer_config(Dropout),
52
- 'GlobalAveragePooling2D': fix_layer_config(GlobalAveragePooling2D),
53
- 'Conv2D': fix_layer_config(Conv2D),
54
- 'BatchNormalization': fix_layer_config(BatchNormalization),
55
- 'Activation': fix_layer_config(Activation),
56
- 'DepthwiseConv2D': fix_layer_config(DepthwiseConv2D),
57
- 'Rescaling': fix_layer_config(Rescaling),
58
- 'ZeroPadding2D': fix_layer_config(ZeroPadding2D),
59
- 'Add': fix_layer_config(Add),
60
- 'Multiply': fix_layer_config(Multiply),
61
- 'InputLayer': fix_layer_config(InputLayer)
62
- }
63
-
64
- # -------------------- ADVANCED PREPROCESSING --------------------
65
  def enhance_medical_image(img):
 
66
  try:
67
- if np.max(img) <= 1.0: img = (img * 255).astype(np.uint8)
68
- else: img = img.astype(np.uint8)
 
 
 
69
 
70
  lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)
71
  l, a, b = cv2.split(lab)
@@ -74,12 +44,13 @@ def enhance_medical_image(img):
74
  limg = cv2.merge((cl, a, b))
75
  final = cv2.cvtColor(limg, cv2.COLOR_LAB2RGB)
76
 
77
- return final.astype(np.float32) / 255.0
 
78
  except Exception:
79
- return img.astype(np.float32) / 255.0
80
 
81
- # -------------------- EYE VALIDATION --------------------
82
  def is_valid_eye_image(img_path):
 
83
  try:
84
  img = cv2.imread(img_path)
85
  if img is None: return False
@@ -98,159 +69,111 @@ def is_valid_eye_image(img_path):
98
  if mean_brightness < 5 or mean_brightness > 240: return False
99
 
100
  return True
101
- except Exception: return True
102
-
103
- # -------------------- TRAINING PIPELINE --------------------
104
- def build_model(trainable=False):
105
- base_model = EfficientNetB0(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
106
- base_model.trainable = trainable
107
-
108
- x = base_model.output
109
- x = GlobalAveragePooling2D()(x)
110
- x = Dense(512, activation='relu')(x)
111
- x = Dropout(0.5)(x)
112
- predictions = Dense(len(CLASSES), activation='softmax')(x)
113
-
114
- model = Model(inputs=base_model.input, outputs=predictions)
115
- return model
116
-
117
- def train_on_dataset():
118
- print(f"\n[TRAINING] 🚀 Starting 5-Class Training Pipeline (Fixed 35 Epochs)...")
119
-
120
- if not os.path.exists(DATASET_PATH):
121
- print("[ERROR] Dataset not found.")
122
- return None
123
-
124
- train_datagen = ImageDataGenerator(
125
- preprocessing_function=enhance_medical_image,
126
- rotation_range=20,
127
- width_shift_range=0.1,
128
- height_shift_range=0.1,
129
- zoom_range=0.1,
130
- horizontal_flip=True,
131
- vertical_flip=True,
132
- fill_mode='nearest',
133
- validation_split=0.2
134
- )
135
-
136
- train_generator = train_datagen.flow_from_directory(
137
- DATASET_PATH, target_size=IMG_SIZE, batch_size=BATCH_SIZE,
138
- class_mode='categorical', subset='training',
139
- classes=CLASSES, shuffle=True
140
- )
141
-
142
- val_generator = train_datagen.flow_from_directory(
143
- DATASET_PATH, target_size=IMG_SIZE, batch_size=BATCH_SIZE,
144
- class_mode='categorical', subset='validation',
145
- classes=CLASSES, shuffle=False
146
- )
147
 
 
148
  try:
149
- train_classes = train_generator.classes
150
- class_weights = class_weight.compute_class_weight(
151
- class_weight='balanced',
152
- classes=np.unique(train_classes),
153
- y=train_classes
154
- )
155
- class_weights_dict = dict(enumerate(class_weights))
156
- print(f"[INFO] Class Weights: {class_weights_dict}")
157
- except:
158
- class_weights_dict = None
159
-
160
- # STAGE 1
161
- print("\n[STAGE 1] Warming up Head...")
162
- model = build_model(trainable=False)
163
- model.compile(optimizer=Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'])
164
-
165
- model.fit(
166
- train_generator,
167
- validation_data=val_generator,
168
- epochs=5,
169
- class_weight=class_weights_dict,
170
- verbose=1
171
  )
 
172
 
173
- # STAGE 2
174
- print("\n[STAGE 2] Unfreezing ENTIRE model (Training for 35 Epochs)...")
175
-
176
- for layer in model.layers:
177
- layer.trainable = True
178
-
179
- model.compile(optimizer=Adam(learning_rate=1e-4), loss='categorical_crossentropy', metrics=['accuracy'])
180
-
181
- # REMOVED EarlyStopping to force full 35 epochs
182
- callbacks = [
183
- ModelCheckpoint(MODEL_FILE, monitor='val_accuracy', save_best_only=True, verbose=1),
184
- ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-7, verbose=1)
185
- ]
186
-
187
- model.fit(
188
- train_generator,
189
- validation_data=val_generator,
190
- epochs=35, # Fixed at 35 epochs
191
- class_weight=class_weights_dict,
192
- callbacks=callbacks,
193
- verbose=1
194
  )
195
-
196
- print(f"[SUCCESS] Training complete.")
197
-
198
- # RELOAD BEST MODEL
199
- # Since we removed EarlyStopping, the model in memory is the LAST epoch.
200
- # We reload the file to ensure we use the BEST epoch saved by ModelCheckpoint.
201
- print(f"[INFO] Reloading best model from {MODEL_FILE}...")
202
- try:
203
- model = load_model(MODEL_FILE, custom_objects=CUSTOM_OBJECTS)
204
- except Exception as e:
205
- print(f"[WARN] Could not reload best model ({e}). Using last epoch weights.")
206
-
207
  return model
208
 
209
  # -------------------- LOAD / INIT --------------------
210
- def init_model():
211
- global MODEL, LOAD_ERROR
212
- LOAD_ERROR = None
213
-
214
- if os.path.exists(MODEL_FILE):
215
- print(f"[INIT] Model found: {MODEL_FILE}")
216
- try:
217
- MODEL = load_model(MODEL_FILE, custom_objects=CUSTOM_OBJECTS)
218
- print("[INIT] Model loaded successfully.")
219
- except Exception as e:
220
- print(f"[ERROR] Failed to load model: {e}")
221
- MODEL = train_on_dataset()
222
  else:
223
- print(f"[INIT] No model found. Starting training...")
224
- MODEL = train_on_dataset()
225
-
226
- # -------------------- INFERENCE HELPERS --------------------
227
- def process_single_image(image_path):
228
- try:
229
- img = load_img(image_path, target_size=IMG_SIZE)
230
- img_array = img_to_array(img)
231
- img_enhanced = enhance_medical_image(img_array)
232
- return np.expand_dims(img_enhanced, axis=0)
233
- except Exception as e: return str(e)
234
-
235
- def calculate_entropy(img_array):
236
- try:
237
- if img_array.dtype != np.uint8:
238
- calc_img = (img_array * 255).astype(np.uint8) if np.max(img_array) <= 1.0 else img_array.astype(np.uint8)
239
- else: calc_img = img_array
240
- if len(calc_img.shape) == 4: calc_img = calc_img[0]
241
- gray = cv2.cvtColor(calc_img, cv2.COLOR_RGB2GRAY)
242
- hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
243
- hist_norm = hist.ravel() / hist.sum()
244
- hist_norm = hist_norm[hist_norm > 0]
245
- return float(-np.sum(hist_norm * np.log2(hist_norm)))
246
- except: return 4.5
247
-
248
- def predict_smart(model, input_batch):
249
- img = input_batch[0]
250
- aug_batch = np.array([img, np.fliplr(img), np.flipud(img)])
251
- preds = model.predict(aug_batch)
252
- avg_pred = np.mean(preds, axis=0)
253
- return avg_pred
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  # -------------------- ROUTES --------------------
256
  @app.route("/")
@@ -259,7 +182,9 @@ def index():
259
 
260
  @app.route("/analyze", methods=["POST"])
261
  def analyze():
262
- if "image" not in request.files: return jsonify({"error": "No image"}), 400
 
 
263
  file = request.files["image"]
264
  temp_path = os.path.join(UPLOAD_FOLDER, f"scan_{int(time.time())}.jpg")
265
  file.save(temp_path)
@@ -267,53 +192,49 @@ def analyze():
267
  try:
268
  if not is_valid_eye_image(temp_path):
269
  return jsonify({
270
- "diagnosis": "Invalid Image",
271
- "severity": "Scan Rejected",
272
- "description": "The image detected does not appear to be a standard retina scan. Please ensure proper alignment.",
273
  "confidence": "0%", "color": "rose", "icon": "alert-circle", "features": {"entropy": "N/A"}
274
  })
275
 
276
- if MODEL is None:
277
- return jsonify({
278
- "diagnosis": "System Error",
279
- "description": "Model is training.",
280
- "confidence": "0%", "color": "rose", "icon": "alert-octagon"
281
- })
282
-
283
- input_data = process_single_image(temp_path)
284
- if isinstance(input_data, str):
285
- return jsonify({"diagnosis": "Error", "description": input_data, "confidence": "0%", "color": "rose", "icon": "alert-triangle"})
286
 
287
- preds = predict_smart(MODEL, input_data)
288
- idx = np.argmax(preds)
289
- label = CLASSES[idx]
290
- conf = preds[idx] * 100
291
- ent = calculate_entropy(input_data)
292
 
293
- # Updated Mapping for 5 Classes
294
  mapping = {
295
  "No_DR": ("No DR", "Normal", "emerald", "check-circle"),
296
  "Mild": ("Mild DR", "Stage 1", "yellow", "alert-triangle"),
297
  "Moderate": ("Moderate DR", "Stage 2", "orange", "alert-triangle"),
298
  "Severe": ("Severe DR", "Stage 3", "rose", "alert-octagon"),
299
- "Proliferate_DR": ("Proliferative DR", "Stage 4", "purple", "alert-octagon"),
300
  }
 
301
  diag, sev, col, icon = mapping.get(label, ("Unknown", "-", "gray", "help-circle"))
302
 
303
  return jsonify({
304
  "diagnosis": diag, "severity": sev, "color": col, "icon": icon,
305
- "description": f"AI Analysis: {diag}",
306
- "confidence": f"{conf:.1f}%",
307
  "features": {"entropy": f"{ent:.3f}"}
308
  })
309
 
310
  except Exception as e:
311
  return jsonify({"diagnosis": "Crash", "description": str(e), "confidence": "0%", "color": "rose", "icon": "x-octagon"})
312
  finally:
313
- if os.path.exists(temp_path): os.remove(temp_path)
 
314
 
315
  # -------------------- MAIN --------------------
316
- init_model()
317
 
318
  if __name__ == "__main__":
319
  app.run(debug=True, port=7860)
 
3
  import time
4
  import numpy as np
5
  import cv2
6
+
7
+ # Deep Learning Libraries (PyTorch)
8
+ import torch
9
+ import torch.nn as nn
10
+ from torchvision import models, transforms
 
 
 
 
 
 
11
 
12
  app = Flask(__name__)
13
 
14
  # -------------------- CONFIG --------------------
15
  UPLOAD_FOLDER = "uploads"
16
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
17
 
18
+ # Important: Update these paths to where your .pth files are saved locally!
19
+ DENSENET_PATH = "dr_model_final.pth"
20
+ EFFICIENTNET_PATH = "efficientnet_dr_model.pth"
21
 
22
+ # Exact class order used by the PyTorch ImageFolder during training
23
+ PYTORCH_CLASSES = ['Mild', 'Moderate', 'No_DR', 'Proliferative', 'Severe']
24
+ NUM_CLASSES = len(PYTORCH_CLASSES)
25
 
26
+ DENSENET_MODEL = None
27
+ EFFICIENTNET_MODEL = None
28
+ DEVICE = None
29
+
30
+ # -------------------- PREPROCESSING --------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def enhance_medical_image(img):
32
+ """Applies CLAHE enhancement to retina images."""
33
  try:
34
+ # Convert to uint8 if not already
35
+ if np.max(img) <= 1.0:
36
+ img = (img * 255).astype(np.uint8)
37
+ else:
38
+ img = img.astype(np.uint8)
39
 
40
  lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)
41
  l, a, b = cv2.split(lab)
 
44
  limg = cv2.merge((cl, a, b))
45
  final = cv2.cvtColor(limg, cv2.COLOR_LAB2RGB)
46
 
47
+ # Return uint8 array for PyTorch ToPILImage transform
48
+ return final
49
  except Exception:
50
+ return img.astype(np.uint8)
51
 
 
52
  def is_valid_eye_image(img_path):
53
+ """Validation to ensure uploaded images are likely retina scans."""
54
  try:
55
  img = cv2.imread(img_path)
56
  if img is None: return False
 
69
  if mean_brightness < 5 or mean_brightness > 240: return False
70
 
71
  return True
72
+ except Exception:
73
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ def calculate_entropy(img_array):
76
  try:
77
+ gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
78
+ hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
79
+ hist_norm = hist.ravel() / hist.sum()
80
+ hist_norm = hist_norm[hist_norm > 0]
81
+ return float(-np.sum(hist_norm * np.log2(hist_norm)))
82
+ except:
83
+ return 4.5
84
+
85
+ # -------------------- MODEL BUILDERS --------------------
86
+ def build_densenet():
87
+ model = models.densenet121(weights=None)
88
+ num_ftrs = model.classifier.in_features
89
+ model.classifier = nn.Sequential(
90
+ nn.Linear(num_ftrs, 512),
91
+ nn.ReLU(),
92
+ nn.Dropout(0.4),
93
+ nn.Linear(512, NUM_CLASSES)
 
 
 
 
 
94
  )
95
+ return model
96
 
97
+ def build_efficientnet():
98
+ model = models.efficientnet_b4(weights=None)
99
+ num_ftrs = model.classifier[1].in_features
100
+ model.classifier = nn.Sequential(
101
+ nn.Dropout(p=0.5, inplace=True),
102
+ nn.Linear(num_ftrs, 512),
103
+ nn.BatchNorm1d(512),
104
+ nn.ReLU(),
105
+ nn.Dropout(p=0.5),
106
+ nn.Linear(512, NUM_CLASSES)
 
 
 
 
 
 
 
 
 
 
 
107
  )
 
 
 
 
 
 
 
 
 
 
 
 
108
  return model
109
 
110
  # -------------------- LOAD / INIT --------------------
111
+ def init_models():
112
+ global DENSENET_MODEL, EFFICIENTNET_MODEL, DEVICE
113
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
114
+ print(f"[INIT] Initializing Ensemble on {DEVICE}...")
115
+
116
+ # Load DenseNet
117
+ DENSENET_MODEL = build_densenet()
118
+ if os.path.exists(DENSENET_PATH):
119
+ checkpoint = torch.load(DENSENET_PATH, map_location=DEVICE)
120
+ DENSENET_MODEL.load_state_dict(checkpoint['model_state_dict'])
121
+ print(f"[INIT] DenseNet loaded successfully.")
 
122
  else:
123
+ print(f"[WARNING] DenseNet file missing at {DENSENET_PATH}. App will crash on inference.")
124
+ DENSENET_MODEL = DENSENET_MODEL.to(DEVICE)
125
+ DENSENET_MODEL.eval()
126
+
127
+ # Load EfficientNet
128
+ EFFICIENTNET_MODEL = build_efficientnet()
129
+ if os.path.exists(EFFICIENTNET_PATH):
130
+ checkpoint = torch.load(EFFICIENTNET_PATH, map_location=DEVICE)
131
+ EFFICIENTNET_MODEL.load_state_dict(checkpoint['model_state_dict'])
132
+ print(f"[INIT] EfficientNet loaded successfully.")
133
+ else:
134
+ print(f"[WARNING] EfficientNet file missing at {EFFICIENTNET_PATH}. App will crash on inference.")
135
+ EFFICIENTNET_MODEL = EFFICIENTNET_MODEL.to(DEVICE)
136
+ EFFICIENTNET_MODEL.eval()
137
+
138
+ # -------------------- ENSEMBLE INFERENCE --------------------
139
+ def predict_ensemble(img_array):
140
+ # PyTorch transforms (requires uint8 array as input to ToPILImage)
141
+ transform_dense = transforms.Compose([
142
+ transforms.ToPILImage(),
143
+ transforms.Resize((256, 256)),
144
+ transforms.ToTensor(),
145
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
146
+ ])
147
+
148
+ transform_eff = transforms.Compose([
149
+ transforms.ToPILImage(),
150
+ transforms.Resize((288, 288)),
151
+ transforms.ToTensor(),
152
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
153
+ ])
154
+
155
+ # Convert directly to device tensors
156
+ t_dense = transform_dense(img_array).unsqueeze(0).to(DEVICE)
157
+ t_eff = transform_eff(img_array).unsqueeze(0).to(DEVICE)
158
+
159
+ with torch.no_grad():
160
+ # --- DenseNet TTA ---
161
+ d_out1 = torch.softmax(DENSENET_MODEL(t_dense), dim=1)
162
+ d_out2 = torch.softmax(DENSENET_MODEL(torch.flip(t_dense, dims=[3])), dim=1) # Horiz flip
163
+ d_out3 = torch.softmax(DENSENET_MODEL(torch.flip(t_dense, dims=[2])), dim=1) # Vert flip
164
+ d_probs = (d_out1 + d_out2 + d_out3) / 3.0
165
+
166
+ # --- EfficientNet TTA ---
167
+ e_out1 = torch.softmax(EFFICIENTNET_MODEL(t_eff), dim=1)
168
+ e_out2 = torch.softmax(EFFICIENTNET_MODEL(torch.flip(t_eff, dims=[3])), dim=1)
169
+ e_out3 = torch.softmax(EFFICIENTNET_MODEL(torch.flip(t_eff, dims=[2])), dim=1)
170
+ e_probs = (e_out1 + e_out2 + e_out3) / 3.0
171
+
172
+ # --- Soft Voting Ensemble ---
173
+ ensemble_probs = (d_probs + e_probs) / 2.0
174
+ confidence, pred_idx = torch.max(ensemble_probs, 1)
175
+
176
+ return PYTORCH_CLASSES[pred_idx.item()], confidence.item()
177
 
178
  # -------------------- ROUTES --------------------
179
  @app.route("/")
 
182
 
183
  @app.route("/analyze", methods=["POST"])
184
  def analyze():
185
+ if "image" not in request.files:
186
+ return jsonify({"error": "No image uploaded"}), 400
187
+
188
  file = request.files["image"]
189
  temp_path = os.path.join(UPLOAD_FOLDER, f"scan_{int(time.time())}.jpg")
190
  file.save(temp_path)
 
192
  try:
193
  if not is_valid_eye_image(temp_path):
194
  return jsonify({
195
+ "diagnosis": "Invalid Image", "severity": "Scan Rejected",
196
+ "description": "The image detected does not appear to be a standard retina scan.",
 
197
  "confidence": "0%", "color": "rose", "icon": "alert-circle", "features": {"entropy": "N/A"}
198
  })
199
 
200
+ # Load image via OpenCV and apply enhancements
201
+ img = cv2.imread(temp_path)
202
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
203
+ img_enhanced = enhance_medical_image(img)
204
+
205
+ # Calculate Entropy
206
+ ent = calculate_entropy(img_enhanced)
 
 
 
207
 
208
+ # Run Ensemble Prediction
209
+ label, conf = predict_ensemble(img_enhanced)
210
+ conf_percentage = conf * 100
 
 
211
 
212
+ # Map PyTorch labels to Front-end UI UI mapping
213
  mapping = {
214
  "No_DR": ("No DR", "Normal", "emerald", "check-circle"),
215
  "Mild": ("Mild DR", "Stage 1", "yellow", "alert-triangle"),
216
  "Moderate": ("Moderate DR", "Stage 2", "orange", "alert-triangle"),
217
  "Severe": ("Severe DR", "Stage 3", "rose", "alert-octagon"),
218
+ "Proliferative": ("Proliferative DR", "Stage 4", "purple", "alert-octagon"),
219
  }
220
+
221
  diag, sev, col, icon = mapping.get(label, ("Unknown", "-", "gray", "help-circle"))
222
 
223
  return jsonify({
224
  "diagnosis": diag, "severity": sev, "color": col, "icon": icon,
225
+ "description": f"Ensemble AI Analysis: {diag}",
226
+ "confidence": f"{conf_percentage:.1f}%",
227
  "features": {"entropy": f"{ent:.3f}"}
228
  })
229
 
230
  except Exception as e:
231
  return jsonify({"diagnosis": "Crash", "description": str(e), "confidence": "0%", "color": "rose", "icon": "x-octagon"})
232
  finally:
233
+ if os.path.exists(temp_path):
234
+ os.remove(temp_path)
235
 
236
  # -------------------- MAIN --------------------
237
+ init_models()
238
 
239
  if __name__ == "__main__":
240
  app.run(debug=True, port=7860)