rocky200416 commited on
Commit
605ba9f
·
verified ·
1 Parent(s): 817d4c7

Upload app_pytorch_inference.py

Browse files
Files changed (1) hide show
  1. app_pytorch_inference.py +503 -0
app_pytorch_inference.py ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_pytorch_inference.py
2
+ """
3
+ Robust Flask inference server for multi-task EfficientNet-B3 model (classification + segmentation).
4
+ - Robust checkpoint/state_dict loading
5
+ - Tolerant Grad-CAM initialization across versions
6
+ - Thread-safe Grad-CAM usage
7
+ - Optional skipping of CAM/mask via query params
8
+ - SQLite logging of predictions
9
+ - CORS configured for dev origins
10
+ """
11
+ import io
12
+ import os
13
+ import json
14
+ import base64
15
+ import traceback
16
+ import threading
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+ from PIL import Image
20
+ import numpy as np
21
+ import cv2
22
+
23
+ from flask import Flask, request, jsonify
24
+ from flask_cors import CORS
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ import torch.nn.functional as F
29
+ from torchvision import models
30
+ import torchvision.transforms as T
31
+
32
+ # tolerant import of pytorch-grad-cam
33
+ try:
34
+ from pytorch_grad_cam import GradCAM
35
+ from pytorch_grad_cam.utils.image import show_cam_on_image, preprocess_image
36
+ from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
37
+ except Exception:
38
+ GradCAM = None
39
+ show_cam_on_image = None
40
+ preprocess_image = None
41
+ ClassifierOutputTarget = None
42
+
43
+ from sqlalchemy import create_engine, text
44
+ import pandas as pd
45
+
46
+ # ---------------- CONFIG - edit these ----------------
47
+ # FIX 1: Use a relative path. Assumes .pth is in the same folder as this script.
48
+ # Fix: Use the script's own location to find the file reliably
49
+ MODEL_PATH = Path(__file__).parent / "models" / "eye_model_lite.pth"
50
+ LOG_DB_PATH = "sqlite:///predictions_flask.db"
51
+ IMG_SIZE = 224
52
+ MAX_UPLOAD_MB = 12
53
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
54
+
55
+ # CORS origins
56
+ CORS_ORIGINS = [
57
+ "http://localhost:3000",
58
+ "http://127.0.0.1:3000",
59
+ "https://ai-eye-disease-detection-chi.vercel.app", # REPLACE THIS with your actual Vercel URL after deploying frontend
60
+ ]
61
+ # ----------------------------------------------------
62
+
63
+ # Flask app
64
+ app = Flask(__name__)
65
+
66
+ # FIX 2: Correct syntax using the 'origins' key and the variable defined above
67
+ CORS(app)
68
+
69
+ app.config["MAX_CONTENT_LENGTH"] = MAX_UPLOAD_MB * 1024 * 1024
70
+
71
+ # DB engine
72
+ engine = create_engine(LOG_DB_PATH, echo=False)
73
+
74
+ # ... (The rest of your Model class, Load functions, and Routes go here) ...
75
+ # ... (The rest of your Model class, Load functions, and Routes go here) ...
76
+
77
+ # class map - MUST match training order
78
+ CLASS_MAP_INV = {
79
+ 0: "Normal",
80
+ 1: "Cataract",
81
+ 2: "Diabetic Retinopathy",
82
+ 3: "Glaucoma"
83
+ }
84
+ NUM_CLASSES = len(CLASS_MAP_INV)
85
+
86
+ # ---------------- Model definition (must match training) ----------------
87
+ class MultiTaskNet(nn.Module):
88
+ def __init__(self, num_classes=4, dropout=0.5, img_size=IMG_SIZE):
89
+ super().__init__()
90
+ # EfficientNet-B3 from torchvision (weights argument available in torchvision)
91
+ self.encoder = models.efficientnet_b3(weights=None)
92
+
93
+ # We need to match the architecture exactly.
94
+ # EfficientNet features usually output 1536 channels for B3.
95
+ # The user script used dynamic lookup: self.encoder.classifier[1].in_features
96
+ # We will replicate that safety check but defaulting to standard if missing.
97
+ try:
98
+ enc_features = self.encoder.classifier[1].in_features
99
+ except:
100
+ enc_features = 1536
101
+
102
+ self.encoder.classifier = nn.Identity()
103
+ self.classifier = nn.Sequential(nn.Dropout(dropout), nn.Linear(enc_features, num_classes))
104
+
105
+ # Segmentation Head - matches kernel_size=4 from your checkpoint
106
+ self.seg_head = nn.Sequential(
107
+ nn.ConvTranspose2d(enc_features, 256, 4, 2, 1), nn.ReLU(),
108
+ nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.ReLU(),
109
+ nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.ReLU(),
110
+ nn.ConvTranspose2d(64, 32, 4, 2, 1), nn.ReLU(),
111
+ nn.ConvTranspose2d(32, 1, 4, 2, 1)
112
+ )
113
+ self.log_vars = nn.Parameter(torch.zeros(2))
114
+ self.img_size = img_size
115
+
116
+ def forward(self, x):
117
+ feats = self.encoder.features(x)
118
+ # Global Average Pooling
119
+ pooled = F.adaptive_avg_pool2d(feats, 1).reshape(feats.shape[0], -1)
120
+ cls_out = self.classifier(pooled)
121
+
122
+ seg_out = self.seg_head(feats)
123
+ # Guarantee output size is (img_size, img_size)
124
+ if seg_out.shape[-2:] != (self.img_size, self.img_size):
125
+ seg_out = F.interpolate(seg_out, size=(self.img_size, self.img_size), mode='bilinear', align_corners=False)
126
+ return cls_out, seg_out
127
+
128
+ # ---------------- Globals ----------------
129
+ _model = None
130
+ _gradcam = None
131
+ _classification_wrapper = None
132
+ _gradcam_lock = threading.Lock()
133
+
134
+ # Preprocess transform
135
+ MEAN = [0.485, 0.456, 0.406]
136
+ STD = [0.229, 0.224, 0.225]
137
+ def build_preprocess():
138
+ return T.Compose([
139
+ T.Resize((IMG_SIZE, IMG_SIZE)),
140
+ T.ToTensor(),
141
+ T.Normalize(mean=MEAN, std=STD)
142
+ ])
143
+
144
+ def pil_to_tensor_for_model(pil_img):
145
+ tf = build_preprocess()
146
+ return tf(pil_img).unsqueeze(0).to(DEVICE)
147
+
148
+ def encode_base64_png_from_pil(pil_img):
149
+ buff = io.BytesIO()
150
+ pil_img.save(buff, format="PNG")
151
+ buff.seek(0)
152
+ return base64.b64encode(buff.read()).decode("utf-8")
153
+
154
+ def overlay_heatmap_on_pil(pil_rgb, cam_mask, alpha=0.4):
155
+ # pil_rgb: PIL Image resized to IMG_SIZE
156
+ rgb = np.array(pil_rgb).astype(np.float32) / 255.0
157
+ # cam_mask assumed 2D, values in [0,1]
158
+ cam_uint8 = (np.clip(cam_mask, 0, 1) * 255).astype("uint8")
159
+ # apply OpenCV colormap
160
+ heatmap = cv2.applyColorMap(cam_uint8, cv2.COLORMAP_JET)
161
+ heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB).astype(np.float32)/255.0
162
+ overlay = (1-alpha)*rgb + alpha*heatmap
163
+ overlay = np.clip(overlay, 0, 1)
164
+ overlay_img = Image.fromarray((overlay*255).astype("uint8"))
165
+ return overlay_img
166
+
167
+ # --- NEW FUNCTION FOR RED MASK OVERLAY ---
168
+ def overlay_red_mask_on_pil(pil_rgb, binary_mask_uint8, alpha=0.5):
169
+ """
170
+ Overlays a red color where the mask is 1 (255), transparent elsewhere.
171
+ binary_mask_uint8: numpy array of shape (H, W), values 0 or 255
172
+ """
173
+ rgb = np.array(pil_rgb)
174
+
175
+ # Create a solid red image
176
+ red_layer = np.zeros_like(rgb)
177
+ red_layer[:, :, 0] = 255 # Red channel full
178
+
179
+ # Create mask boolean
180
+ mask_bool = binary_mask_uint8 > 0
181
+
182
+ # Blend only where mask is True
183
+ output = rgb.copy()
184
+ output[mask_bool] = (rgb[mask_bool] * (1 - alpha) + red_layer[mask_bool] * alpha).astype(np.uint8)
185
+
186
+ return Image.fromarray(output)
187
+
188
+ # ---------------- DB utilities ----------------
189
+ def init_db():
190
+ with engine.begin() as conn:
191
+ conn.execute(text("""
192
+ CREATE TABLE IF NOT EXISTS predictions (
193
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
194
+ filename TEXT,
195
+ predicted_disease TEXT,
196
+ confidence REAL,
197
+ probabilities TEXT,
198
+ heatmap_base64 TEXT,
199
+ mask_base64 TEXT,
200
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
201
+ )
202
+ """))
203
+
204
+ # ---------------- Model loader (robust GradCAM init) ----------------
205
+ def _find_target_conv(module: nn.Module):
206
+ # Prefer last Conv2d in encoder.features, else search entire model
207
+ try:
208
+ feats = module.encoder.features
209
+ # attempt: find last Conv2d inside features (descend)
210
+ last = None
211
+ for m in feats.modules():
212
+ if isinstance(m, nn.Conv2d):
213
+ last = m
214
+ if last is not None:
215
+ return last
216
+ except Exception:
217
+ pass
218
+ # fallback: search entire module
219
+ last = None
220
+ for m in module.modules():
221
+ if isinstance(m, nn.Conv2d):
222
+ last = m
223
+ return last
224
+
225
+ def load_model():
226
+ global _model, _gradcam, _classification_wrapper
227
+ if _model is not None:
228
+ return
229
+
230
+ if not MODEL_PATH.exists():
231
+ raise FileNotFoundError(f"Model checkpoint not found: {MODEL_PATH}")
232
+
233
+ print("Loading model from:", MODEL_PATH)
234
+ m = MultiTaskNet(num_classes=NUM_CLASSES).to(DEVICE)
235
+
236
+ try:
237
+ ckpt = torch.load(MODEL_PATH, map_location=DEVICE)
238
+ except Exception as e:
239
+ print(f"Failed to load checkpoint file: {e}")
240
+ raise e
241
+
242
+ # robustly obtain state_dict
243
+ state = ckpt
244
+ if isinstance(ckpt, dict):
245
+ # Priority check for 'model' key which we know works
246
+ if 'model' in ckpt:
247
+ state = ckpt['model']
248
+ else:
249
+ for key in ("model_state", "model_state_dict", "state_dict"):
250
+ if key in ckpt:
251
+ state = ckpt[key]
252
+ break
253
+
254
+ # normalize keys (strip 'module.' if present) - Keeping this for robustness
255
+ if isinstance(state, dict):
256
+ new_state = {}
257
+ for k, v in state.items():
258
+ nk = k.replace("module.", "") if isinstance(k, str) and k.startswith("module.") else k
259
+ new_state[nk] = v
260
+ state = new_state
261
+
262
+ # attempt strict load, then fallback
263
+ try:
264
+ m.load_state_dict(state, strict=True)
265
+ print("✅ Model loaded with strict=True")
266
+ except Exception as e:
267
+ print("Warning: strict load_state_dict failed:", e)
268
+ try:
269
+ m.load_state_dict(state, strict=False)
270
+ print("⚠️ Loaded with strict=False (Some keys might be missing/unexpected, this is often okay).")
271
+ except Exception as e2:
272
+ print("Final load attempt failed:", e2)
273
+ raise e2
274
+
275
+ m.eval()
276
+ _model = m.to(DEVICE)
277
+ print("Model loaded to", DEVICE)
278
+
279
+ # Setup Grad-CAM tolerant to API differences
280
+ if GradCAM is None or show_cam_on_image is None or preprocess_image is None:
281
+ print("pytorch-grad-cam not available or incomplete; Grad-CAM disabled.")
282
+ _gradcam = None
283
+ _classification_wrapper = None
284
+ return
285
+
286
+ # find a sensible target layer
287
+ target_layer = _find_target_conv(_model)
288
+ if target_layer is None:
289
+ print("Could not find a conv layer for Grad-CAM; disabling CAM.")
290
+ _gradcam = None
291
+ _classification_wrapper = None
292
+ return
293
+
294
+ # wrapper returns only logits for Grad-CAM
295
+ class ClassificationOnlyWrapper(nn.Module):
296
+ def __init__(self, full_model):
297
+ super().__init__()
298
+ self.full = full_model
299
+ def forward(self, x):
300
+ cls, _ = self.full(x)
301
+ return cls
302
+
303
+ _classification_wrapper = ClassificationOnlyWrapper(_model).to(DEVICE)
304
+
305
+ # Try multiple GradCAM init signatures
306
+ _gradcam = None
307
+ try:
308
+ # preferred: use_cuda argument (older versions)
309
+ _gradcam = GradCAM(model=_classification_wrapper, target_layers=[target_layer], use_cuda=(DEVICE=="cuda"))
310
+ print("GradCAM initialized with use_cuda.")
311
+ except TypeError:
312
+ try:
313
+ # alternate: device argument
314
+ _gradcam = GradCAM(model=_classification_wrapper, target_layers=[target_layer], device=torch.device(DEVICE))
315
+ print("GradCAM initialized with device arg.")
316
+ except TypeError:
317
+ try:
318
+ # simplest init
319
+ _gradcam = GradCAM(model=_classification_wrapper, target_layers=[target_layer])
320
+ print("GradCAM initialized without extra kwargs.")
321
+ except Exception as e:
322
+ print("GradCAM initialization failed; disabling CAM. Error:", e)
323
+ _gradcam = None
324
+ except Exception as e:
325
+ print("Unexpected error initializing GradCAM; disabling CAM. Error:", e)
326
+ _gradcam = None
327
+
328
+ if _gradcam is not None:
329
+ print("GradCAM ready.")
330
+ else:
331
+ print("GradCAM not available; continuing without CAM.")
332
+
333
+ # ---------------- Routes ----------------
334
+ @app.route("/", methods=["GET", "HEAD"])
335
+ def index():
336
+ return "Backend is running!"
337
+
338
+ @app.route("/health", methods=["GET"])
339
+ def health():
340
+ return jsonify({"status": "ok", "device": DEVICE})
341
+
342
+ @app.route("/history", methods=["GET"])
343
+ def history():
344
+ try:
345
+ with engine.connect() as conn:
346
+ rows = conn.execute(text(
347
+ "SELECT id, filename, predicted_disease, confidence, probabilities, created_at FROM predictions ORDER BY created_at DESC LIMIT 200"
348
+ )).fetchall()
349
+ out = []
350
+ for r in rows:
351
+ out.append({
352
+ "id": r[0],
353
+ "filename": r[1],
354
+ "predicted_disease": r[2],
355
+ "confidence": float(r[3]) if r[3] is not None else None,
356
+ "probabilities": json.loads(r[4]) if r[4] else None,
357
+ "created_at": str(r[5])
358
+ })
359
+ return jsonify(out)
360
+ except Exception as e:
361
+ return jsonify({"error": str(e)}), 500
362
+
363
+ @app.route("/predict", methods=["POST"])
364
+ def predict():
365
+ """
366
+ POST multipart/form-data with key "image" -> file
367
+ Optional query params:
368
+ - no_cam=1 -> skip Grad-CAM generation
369
+ - no_mask=1 -> skip mask generation (return no mask)
370
+ """
371
+ try:
372
+ # ensure model + DB ready
373
+ load_model()
374
+ init_db()
375
+
376
+ if "image" not in request.files:
377
+ return jsonify({"error": "no image file uploaded under key 'image'"}), 400
378
+ f = request.files["image"]
379
+ if f.filename == "":
380
+ return jsonify({"error": "empty filename"}), 400
381
+
382
+ no_cam = request.args.get("no_cam", "0").lower() in ("1", "true", "yes")
383
+ no_mask = request.args.get("no_mask", "0").lower() in ("1", "true", "yes")
384
+
385
+ pil = Image.open(f.stream).convert("RGB")
386
+ pil_resized = pil.resize((IMG_SIZE, IMG_SIZE))
387
+ inp_tensor = pil_to_tensor_for_model(pil_resized)
388
+
389
+ # run forward (classification + segmentation) under no_grad
390
+ # with torch.no_grad():
391
+ # run forward (classification + segmentation) using inference_mode (uses less RAM)
392
+ with torch.inference_mode():
393
+ out = _model(inp_tensor)
394
+ # Handle potential output formats
395
+ if isinstance(out, (list, tuple)):
396
+ cls_logits = out[0]
397
+ seg_logits = out[1] if len(out) > 1 else None
398
+ else:
399
+ cls_logits = out
400
+ seg_logits = None
401
+
402
+ if not isinstance(cls_logits, torch.Tensor):
403
+ raise RuntimeError(f"Unexpected classification output type: {type(cls_logits)}")
404
+
405
+ # --- CLASSIFICATION LOGIC ---
406
+ probs = torch.softmax(cls_logits, dim=1).cpu().numpy()[0]
407
+ pred_idx = int(np.argmax(probs))
408
+ pred_label = CLASS_MAP_INV.get(pred_idx, str(pred_idx))
409
+ confidence = float(probs[pred_idx])
410
+
411
+ # --- SEGMENTATION MASK LOGIC (UPDATED) ---
412
+ mask_b64 = None
413
+ if (not no_mask) and (seg_logits is not None):
414
+ try:
415
+ seg_prob = torch.sigmoid(seg_logits).detach().cpu().numpy()[0, 0]
416
+
417
+ # 1. NORMAL SUPPRESSION (If Normal, mask is empty)
418
+ if pred_label == "Normal":
419
+ mask_uint8 = np.zeros_like(seg_prob, dtype="uint8")
420
+ else:
421
+ # 2. LOW THRESHOLD (0.25 to catch partial confidence)
422
+ mask_uint8 = (seg_prob > 0.25).astype("uint8") * 255
423
+
424
+ # 3. GENERATE RED OVERLAY
425
+ if mask_uint8.max() > 0:
426
+ # Use the new Red Overlay function
427
+ mask_pil_overlay = overlay_red_mask_on_pil(pil_resized, mask_uint8)
428
+ mask_b64 = encode_base64_png_from_pil(mask_pil_overlay)
429
+ else:
430
+ # Return clean image if empty
431
+ mask_b64 = encode_base64_png_from_pil(pil_resized)
432
+
433
+ except Exception as e:
434
+ print(f"Mask generation failed: {e}")
435
+ traceback.print_exc()
436
+ mask_b64 = None
437
+
438
+ # Grad-CAM (thread-safe)
439
+ overlay_b64 = None
440
+ if (not no_cam) and (_gradcam is not None) and (preprocess_image is not None):
441
+ try:
442
+ rgb_for_cam = np.array(pil_resized).astype(np.float32) / 255.0
443
+ input_for_cam = preprocess_image(rgb_for_cam, mean=MEAN, std=STD).to(DEVICE)
444
+ # thread-safe call
445
+ with _gradcam_lock:
446
+ grayscale_cam = _gradcam(input_for_cam, targets=[ClassifierOutputTarget(pred_idx)])
447
+
448
+ cam_np = np.array(grayscale_cam)
449
+ cam_np = np.squeeze(cam_np)
450
+ if cam_np.ndim == 3:
451
+ cam_np = cam_np[0]
452
+
453
+ cam_np = cam_np.astype(np.float32)
454
+ if cam_np.max() > 0:
455
+ cam_np = (cam_np - cam_np.min()) / (cam_np.max() - cam_np.min() + 1e-8)
456
+ else:
457
+ cam_np = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.float32)
458
+
459
+ if cam_np.shape != (IMG_SIZE, IMG_SIZE):
460
+ cam_np = cv2.resize(cam_np, (IMG_SIZE, IMG_SIZE))
461
+
462
+ overlay_pil = overlay_heatmap_on_pil(pil_resized, cam_np)
463
+ overlay_b64 = encode_base64_png_from_pil(overlay_pil)
464
+ except Exception as e:
465
+ print("Grad-CAM generation error:", e)
466
+ traceback.print_exc()
467
+ overlay_b64 = None
468
+
469
+ probabilities_json = json.dumps({CLASS_MAP_INV[i]: float(round(float(probs[i]), 6)) for i in range(len(probs))})
470
+
471
+ # store in DB
472
+ with engine.begin() as conn:
473
+ conn.execute(text(
474
+ "INSERT INTO predictions (filename, predicted_disease, confidence, probabilities, heatmap_base64, mask_base64) VALUES (:fn,:pd,:c,:p,:h,:m)"
475
+ ), {"fn": f.filename, "pd": pred_label, "c": confidence, "p": probabilities_json, "h": overlay_b64, "m": mask_b64})
476
+
477
+ response = {
478
+ "predicted_disease": pred_label,
479
+ "confidence": confidence,
480
+ "probabilities": json.loads(probabilities_json)
481
+ }
482
+ if overlay_b64 is not None:
483
+ response["heatmap_png_base64"] = overlay_b64
484
+ if mask_b64 is not None:
485
+ response["mask_png_base64"] = mask_b64
486
+
487
+ return jsonify(response)
488
+
489
+ except Exception as e:
490
+ traceback.print_exc()
491
+ return jsonify({"error": str(e)}), 500
492
+
493
+ # ---------------- Main ----------------
494
+ if __name__ == "__main__":
495
+ print("Starting Flask server on 0.0.0.0:8000")
496
+ init_db()
497
+ # lazy model load on first request, or load now:
498
+ try:
499
+ load_model()
500
+ except Exception as e:
501
+ print("Model failed to load on startup:", e)
502
+ # For production use a WSGI server (gunicorn, waitress, etc.)
503
+ app.run(host="0.0.0.0", port=8000, debug=False)