Spaces:
Sleeping
Sleeping
Navy
commited on
Commit
·
9541352
1
Parent(s):
a20a645
refactor: payload & response adjusment
Browse files- app.py +22 -0
- core/defect_detection.py +58 -28
- utils.py +45 -2
app.py
CHANGED
|
@@ -72,6 +72,28 @@ async def start_detection(data: Dict):
|
|
| 72 |
if not station_id or not parts or not webhook_url or not cameras:
|
| 73 |
return {"status": "error", "message": "Missing required fields"}
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
logger.info(f"[INFO] Get metadata parts")
|
| 76 |
model_path = model_by_id_metadata(parts['id'])
|
| 77 |
|
|
|
|
| 72 |
if not station_id or not parts or not webhook_url or not cameras:
|
| 73 |
return {"status": "error", "message": "Missing required fields"}
|
| 74 |
|
| 75 |
+
# -------------------------------
|
| 76 |
+
# VALIDATION BEFORE EXECUTION
|
| 77 |
+
# -------------------------------
|
| 78 |
+
required_parts_fields = [
|
| 79 |
+
"id",
|
| 80 |
+
"pin_api",
|
| 81 |
+
"name",
|
| 82 |
+
"sku"
|
| 83 |
+
]
|
| 84 |
+
validation_errors = validate_input(required_parts_fields, station_id, cameras, parts, webhook_url)
|
| 85 |
+
|
| 86 |
+
if validation_errors:
|
| 87 |
+
logger.error("[VALIDATION FAILED] Input data is invalid.")
|
| 88 |
+
for err in validation_errors:
|
| 89 |
+
logger.error(f" - {err}")
|
| 90 |
+
return {
|
| 91 |
+
"status": "error",
|
| 92 |
+
"station_id": station_id,
|
| 93 |
+
"camera_count": len(cameras),
|
| 94 |
+
"message": " | ".join(validation_errors)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
logger.info(f"[INFO] Get metadata parts")
|
| 98 |
model_path = model_by_id_metadata(parts['id'])
|
| 99 |
|
core/defect_detection.py
CHANGED
|
@@ -7,13 +7,13 @@ from utils import *
|
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
|
| 13 |
-
MAX_RUNTIME_SEC = float(os.getenv("MAX_RUNTIME_SEC"
|
| 14 |
-
FRAME_FAIL_SLEEP = float(os.getenv("FRAME_FAIL_SLEEP"
|
| 15 |
-
DEFAULT_FPS = float(os.getenv("DEFAULT_FPS"
|
| 16 |
-
WEBHOOK_TIMEOUT = float(os.getenv("WEBHOOK_TIMEOUT"
|
| 17 |
|
| 18 |
# ============================================================
|
| 19 |
# DEFECT DETECTION FROM VIDEO URL
|
|
@@ -34,6 +34,9 @@ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, mod
|
|
| 34 |
"station_id": station_id,
|
| 35 |
"camera_id": camera_id,
|
| 36 |
"status": "error",
|
|
|
|
|
|
|
|
|
|
| 37 |
"detections": [],
|
| 38 |
"message": f"Cannot open video URL: {video_url}"
|
| 39 |
}
|
|
@@ -96,10 +99,10 @@ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, mod
|
|
| 96 |
frame_base64 = base64.b64encode(buffer).decode("utf-8")
|
| 97 |
|
| 98 |
# Save annotated image
|
| 99 |
-
output_dir = "outputs/images"
|
| 100 |
# os.makedirs(output_dir, exist_ok=True)
|
| 101 |
-
filename = f"{station_id}_{camera_id}_NG_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
| 102 |
-
filepath = os.path.join(output_dir, filename)
|
| 103 |
# cv2.imwrite(filepath, frame)
|
| 104 |
# logger.info(f"[SAVED] NG image saved to {filepath}")
|
| 105 |
|
|
@@ -109,14 +112,16 @@ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, mod
|
|
| 109 |
return {
|
| 110 |
"station_id": station_id,
|
| 111 |
"camera_id": camera_id,
|
|
|
|
| 112 |
"status_defect": "NG",
|
| 113 |
"image_base64": frame_base64,
|
| 114 |
-
"image_path": filepath,
|
| 115 |
"detections": [{
|
| 116 |
"class": defect_name,
|
| 117 |
"confidence": conf,
|
| 118 |
"bbox": xyxy
|
| 119 |
-
}]
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
# --- no defect detected ---
|
|
@@ -127,12 +132,12 @@ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, mod
|
|
| 127 |
frame_base64 = base64.b64encode(buffer).decode("utf-8")
|
| 128 |
|
| 129 |
# Save OK image (no bbox)
|
| 130 |
-
output_dir = "outputs/images"
|
| 131 |
-
os.makedirs(output_dir, exist_ok=True)
|
| 132 |
-
filename = f"{station_id}_{camera_id}_OK_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
| 133 |
-
filepath = os.path.join(output_dir, filename)
|
| 134 |
-
cv2.imwrite(filepath, last_frame)
|
| 135 |
-
logger.info(f"[SAVED] OK image saved to {filepath}")
|
| 136 |
else:
|
| 137 |
frame_base64 = ""
|
| 138 |
filepath = None
|
|
@@ -140,10 +145,12 @@ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, mod
|
|
| 140 |
return {
|
| 141 |
"station_id": station_id,
|
| 142 |
"camera_id": camera_id,
|
|
|
|
| 143 |
"status_defect": "OK",
|
| 144 |
"image_base64": frame_base64,
|
| 145 |
-
"image_path": filepath,
|
| 146 |
-
"detections": []
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
|
|
@@ -158,25 +165,47 @@ async def _detect_camera_video(station_id: str, camera: Dict, stop_flag: Dict, m
|
|
| 158 |
async def run_detection_group(station_id: str, cameras: List[Dict], webhook_url: str, model=None, parts=str):
|
| 159 |
"""
|
| 160 |
Run detection for all cameras in parallel.
|
|
|
|
| 161 |
Send webhook with NG/OK status.
|
| 162 |
"""
|
|
|
|
| 163 |
stop_flag = {"stop": False}
|
| 164 |
logger.info(f"[START] Station {station_id} → {len(cameras)} camera(s)")
|
| 165 |
-
|
| 166 |
-
results = await asyncio.gather(
|
| 167 |
-
_detect_camera_video(station_id, cam, stop_flag, model)
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
payload = {
|
| 172 |
-
"status":
|
| 173 |
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%
|
| 174 |
"model_version": MODEL_VERSION,
|
| 175 |
-
"
|
|
|
|
| 176 |
"data": results,
|
| 177 |
}
|
| 178 |
|
| 179 |
-
# Send webhook
|
| 180 |
try:
|
| 181 |
async with httpx.AsyncClient(timeout=WEBHOOK_TIMEOUT) as client:
|
| 182 |
await client.post(webhook_url, json=payload)
|
|
@@ -184,4 +213,5 @@ async def run_detection_group(station_id: str, cameras: List[Dict], webhook_url:
|
|
| 184 |
except Exception as e:
|
| 185 |
logger.error(f"[ERROR] Webhook failed: {e}")
|
| 186 |
|
|
|
|
| 187 |
# return payload
|
|
|
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
| 10 |
+
MODEL_VERSION = os.getenv("MODEL_VERSION")
|
| 11 |
+
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
|
| 12 |
|
| 13 |
+
MAX_RUNTIME_SEC = float(os.getenv("MAX_RUNTIME_SEC"))
|
| 14 |
+
FRAME_FAIL_SLEEP = float(os.getenv("FRAME_FAIL_SLEEP"))
|
| 15 |
+
DEFAULT_FPS = float(os.getenv("DEFAULT_FPS"))
|
| 16 |
+
WEBHOOK_TIMEOUT = float(os.getenv("WEBHOOK_TIMEOUT"))
|
| 17 |
|
| 18 |
# ============================================================
|
| 19 |
# DEFECT DETECTION FROM VIDEO URL
|
|
|
|
| 34 |
"station_id": station_id,
|
| 35 |
"camera_id": camera_id,
|
| 36 |
"status": "error",
|
| 37 |
+
"status_defect": "",
|
| 38 |
+
"image_base64": "",
|
| 39 |
+
"image_path": "",
|
| 40 |
"detections": [],
|
| 41 |
"message": f"Cannot open video URL: {video_url}"
|
| 42 |
}
|
|
|
|
| 99 |
frame_base64 = base64.b64encode(buffer).decode("utf-8")
|
| 100 |
|
| 101 |
# Save annotated image
|
| 102 |
+
# output_dir = "outputs/images"
|
| 103 |
# os.makedirs(output_dir, exist_ok=True)
|
| 104 |
+
# filename = f"{station_id}_{camera_id}_NG_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
| 105 |
+
# filepath = os.path.join(output_dir, filename)
|
| 106 |
# cv2.imwrite(filepath, frame)
|
| 107 |
# logger.info(f"[SAVED] NG image saved to {filepath}")
|
| 108 |
|
|
|
|
| 112 |
return {
|
| 113 |
"station_id": station_id,
|
| 114 |
"camera_id": camera_id,
|
| 115 |
+
"status": "success",
|
| 116 |
"status_defect": "NG",
|
| 117 |
"image_base64": frame_base64,
|
| 118 |
+
# "image_path": filepath,
|
| 119 |
"detections": [{
|
| 120 |
"class": defect_name,
|
| 121 |
"confidence": conf,
|
| 122 |
"bbox": xyxy
|
| 123 |
+
}],
|
| 124 |
+
"message": f"Detected as defect"
|
| 125 |
}
|
| 126 |
|
| 127 |
# --- no defect detected ---
|
|
|
|
| 132 |
frame_base64 = base64.b64encode(buffer).decode("utf-8")
|
| 133 |
|
| 134 |
# Save OK image (no bbox)
|
| 135 |
+
# output_dir = "outputs/images"
|
| 136 |
+
# os.makedirs(output_dir, exist_ok=True)
|
| 137 |
+
# filename = f"{station_id}_{camera_id}_OK_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
| 138 |
+
# filepath = os.path.join(output_dir, filename)
|
| 139 |
+
# cv2.imwrite(filepath, last_frame)
|
| 140 |
+
# logger.info(f"[SAVED] OK image saved to {filepath}")
|
| 141 |
else:
|
| 142 |
frame_base64 = ""
|
| 143 |
filepath = None
|
|
|
|
| 145 |
return {
|
| 146 |
"station_id": station_id,
|
| 147 |
"camera_id": camera_id,
|
| 148 |
+
"status": "success",
|
| 149 |
"status_defect": "OK",
|
| 150 |
"image_base64": frame_base64,
|
| 151 |
+
# "image_path": filepath,
|
| 152 |
+
"detections": [],
|
| 153 |
+
"message": f"Detected as normal (no defect)"
|
| 154 |
}
|
| 155 |
|
| 156 |
|
|
|
|
| 165 |
async def run_detection_group(station_id: str, cameras: List[Dict], webhook_url: str, model=None, parts=str):
|
| 166 |
"""
|
| 167 |
Run detection for all cameras in parallel.
|
| 168 |
+
Validate input before detection.
|
| 169 |
Send webhook with NG/OK status.
|
| 170 |
"""
|
| 171 |
+
|
| 172 |
stop_flag = {"stop": False}
|
| 173 |
logger.info(f"[START] Station {station_id} → {len(cameras)} camera(s)")
|
| 174 |
+
|
| 175 |
+
results = await asyncio.gather(
|
| 176 |
+
*[_detect_camera_video(station_id, cam, stop_flag, model) for cam in cameras],
|
| 177 |
+
return_exceptions=True
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# misalnya results sudah berisi hasil dari tiap kamera
|
| 181 |
+
has_error = any(
|
| 182 |
+
isinstance(r, Exception) or (isinstance(r, dict) and r.get("status") == "error")
|
| 183 |
+
for r in results
|
| 184 |
+
)
|
| 185 |
+
all_error = all(
|
| 186 |
+
isinstance(r, Exception) or (isinstance(r, dict) and r.get("status") == "error")
|
| 187 |
+
for r in results
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
if all_error:
|
| 191 |
+
status = "error"
|
| 192 |
+
message = "All cameras failed during detection"
|
| 193 |
+
elif has_error:
|
| 194 |
+
status = "partial_error"
|
| 195 |
+
message = "Some cameras failed during detection"
|
| 196 |
+
else:
|
| 197 |
+
status = "success"
|
| 198 |
+
message = "Success detecting defects"
|
| 199 |
|
| 200 |
payload = {
|
| 201 |
+
"status": status,
|
| 202 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
|
| 203 |
"model_version": MODEL_VERSION,
|
| 204 |
+
"message": message,
|
| 205 |
+
"parts": parts,
|
| 206 |
"data": results,
|
| 207 |
}
|
| 208 |
|
|
|
|
| 209 |
try:
|
| 210 |
async with httpx.AsyncClient(timeout=WEBHOOK_TIMEOUT) as client:
|
| 211 |
await client.post(webhook_url, json=payload)
|
|
|
|
| 213 |
except Exception as e:
|
| 214 |
logger.error(f"[ERROR] Webhook failed: {e}")
|
| 215 |
|
| 216 |
+
return "DONE"
|
| 217 |
# return payload
|
utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
import logging, sys, asyncio, random, json
|
| 2 |
from pathlib import Path
|
|
|
|
| 3 |
|
| 4 |
# ============================================================
|
| 5 |
# LOGGER SETUP
|
|
@@ -22,6 +23,37 @@ async def async_sleep_random(min_s=0.2, max_s=0.8):
|
|
| 22 |
"""
|
| 23 |
durasi = random.uniform(min_s, max_s)
|
| 24 |
await asyncio.sleep(durasi)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
# ============================================================
|
| 27 |
# HELPER
|
|
@@ -83,4 +115,15 @@ def color_defect(defect_name):
|
|
| 83 |
r = int(hex_color[0:2], 16)
|
| 84 |
g = int(hex_color[2:4], 16)
|
| 85 |
b = int(hex_color[4:6], 16)
|
| 86 |
-
return (b, g, r) # Convert to BGR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging, sys, asyncio, random, json, requests, os
|
| 2 |
from pathlib import Path
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
|
| 5 |
# ============================================================
|
| 6 |
# LOGGER SETUP
|
|
|
|
| 23 |
"""
|
| 24 |
durasi = random.uniform(min_s, max_s)
|
| 25 |
await asyncio.sleep(durasi)
|
| 26 |
+
|
| 27 |
+
# ==============================================================
|
| 28 |
+
# VALIDATION FUNCTION
|
| 29 |
+
# ==============================================================
|
| 30 |
+
def validate_input(required_parts_fields, station_id, cameras, parts, webhook_url):
|
| 31 |
+
errors = []
|
| 32 |
+
|
| 33 |
+
# Validate station_id
|
| 34 |
+
if not station_id or not str(station_id).strip():
|
| 35 |
+
errors.append("station_id is missing or empty")
|
| 36 |
+
|
| 37 |
+
# Validate parts
|
| 38 |
+
for field in required_parts_fields:
|
| 39 |
+
if field not in parts or not str(parts[field]).strip():
|
| 40 |
+
errors.append(f"parts.{field} is missing or empty")
|
| 41 |
+
|
| 42 |
+
# Validate cameras list
|
| 43 |
+
if not isinstance(cameras, list) or len(cameras) == 0:
|
| 44 |
+
errors.append("cameras must be a non-empty list")
|
| 45 |
+
else:
|
| 46 |
+
for index, cam in enumerate(cameras):
|
| 47 |
+
if "camera_id" not in cam or not str(cam["camera_id"]).strip():
|
| 48 |
+
errors.append(f"camera[{index}].camera_id missing or empty")
|
| 49 |
+
if "rtsp_url" not in cam or not str(cam["rtsp_url"]).strip():
|
| 50 |
+
errors.append(f"camera[{index}].rtsp_url missing or empty")
|
| 51 |
+
|
| 52 |
+
# Validate webhook
|
| 53 |
+
if not webhook_url or not webhook_url.startswith("http"):
|
| 54 |
+
errors.append("webhook_url is invalid or missing")
|
| 55 |
+
|
| 56 |
+
return errors
|
| 57 |
|
| 58 |
# ============================================================
|
| 59 |
# HELPER
|
|
|
|
| 115 |
r = int(hex_color[0:2], 16)
|
| 116 |
g = int(hex_color[2:4], 16)
|
| 117 |
b = int(hex_color[4:6], 16)
|
| 118 |
+
return (b, g, r) # Convert to BGR
|
| 119 |
+
|
| 120 |
+
def load_json(path):
|
| 121 |
+
with open(path, "r") as f:
|
| 122 |
+
return json.load(f)
|
| 123 |
+
|
| 124 |
+
def start_detection(base_url, payload):
|
| 125 |
+
return requests.post(f"{base_url}/start-detection", json=payload)
|
| 126 |
+
|
| 127 |
+
def load_config():
|
| 128 |
+
load_dotenv()
|
| 129 |
+
return os.getenv("BASE_URL"), os.getenv("WEBHOOK")
|