eye-cv 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. eye/__init__.py +115 -0
  2. eye/__init___supervision_original.py +120 -0
  3. eye/annotators/__init__.py +0 -0
  4. eye/annotators/base.py +22 -0
  5. eye/annotators/core.py +2699 -0
  6. eye/annotators/line.py +107 -0
  7. eye/annotators/modern.py +529 -0
  8. eye/annotators/trace.py +142 -0
  9. eye/annotators/utils.py +177 -0
  10. eye/assets/__init__.py +2 -0
  11. eye/assets/downloader.py +95 -0
  12. eye/assets/list.py +83 -0
  13. eye/classification/__init__.py +0 -0
  14. eye/classification/core.py +188 -0
  15. eye/config.py +2 -0
  16. eye/core/__init__.py +0 -0
  17. eye/core/trackers/__init__.py +1 -0
  18. eye/core/trackers/botsort_tracker.py +336 -0
  19. eye/core/trackers/bytetrack_tracker.py +284 -0
  20. eye/core/trackers/sort_tracker.py +200 -0
  21. eye/core/tracking.py +146 -0
  22. eye/dataset/__init__.py +0 -0
  23. eye/dataset/core.py +919 -0
  24. eye/dataset/formats/__init__.py +0 -0
  25. eye/dataset/formats/coco.py +258 -0
  26. eye/dataset/formats/pascal_voc.py +279 -0
  27. eye/dataset/formats/yolo.py +272 -0
  28. eye/dataset/utils.py +259 -0
  29. eye/detection/__init__.py +0 -0
  30. eye/detection/auto_convert.py +155 -0
  31. eye/detection/core.py +1529 -0
  32. eye/detection/detections_enhanced.py +392 -0
  33. eye/detection/line_zone.py +859 -0
  34. eye/detection/lmm.py +184 -0
  35. eye/detection/overlap_filter.py +270 -0
  36. eye/detection/tools/__init__.py +0 -0
  37. eye/detection/tools/csv_sink.py +181 -0
  38. eye/detection/tools/inference_slicer.py +288 -0
  39. eye/detection/tools/json_sink.py +142 -0
  40. eye/detection/tools/polygon_zone.py +202 -0
  41. eye/detection/tools/smoother.py +123 -0
  42. eye/detection/tools/smoothing.py +179 -0
  43. eye/detection/tools/smoothing_config.py +202 -0
  44. eye/detection/tools/transformers.py +247 -0
  45. eye/detection/utils.py +1175 -0
  46. eye/draw/__init__.py +0 -0
  47. eye/draw/color.py +154 -0
  48. eye/draw/utils.py +374 -0
  49. eye/filters.py +112 -0
  50. eye/geometry/__init__.py +0 -0
  51. eye/geometry/core.py +128 -0
  52. eye/geometry/utils.py +47 -0
  53. eye/keypoint/__init__.py +0 -0
  54. eye/keypoint/annotators.py +442 -0
  55. eye/keypoint/core.py +687 -0
  56. eye/keypoint/skeletons.py +2647 -0
  57. eye/metrics/__init__.py +21 -0
  58. eye/metrics/core.py +72 -0
  59. eye/metrics/detection.py +843 -0
  60. eye/metrics/f1_score.py +648 -0
  61. eye/metrics/mean_average_precision.py +628 -0
  62. eye/metrics/mean_average_recall.py +697 -0
  63. eye/metrics/precision.py +653 -0
  64. eye/metrics/recall.py +652 -0
  65. eye/metrics/utils/__init__.py +0 -0
  66. eye/metrics/utils/object_size.py +158 -0
  67. eye/metrics/utils/utils.py +9 -0
  68. eye/py.typed +0 -0
  69. eye/quick.py +104 -0
  70. eye/tracker/__init__.py +0 -0
  71. eye/tracker/byte_tracker/__init__.py +0 -0
  72. eye/tracker/byte_tracker/core.py +386 -0
  73. eye/tracker/byte_tracker/kalman_filter.py +205 -0
  74. eye/tracker/byte_tracker/matching.py +69 -0
  75. eye/tracker/byte_tracker/single_object_track.py +178 -0
  76. eye/tracker/byte_tracker/utils.py +18 -0
  77. eye/utils/__init__.py +0 -0
  78. eye/utils/conversion.py +132 -0
  79. eye/utils/file.py +159 -0
  80. eye/utils/image.py +794 -0
  81. eye/utils/internal.py +200 -0
  82. eye/utils/iterables.py +84 -0
  83. eye/utils/notebook.py +114 -0
  84. eye/utils/video.py +307 -0
  85. eye/utils_eye/__init__.py +1 -0
  86. eye/utils_eye/geometry.py +71 -0
  87. eye/utils_eye/nms.py +55 -0
  88. eye/validators/__init__.py +140 -0
  89. eye/web.py +271 -0
  90. eye_cv-1.0.0.dist-info/METADATA +319 -0
  91. eye_cv-1.0.0.dist-info/RECORD +94 -0
  92. eye_cv-1.0.0.dist-info/WHEEL +5 -0
  93. eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
  94. eye_cv-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,71 @@
1
+ """Geometry utilities."""
2
+
3
+ import numpy as np
4
+ from dataclasses import dataclass
5
+ from typing import Tuple
6
+
7
+
8
+ @dataclass
9
+ class Point:
10
+ """2D point."""
11
+ x: float
12
+ y: float
13
+
14
+ def as_tuple(self) -> Tuple[float, float]:
15
+ return (self.x, self.y)
16
+
17
+ def as_int_tuple(self) -> Tuple[int, int]:
18
+ return (int(self.x), int(self.y))
19
+
20
+
21
+ def get_polygon_center(polygon: np.ndarray) -> Point:
22
+ """Calculate center of polygon."""
23
+ center_x = np.mean(polygon[:, 0])
24
+ center_y = np.mean(polygon[:, 1])
25
+ return Point(center_x, center_y)
26
+
27
+
28
+ def is_point_in_polygon(points: np.ndarray, polygon: np.ndarray) -> np.ndarray:
29
+ """Check if points are inside polygon.
30
+
31
+ Args:
32
+ points: (N, 2) array of points
33
+ polygon: (M, 2) array of polygon vertices
34
+
35
+ Returns:
36
+ Boolean array of length N
37
+ """
38
+ from matplotlib.path import Path
39
+ path = Path(polygon)
40
+ return path.contains_points(points)
41
+
42
+
43
+ def calculate_iou(box1: np.ndarray, box2: np.ndarray) -> float:
44
+ """Calculate IoU between two boxes.
45
+
46
+ Args:
47
+ box1, box2: [x1, y1, x2, y2]
48
+ """
49
+ x1_inter = max(box1[0], box2[0])
50
+ y1_inter = max(box1[1], box2[1])
51
+ x2_inter = min(box1[2], box2[2])
52
+ y2_inter = min(box1[3], box2[3])
53
+
54
+ if x2_inter < x1_inter or y2_inter < y1_inter:
55
+ return 0.0
56
+
57
+ intersection = (x2_inter - x1_inter) * (y2_inter - y1_inter)
58
+
59
+ area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
60
+ area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
61
+
62
+ union = area1 + area2 - intersection
63
+
64
+ return intersection / (union + 1e-6)
65
+
66
+
67
+ def polygon_area(polygon: np.ndarray) -> float:
68
+ """Calculate area of polygon using shoelace formula."""
69
+ x = polygon[:, 0]
70
+ y = polygon[:, 1]
71
+ return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
eye/utils_eye/nms.py ADDED
@@ -0,0 +1,55 @@
1
+ """Non-maximum suppression."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def non_max_suppression(
7
+ boxes: np.ndarray,
8
+ scores: np.ndarray,
9
+ iou_threshold: float = 0.5
10
+ ) -> np.ndarray:
11
+ """Apply NMS to remove overlapping boxes.
12
+
13
+ Args:
14
+ boxes: (N, 4) array of boxes [x1, y1, x2, y2]
15
+ scores: (N,) array of scores
16
+ iou_threshold: IoU threshold for suppression
17
+
18
+ Returns:
19
+ Indices of boxes to keep
20
+ """
21
+ if len(boxes) == 0:
22
+ return np.array([], dtype=int)
23
+
24
+ x1 = boxes[:, 0]
25
+ y1 = boxes[:, 1]
26
+ x2 = boxes[:, 2]
27
+ y2 = boxes[:, 3]
28
+
29
+ areas = (x2 - x1) * (y2 - y1)
30
+ order = scores.argsort()[::-1]
31
+
32
+ keep = []
33
+ while len(order) > 0:
34
+ i = order[0]
35
+ keep.append(i)
36
+
37
+ xx1 = np.maximum(x1[i], x1[order[1:]])
38
+ yy1 = np.maximum(y1[i], y1[order[1:]])
39
+ xx2 = np.minimum(x2[i], x2[order[1:]])
40
+ yy2 = np.minimum(y2[i], y2[order[1:]])
41
+
42
+ w = np.maximum(0, xx2 - xx1)
43
+ h = np.maximum(0, yy2 - yy1)
44
+
45
+ intersection = w * h
46
+ iou = intersection / (areas[i] + areas[order[1:]] - intersection + 1e-6)
47
+
48
+ inds = np.where(iou <= iou_threshold)[0]
49
+ order = order[inds + 1]
50
+
51
+ return np.array(keep, dtype=int)
52
+
53
+
54
+ # Alias for simpler name
55
+ nms = non_max_suppression
@@ -0,0 +1,140 @@
1
+ from typing import Any, Dict
2
+
3
+ import numpy as np
4
+
5
+
6
+ def validate_xyxy(xyxy: Any) -> None:
7
+ expected_shape = "(_, 4)"
8
+ actual_shape = str(getattr(xyxy, "shape", None))
9
+ is_valid = isinstance(xyxy, np.ndarray) and xyxy.ndim == 2 and xyxy.shape[1] == 4
10
+ if not is_valid:
11
+ raise ValueError(
12
+ f"xyxy must be a 2D np.ndarray with shape {expected_shape}, but got shape "
13
+ f"{actual_shape}"
14
+ )
15
+
16
+
17
+ def validate_mask(mask: Any, n: int) -> None:
18
+ expected_shape = f"({n}, H, W)"
19
+ actual_shape = str(getattr(mask, "shape", None))
20
+ is_valid = mask is None or (
21
+ isinstance(mask, np.ndarray) and len(mask.shape) == 3 and mask.shape[0] == n
22
+ )
23
+ if not is_valid:
24
+ raise ValueError(
25
+ f"mask must be a 3D np.ndarray with shape {expected_shape}, but got shape "
26
+ f"{actual_shape}"
27
+ )
28
+
29
+
30
+ def validate_class_id(class_id: Any, n: int) -> None:
31
+ expected_shape = f"({n},)"
32
+ actual_shape = str(getattr(class_id, "shape", None))
33
+ is_valid = class_id is None or (
34
+ isinstance(class_id, np.ndarray) and class_id.shape == (n,)
35
+ )
36
+ if not is_valid:
37
+ raise ValueError(
38
+ f"class_id must be a 1D np.ndarray with shape {expected_shape}, but got "
39
+ f"shape {actual_shape}"
40
+ )
41
+
42
+
43
+ def validate_confidence(confidence: Any, n: int) -> None:
44
+ expected_shape = f"({n},)"
45
+ actual_shape = str(getattr(confidence, "shape", None))
46
+ is_valid = confidence is None or (
47
+ isinstance(confidence, np.ndarray) and confidence.shape == (n,)
48
+ )
49
+ if not is_valid:
50
+ raise ValueError(
51
+ f"confidence must be a 1D np.ndarray with shape {expected_shape}, but got "
52
+ f"shape {actual_shape}"
53
+ )
54
+
55
+
56
+ def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
57
+ expected_shape = f"({n,m})"
58
+ actual_shape = str(getattr(confidence, "shape", None))
59
+
60
+ if confidence is not None:
61
+ is_valid = isinstance(confidence, np.ndarray) and confidence.shape == (n, m)
62
+ if not is_valid:
63
+ raise ValueError(
64
+ f"confidence must be a 1D np.ndarray with shape {expected_shape}, but "
65
+ f"got shape {actual_shape}"
66
+ )
67
+
68
+
69
+ def validate_tracker_id(tracker_id: Any, n: int) -> None:
70
+ expected_shape = f"({n},)"
71
+ actual_shape = str(getattr(tracker_id, "shape", None))
72
+ is_valid = tracker_id is None or (
73
+ isinstance(tracker_id, np.ndarray) and tracker_id.shape == (n,)
74
+ )
75
+ if not is_valid:
76
+ raise ValueError(
77
+ f"tracker_id must be a 1D np.ndarray with shape {expected_shape}, but got "
78
+ f"shape {actual_shape}"
79
+ )
80
+
81
+
82
+ def validate_data(data: Dict[str, Any], n: int) -> None:
83
+ for key, value in data.items():
84
+ if isinstance(value, list):
85
+ if len(value) != n:
86
+ raise ValueError(f"Length of list for key '{key}' must be {n}")
87
+ elif isinstance(value, np.ndarray):
88
+ if value.ndim == 1 and value.shape[0] != n:
89
+ raise ValueError(f"Shape of np.ndarray for key '{key}' must be ({n},)")
90
+ elif value.ndim > 1 and value.shape[0] != n:
91
+ raise ValueError(
92
+ f"First dimension of np.ndarray for key '{key}' must have size {n}"
93
+ )
94
+ else:
95
+ raise ValueError(f"Value for key '{key}' must be a list or np.ndarray")
96
+
97
+
98
+ def validate_xy(xy: Any, n: int, m: int) -> None:
99
+ expected_shape = f"({n, m},)"
100
+ actual_shape = str(getattr(xy, "shape", None))
101
+
102
+ is_valid = isinstance(xy, np.ndarray) and (
103
+ xy.shape == (n, m, 2) or xy.shape == (n, m, 3)
104
+ )
105
+ if not is_valid:
106
+ raise ValueError(
107
+ f"xy must be a 2D np.ndarray with shape {expected_shape}, but got shape "
108
+ f"{actual_shape}"
109
+ )
110
+
111
+
112
+ def validate_detections_fields(
113
+ xyxy: Any,
114
+ mask: Any,
115
+ class_id: Any,
116
+ confidence: Any,
117
+ tracker_id: Any,
118
+ data: Dict[str, Any],
119
+ ) -> None:
120
+ validate_xyxy(xyxy)
121
+ n = len(xyxy)
122
+ validate_mask(mask, n)
123
+ validate_class_id(class_id, n)
124
+ validate_confidence(confidence, n)
125
+ validate_tracker_id(tracker_id, n)
126
+ validate_data(data, n)
127
+
128
+
129
+ def validate_keypoints_fields(
130
+ xy: Any,
131
+ class_id: Any,
132
+ confidence: Any,
133
+ data: Dict[str, Any],
134
+ ) -> None:
135
+ n = len(xy)
136
+ m = len(xy[0]) if len(xy) > 0 else 0
137
+ validate_xy(xy, n, m)
138
+ validate_class_id(class_id, n)
139
+ validate_keypoint_confidence(confidence, n, m)
140
+ validate_data(data, n)
eye/web.py ADDED
@@ -0,0 +1,271 @@
1
+ """Web-ready API for Flask, FastAPI, Django, etc."""
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ import base64
5
+ import io
6
+ import numpy as np
7
+ import cv2
8
+ from dataclasses import dataclass, asdict
9
+ import json
10
+
11
+
12
+ @dataclass
13
+ class WebDetection:
14
+ """Web-friendly detection format (JSON serializable)."""
15
+ bbox: List[float] # [x1, y1, x2, y2]
16
+ confidence: float
17
+ class_id: int
18
+ class_name: str
19
+ tracker_id: Optional[int] = None
20
+ mask: Optional[str] = None # Base64 encoded
21
+
22
+
23
+ @dataclass
24
+ class WebResponse:
25
+ """Standard web API response."""
26
+ success: bool
27
+ detections: List[WebDetection]
28
+ frame_id: int
29
+ processing_time_ms: float
30
+ metadata: Dict[str, Any]
31
+ error: Optional[str] = None
32
+
33
+
34
+ class WebAPI:
35
+ """Web-ready interface for Eye library.
36
+
37
+ Features:
38
+ - JSON serializable responses
39
+ - Base64 image encoding/decoding
40
+ - CORS-ready
41
+ - Stateless operation
42
+ - Fast processing
43
+
44
+ Perfect for: Flask, FastAPI, Django, Node.js bridges, etc.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ model: Any,
50
+ tracker: Optional[Any] = None,
51
+ class_names: Optional[Dict[int, str]] = None
52
+ ):
53
+ """
54
+ Args:
55
+ model: Detection model (YOLO, etc.)
56
+ tracker: Optional tracker
57
+ class_names: Dict mapping class IDs to names
58
+ """
59
+ self.model = model
60
+ self.tracker = tracker
61
+ self.class_names = class_names or {}
62
+
63
+ def process_base64(
64
+ self,
65
+ image_b64: str,
66
+ conf_threshold: float = 0.5,
67
+ frame_id: int = 0
68
+ ) -> Dict[str, Any]:
69
+ """Process base64-encoded image.
70
+
71
+ Args:
72
+ image_b64: Base64 encoded image
73
+ conf_threshold: Confidence threshold
74
+ frame_id: Frame identifier
75
+
76
+ Returns:
77
+ JSON-serializable dict
78
+ """
79
+ import time
80
+ start = time.time()
81
+
82
+ try:
83
+ # Decode image
84
+ image = self.decode_base64_image(image_b64)
85
+
86
+ # Process
87
+ response = self.process_image(image, conf_threshold, frame_id)
88
+ response.processing_time_ms = (time.time() - start) * 1000
89
+
90
+ return asdict(response)
91
+
92
+ except Exception as e:
93
+ return asdict(WebResponse(
94
+ success=False,
95
+ detections=[],
96
+ frame_id=frame_id,
97
+ processing_time_ms=(time.time() - start) * 1000,
98
+ metadata={},
99
+ error=str(e)
100
+ ))
101
+
102
+ def process_image(
103
+ self,
104
+ image: np.ndarray,
105
+ conf_threshold: float = 0.5,
106
+ frame_id: int = 0
107
+ ) -> WebResponse:
108
+ """Process numpy image.
109
+
110
+ Args:
111
+ image: Input image (numpy array)
112
+ conf_threshold: Confidence threshold
113
+ frame_id: Frame identifier
114
+
115
+ Returns:
116
+ WebResponse
117
+ """
118
+ try:
119
+ from ..core.auto_detect import auto_convert
120
+ except ImportError:
121
+ from eye.core.auto_detect import auto_convert
122
+
123
+ # Detect
124
+ results = self.model(image, conf=conf_threshold, verbose=False)
125
+ detections = auto_convert(results)
126
+
127
+ # Track
128
+ if self.tracker:
129
+ detections = self.tracker.update(detections)
130
+
131
+ # Convert to web format
132
+ web_dets = []
133
+ for i in range(len(detections)):
134
+ web_det = WebDetection(
135
+ bbox=detections.xyxy[i].tolist(),
136
+ confidence=float(detections.confidence[i]) if detections.confidence is not None else 1.0,
137
+ class_id=int(detections.class_id[i]) if detections.class_id is not None else 0,
138
+ class_name=self.class_names.get(
139
+ int(detections.class_id[i]) if detections.class_id is not None else 0,
140
+ 'unknown'
141
+ ),
142
+ tracker_id=int(detections.tracker_id[i]) if detections.tracker_id is not None else None
143
+ )
144
+ web_dets.append(web_det)
145
+
146
+ return WebResponse(
147
+ success=True,
148
+ detections=web_dets,
149
+ frame_id=frame_id,
150
+ processing_time_ms=0.0, # Will be set by caller
151
+ metadata={
152
+ 'image_shape': list(image.shape),
153
+ 'num_detections': len(web_dets)
154
+ }
155
+ )
156
+
157
+ @staticmethod
158
+ def decode_base64_image(image_b64: str) -> np.ndarray:
159
+ """Decode base64 image to numpy array."""
160
+ # Remove data URL prefix if present
161
+ if ',' in image_b64:
162
+ image_b64 = image_b64.split(',')[1]
163
+
164
+ # Decode
165
+ img_bytes = base64.b64decode(image_b64)
166
+ img_array = np.frombuffer(img_bytes, dtype=np.uint8)
167
+ image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
168
+
169
+ return image
170
+
171
+ @staticmethod
172
+ def encode_image_base64(image: np.ndarray, format: str = 'jpg') -> str:
173
+ """Encode numpy image to base64."""
174
+ _, buffer = cv2.imencode(f'.{format}', image)
175
+ img_b64 = base64.b64encode(buffer).decode('utf-8')
176
+ return f"data:image/{format};base64,{img_b64}"
177
+
178
+
179
+ # Flask example
180
+ def create_flask_app(model, tracker=None, class_names=None):
181
+ """Create Flask app with Eye detection endpoint.
182
+
183
+ Example:
184
+ >>> from flask import Flask
185
+ >>> from ultralytics import YOLO
186
+ >>> import eye
187
+ >>>
188
+ >>> model = YOLO("yolo11n.pt")
189
+ >>> app = eye.web.create_flask_app(model, class_names=model.names)
190
+ >>> app.run(host='0.0.0.0', port=5000)
191
+ """
192
+ try:
193
+ from flask import Flask, request, jsonify
194
+ from flask_cors import CORS
195
+ except ImportError:
196
+ raise ImportError("Flask not installed. Run: pip install flask flask-cors")
197
+
198
+ app = Flask(__name__)
199
+ CORS(app) # Enable CORS
200
+
201
+ api = WebAPI(model, tracker, class_names)
202
+
203
+ @app.route('/detect', methods=['POST'])
204
+ def detect():
205
+ """Detection endpoint."""
206
+ data = request.json
207
+ image_b64 = data.get('image')
208
+ conf_threshold = data.get('confidence', 0.5)
209
+ frame_id = data.get('frame_id', 0)
210
+
211
+ result = api.process_base64(image_b64, conf_threshold, frame_id)
212
+ return jsonify(result)
213
+
214
+ @app.route('/health', methods=['GET'])
215
+ def health():
216
+ """Health check endpoint."""
217
+ return jsonify({'status': 'healthy'})
218
+
219
+ return app
220
+
221
+
222
+ # FastAPI example
223
+ def create_fastapi_app(model, tracker=None, class_names=None):
224
+ """Create FastAPI app with Eye detection endpoint.
225
+
226
+ Example:
227
+ >>> from fastapi import FastAPI
228
+ >>> from ultralytics import YOLO
229
+ >>> import eye
230
+ >>>
231
+ >>> model = YOLO("yolo11n.pt")
232
+ >>> app = eye.web.create_fastapi_app(model, class_names=model.names)
233
+ >>> # Run with: uvicorn main:app --host 0.0.0.0 --port 8000
234
+ """
235
+ try:
236
+ from fastapi import FastAPI
237
+ from fastapi.middleware.cors import CORSMiddleware
238
+ from pydantic import BaseModel
239
+ except ImportError:
240
+ raise ImportError("FastAPI not installed. Run: pip install fastapi uvicorn")
241
+
242
+ app = FastAPI(title="Eye Detection API")
243
+
244
+ # Enable CORS
245
+ app.add_middleware(
246
+ CORSMiddleware,
247
+ allow_origins=["*"],
248
+ allow_credentials=True,
249
+ allow_methods=["*"],
250
+ allow_headers=["*"],
251
+ )
252
+
253
+ api = WebAPI(model, tracker, class_names)
254
+
255
+ class DetectionRequest(BaseModel):
256
+ image: str
257
+ confidence: float = 0.5
258
+ frame_id: int = 0
259
+
260
+ @app.post("/detect")
261
+ async def detect(request: DetectionRequest):
262
+ """Detection endpoint."""
263
+ result = api.process_base64(request.image, request.confidence, request.frame_id)
264
+ return result
265
+
266
+ @app.get("/health")
267
+ async def health():
268
+ """Health check endpoint."""
269
+ return {"status": "healthy"}
270
+
271
+ return app