Spaces:
Paused
Paused
Deploy Sensorflow Studio from fix/legacy-help-chatbot (f7ddaf4)
Browse filesPush coherent hf-space/ snapshot including pipeline browse, UX toasts, execution integrity, and help chatbot. Removes nested hf-space/ leftover from prior monorepo upload. Does not include unmerged #6 license or #10 number-format (sibling branches).
This view is limited to 50 files because it contains too many changes. See raw diff
- README.md +6 -0
- app_backend.py +922 -142
- docs/execution-integrity-audit.md +99 -0
- hf-space/.dockerignore +0 -12
- hf-space/.gitattributes +0 -35
- hf-space/.gitignore +0 -72
- hf-space/ANTIGRAVITY_PROMPT.md +0 -192
- hf-space/ARCHITECTURE.md +0 -78
- hf-space/CRASH_DATA_GUIDE.md +0 -378
- hf-space/DASHBOARD_README.md +0 -393
- hf-space/DATA_EXTRACTION_QUICK_REF.md +0 -348
- hf-space/Dockerfile +0 -51
- hf-space/FILES_CREATED.txt +0 -135
- hf-space/LICENSE +0 -17
- hf-space/README.md +0 -31
- hf-space/TOOLKIT_SUMMARY.md +0 -440
- hf-space/accident_analysis.py +0 -99
- hf-space/accident_cleaner.py +0 -62
- hf-space/accident_importer.py +0 -103
- hf-space/accident_insights.py +0 -54
- hf-space/accident_report.py +0 -81
- hf-space/accident_validator.py +0 -83
- hf-space/app_backend.py +0 -1481
- hf-space/autograder.py +0 -128
- hf-space/coco8.yaml +0 -21
- hf-space/comparative_analytics.py +0 -209
- hf-space/dashboard_viz.py +0 -468
- hf-space/data/test.jpg +0 -0
- hf-space/deploy/CHECKLIST.md +0 -140
- hf-space/deploy/DEPLOYMENT.md +0 -201
- hf-space/deploy/accident-analysis.service +0 -17
- hf-space/deploy/download-models.sh +0 -36
- hf-space/deploy/huggingface/README.md +0 -31
- hf-space/deploy/huggingface/deploy.py +0 -133
- hf-space/deploy/huggingface/requirements-space.txt +0 -15
- hf-space/deploy/nginx.conf +0 -69
- hf-space/deploy/sensorflow-studio.service +0 -17
- hf-space/deploy/setup-dgx.sh +0 -87
- hf-space/index.html +0 -10
- hf-space/infer.py +0 -111
- hf-space/main.py +0 -386
- hf-space/package.json +0 -33
- hf-space/requirements-3d.txt +0 -13
- hf-space/requirements-prod.txt +0 -18
- hf-space/requirements.txt +0 -59
- hf-space/sensorflow/__init__.py +0 -26
- hf-space/sensorflow/adapters/__init__.py +0 -4
- hf-space/sensorflow/adapters/alpamayo_adapter.py +0 -133
- hf-space/sensorflow/adapters/base.py +0 -14
- hf-space/sensorflow/bevfusion/__init__.py +0 -53
README.md
CHANGED
|
@@ -29,3 +29,9 @@ copilot — are not functional here; the dashboards and analytics APIs are.
|
|
| 29 |
Deployed from the project repository via
|
| 30 |
`deploy/huggingface/deploy.py` (uploads files with `huggingface_hub`; the
|
| 31 |
frontend is built inside the Docker image).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
Deployed from the project repository via
|
| 30 |
`deploy/huggingface/deploy.py` (uploads files with `huggingface_hub`; the
|
| 31 |
frontend is built inside the Docker image).
|
| 32 |
+
|
| 33 |
+
## Version
|
| 34 |
+
|
| 35 |
+
Current version is shown in the Studio AppBar (`vX.Y.Z` chip) and Help → About,
|
| 36 |
+
with release notes for each notable version. The same catalog is served at
|
| 37 |
+
`GET /api/about` and `GET /api/version`. See `CHANGELOG.md` in the GitHub repo.
|
app_backend.py
CHANGED
|
@@ -5,21 +5,24 @@ import json
|
|
| 5 |
import subprocess
|
| 6 |
import signal
|
| 7 |
from pathlib import Path
|
| 8 |
-
from typing import List, Optional
|
| 9 |
from pydantic import BaseModel
|
| 10 |
import httpx
|
| 11 |
-
from fastapi import FastAPI, HTTPException
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
-
from fastapi.responses import FileResponse
|
| 14 |
|
| 15 |
app = FastAPI(title="Sensorflow Studio Backend")
|
| 16 |
|
| 17 |
# State tracking
|
| 18 |
ACTIVE_TRAIN_PROC: Optional[subprocess.Popen] = None
|
|
|
|
| 19 |
TRAINING_LOGS_ACCUMULATED = ""
|
| 20 |
TRAINING_LOSSES: List[float] = []
|
| 21 |
TOTAL_EPOCHS = 10
|
| 22 |
CURRENT_EPOCH = 0
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# Configuration
|
| 25 |
CONFIG_PATH = Path("runs/studio_config.json")
|
|
@@ -32,7 +35,7 @@ class StudioConfig(BaseModel):
|
|
| 32 |
model_type: str = "yolov8"
|
| 33 |
pipeline_mode: str = "3d"
|
| 34 |
sam_checkpoint: str = "models/sam_vit_b.pth"
|
| 35 |
-
vendors: List[str] = ["alpamayo", "waymo"]
|
| 36 |
gate_thresholds_path: str = "runs/pipeline/gate_thresholds.json"
|
| 37 |
sequence_id: str = "seq_001"
|
| 38 |
|
|
@@ -105,32 +108,236 @@ def save_yaml_content(req: SaveYamlRequest):
|
|
| 105 |
|
| 106 |
@app.get("/api/precheck")
|
| 107 |
def precheck():
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
if not (train_exists and infer_exists and grader_exists):
|
| 114 |
-
return {
|
| 115 |
-
"status": "warning",
|
| 116 |
-
"message": "Some scripts are missing in the current folder. Please verify train.py, infer.py, and autograder.py exist."
|
| 117 |
-
}
|
| 118 |
return {
|
| 119 |
-
"status": "
|
| 120 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
@app.post("/api/train/start")
|
| 124 |
def start_train(params: TrainParams):
|
| 125 |
-
global ACTIVE_TRAIN_PROC,
|
|
|
|
|
|
|
|
|
|
| 126 |
if ACTIVE_TRAIN_PROC and ACTIVE_TRAIN_PROC.poll() is None:
|
| 127 |
raise HTTPException(status_code=400, detail="Training is already running.")
|
| 128 |
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
TRAINING_LOGS_ACCUMULATED = f"Starting YOLO training with base model {params.model}...\n"
|
| 131 |
TRAINING_LOSSES = []
|
| 132 |
TOTAL_EPOCHS = params.epochs
|
| 133 |
CURRENT_EPOCH = 0
|
|
|
|
| 134 |
|
| 135 |
cmd = [
|
| 136 |
sys.executable, "train.py",
|
|
@@ -138,8 +345,17 @@ def start_train(params: TrainParams):
|
|
| 138 |
"--batch", str(params.batch),
|
| 139 |
"--device", params.device,
|
| 140 |
"--model", params.model,
|
| 141 |
-
"--data", params.data
|
| 142 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
try:
|
| 145 |
ACTIVE_TRAIN_PROC = subprocess.Popen(
|
|
@@ -148,47 +364,80 @@ def start_train(params: TrainParams):
|
|
| 148 |
stderr=subprocess.STDOUT,
|
| 149 |
text=True,
|
| 150 |
bufsize=1,
|
| 151 |
-
preexec_fn=os.setsid if os.name !=
|
| 152 |
)
|
| 153 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
raise HTTPException(status_code=500, detail=f"Failed to spawn training process: {str(e)}")
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
@app.post("/api/train/stop")
|
| 159 |
def stop_train():
|
| 160 |
-
global ACTIVE_TRAIN_PROC
|
|
|
|
|
|
|
| 161 |
if not ACTIVE_TRAIN_PROC or ACTIVE_TRAIN_PROC.poll() is not None:
|
| 162 |
-
return {"status": "not_running"}
|
| 163 |
|
|
|
|
| 164 |
try:
|
| 165 |
-
if os.name !=
|
| 166 |
os.killpg(os.getpgid(ACTIVE_TRAIN_PROC.pid), signal.SIGTERM)
|
| 167 |
else:
|
| 168 |
ACTIVE_TRAIN_PROC.terminate()
|
| 169 |
ACTIVE_TRAIN_PROC.wait(timeout=3)
|
| 170 |
-
except Exception
|
| 171 |
-
# Fallback to simple kill
|
| 172 |
try:
|
| 173 |
ACTIVE_TRAIN_PROC.kill()
|
| 174 |
except Exception:
|
| 175 |
pass
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
@app.get("/api/train/status")
|
| 180 |
def get_train_status():
|
| 181 |
-
global ACTIVE_TRAIN_PROC,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
running = False
|
| 183 |
-
|
|
|
|
|
|
|
| 184 |
if ACTIVE_TRAIN_PROC:
|
| 185 |
-
# Read new output line by line (non-blocking)
|
| 186 |
-
# Use select or set standard output stream non-blocking if needed, or read from stdout.
|
| 187 |
-
# To avoid blocking, we read line-by-line if there is data.
|
| 188 |
import select
|
|
|
|
| 189 |
if ACTIVE_TRAIN_PROC.poll() is None:
|
| 190 |
running = True
|
| 191 |
-
# Read available outputs
|
| 192 |
while True:
|
| 193 |
r, _, _ = select.select([ACTIVE_TRAIN_PROC.stdout], [], [], 0.02)
|
| 194 |
if ACTIVE_TRAIN_PROC.stdout in r:
|
|
@@ -196,23 +445,22 @@ def get_train_status():
|
|
| 196 |
if not line:
|
| 197 |
break
|
| 198 |
TRAINING_LOGS_ACCUMULATED += line
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
if "epoch" in line.lower() or "/" in line:
|
| 203 |
try:
|
| 204 |
-
# Try to extract numbers
|
| 205 |
parts = line.split()
|
| 206 |
if len(parts) >= 3 and "/" in parts[0]:
|
| 207 |
ep_part = parts[0].split("/")
|
| 208 |
epoch_num = int(ep_part[0])
|
| 209 |
CURRENT_EPOCH = epoch_num
|
| 210 |
-
# Try to grab a float that might be loss
|
| 211 |
for p in parts[2:]:
|
| 212 |
try:
|
| 213 |
val = float(p)
|
| 214 |
if 0.0 < val < 10.0:
|
| 215 |
-
# Found a candidate loss value
|
| 216 |
if len(TRAINING_LOSSES) < epoch_num:
|
| 217 |
TRAINING_LOSSES.append(val)
|
| 218 |
else:
|
|
@@ -225,54 +473,237 @@ def get_train_status():
|
|
| 225 |
else:
|
| 226 |
break
|
| 227 |
else:
|
| 228 |
-
# Done running, read all remaining output
|
| 229 |
remaining = ACTIVE_TRAIN_PROC.stdout.read()
|
| 230 |
if remaining:
|
| 231 |
TRAINING_LOGS_ACCUMULATED += remaining
|
|
|
|
|
|
|
|
|
|
| 232 |
ACTIVE_TRAIN_PROC = None
|
| 233 |
|
| 234 |
-
# Calculate mock losses if empty to display curves in frontend
|
| 235 |
-
if not running and not TRAINING_LOSSES and TRAINING_LOGS_ACCUMULATED:
|
| 236 |
-
# Finished but no parsed losses, generate dummy decreasing loss
|
| 237 |
-
TRAINING_LOSSES = [2.5 - (2.0 / TOTAL_EPOCHS) * i for i in range(TOTAL_EPOCHS)]
|
| 238 |
-
|
| 239 |
progress = 0.0
|
| 240 |
-
if TOTAL_EPOCHS > 0:
|
| 241 |
-
progress = float(CURRENT_EPOCH) / float(TOTAL_EPOCHS)
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
return {
|
| 246 |
"running": running,
|
| 247 |
"progress": progress,
|
| 248 |
-
"logs": TRAINING_LOGS_ACCUMULATED[-20000:],
|
| 249 |
-
"losses": TRAINING_LOSSES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
|
|
|
|
| 252 |
@app.post("/api/infer/run")
|
| 253 |
def run_infer(params: InferParams):
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
|
|
|
|
|
|
| 264 |
try:
|
| 265 |
-
res = subprocess.run(cmd, capture_output=True, text=True
|
| 266 |
-
except
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
-
# Gather list of output annotated images
|
| 270 |
-
out_dir = Path("runs/infer")
|
| 271 |
images = []
|
| 272 |
if out_dir.exists():
|
| 273 |
images = [f.name for f in out_dir.glob("annotated_*") if f.is_file()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
@app.get("/api/images/{filename}")
|
| 278 |
def serve_image(filename: str):
|
|
@@ -283,76 +714,146 @@ def serve_image(filename: str):
|
|
| 283 |
|
| 284 |
@app.get("/api/grade")
|
| 285 |
def grade_predictions():
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
predictions_file = Path("runs/infer/predictions.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
if not predictions_file.exists():
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
return {
|
|
|
|
|
|
|
| 291 |
"total_predictions": 0,
|
| 292 |
"total_images": 0,
|
| 293 |
-
"quality_score":
|
|
|
|
| 294 |
"issues": [
|
| 295 |
{
|
| 296 |
"severity": "WARNING",
|
| 297 |
"type": "Missing predictions",
|
| 298 |
-
"description": "
|
| 299 |
-
"recommendation": "
|
| 300 |
}
|
| 301 |
-
]
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
"--predictions", str(predictions_file)
|
| 307 |
-
]
|
| 308 |
-
|
| 309 |
try:
|
| 310 |
-
subprocess.run(cmd, capture_output=True, text=True
|
| 311 |
-
except
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
-
# Read generated quality report
|
| 315 |
report_file = Path("runs/infer/quality_report.json")
|
| 316 |
-
if report_file.exists():
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
"
|
| 350 |
-
"
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
@app.post("/api/export")
|
| 358 |
def export_weights(params: ExportParams):
|
|
@@ -823,23 +1324,62 @@ DATASET_METADATA_STORE = {
|
|
| 823 |
|
| 824 |
@app.get("/api/dataset/details")
|
| 825 |
def get_dataset_details(type: str = "local"):
|
| 826 |
-
metadata = DATASET_METADATA_STORE.get(type, DATASET_METADATA_STORE["local"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
return {"status": "ok", "metadata": metadata}
|
| 828 |
|
| 829 |
@app.post("/api/dataset/preprocess")
|
| 830 |
def preprocess_dataset(params: dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
dataset_type = params.get("dataset_type", "local")
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
meta
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
return {
|
| 838 |
-
"status": "
|
| 839 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
"dataset_type": dataset_type,
|
| 841 |
-
"total_frames":
|
| 842 |
-
"
|
|
|
|
|
|
|
| 843 |
}
|
| 844 |
|
| 845 |
@app.post("/api/dataset/save-pipeline-tools")
|
|
@@ -1273,8 +1813,10 @@ def annotate_street(req: AnnotateStreetRequest):
|
|
| 1273 |
# -----------------------------------------------------------------------
|
| 1274 |
|
| 1275 |
class IngestParams(BaseModel):
|
| 1276 |
-
vendors: List[str] = ["alpamayo", "waymo"]
|
| 1277 |
sequence_id: str = "seq_001"
|
|
|
|
|
|
|
| 1278 |
|
| 1279 |
class AutoLabelParams(BaseModel):
|
| 1280 |
sequence_id: str = "seq_001"
|
|
@@ -1295,20 +1837,45 @@ class GateParams(BaseModel):
|
|
| 1295 |
@app.post("/api/dataset/ingest")
|
| 1296 |
def ingest_dataset(params: IngestParams):
|
| 1297 |
from sensorflow.dataset_fusion_engine import DatasetFusionEngine
|
|
|
|
|
|
|
| 1298 |
try:
|
| 1299 |
engine = DatasetFusionEngine()
|
| 1300 |
-
sequence = engine.ingest(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
manifest_path = engine.save_manifest(sequence)
|
|
|
|
|
|
|
| 1302 |
except Exception as e:
|
| 1303 |
raise HTTPException(status_code=500, detail=f"Ingest failed: {str(e)}")
|
| 1304 |
|
| 1305 |
-
config = load_config()
|
| 1306 |
config.sequence_id = params.sequence_id
|
| 1307 |
config.vendors = params.vendors
|
|
|
|
|
|
|
| 1308 |
with open(CONFIG_PATH, "w") as f:
|
| 1309 |
json.dump(config.model_dump(), f, indent=2)
|
| 1310 |
|
| 1311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1312 |
|
| 1313 |
|
| 1314 |
@app.get("/api/dataset/ingest/status")
|
|
@@ -1323,6 +1890,9 @@ def ingest_status(sequence_id: str = "seq_001"):
|
|
| 1323 |
status["manifest_exists"] = True
|
| 1324 |
status["frames"] = len(manifest_data.get("frames", []))
|
| 1325 |
status["vendor"] = manifest_data.get("vendor", "unknown")
|
|
|
|
|
|
|
|
|
|
| 1326 |
return {"status": "ok", "sequence_id": sequence_id, **status}
|
| 1327 |
|
| 1328 |
|
|
@@ -1330,13 +1900,43 @@ def ingest_status(sequence_id: str = "seq_001"):
|
|
| 1330 |
def auto_label(params: AutoLabelParams):
|
| 1331 |
from sensorflow.perception_automator import PerceptionAutomator
|
| 1332 |
from sensorflow.schemas.unified_frame import UnifiedSequence
|
|
|
|
|
|
|
| 1333 |
|
| 1334 |
manifest = Path("runs/pipeline") / params.sequence_id / "manifest.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1335 |
if not manifest.exists():
|
| 1336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
|
|
|
|
| 1338 |
try:
|
| 1339 |
sequence = UnifiedSequence.load(manifest)
|
|
|
|
| 1340 |
output_dir = manifest.parent / "proposals"
|
| 1341 |
automator = PerceptionAutomator(
|
| 1342 |
sam_checkpoint=params.sam_checkpoint,
|
|
@@ -1344,12 +1944,93 @@ def auto_label(params: AutoLabelParams):
|
|
| 1344 |
use_sam=not params.no_sam,
|
| 1345 |
)
|
| 1346 |
automator.run_sequence(sequence, output_dir)
|
|
|
|
| 1347 |
except Exception as e:
|
| 1348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
|
| 1350 |
proposals_dir = manifest.parent / "proposals"
|
| 1351 |
-
|
| 1352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1353 |
|
| 1354 |
|
| 1355 |
@app.post("/api/perception/track")
|
|
@@ -1440,18 +2121,109 @@ def pipeline_status(sequence_id: str = "seq_001"):
|
|
| 1440 |
|
| 1441 |
seq_state = state.get(sequence_id, {})
|
| 1442 |
base = Path("runs/pipeline") / sequence_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1443 |
return {
|
| 1444 |
"status": "ok",
|
| 1445 |
"sequence_id": sequence_id,
|
| 1446 |
-
"ingest_complete":
|
| 1447 |
-
"perception_complete":
|
| 1448 |
-
"tracking_complete":
|
| 1449 |
-
"benchmark_complete":
|
| 1450 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1451 |
"state": seq_state,
|
| 1452 |
}
|
| 1453 |
|
| 1454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1455 |
@app.get("/api/mitl/queue")
|
| 1456 |
def get_mitl_queue(sequence_id: str = "seq_001"):
|
| 1457 |
from sensorflow.mitl_copilot import MitlCopilot
|
|
@@ -1534,6 +2306,14 @@ app.include_router(studio2_router)
|
|
| 1534 |
from sensorflow.rotr.api import router as rotr_router
|
| 1535 |
app.include_router(rotr_router)
|
| 1536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1537 |
# Mount static folder
|
| 1538 |
static_dir = Path(__file__).parent / "static"
|
| 1539 |
if static_dir.exists():
|
|
|
|
| 5 |
import subprocess
|
| 6 |
import signal
|
| 7 |
from pathlib import Path
|
| 8 |
+
from typing import List, Optional, Any, Dict
|
| 9 |
from pydantic import BaseModel
|
| 10 |
import httpx
|
| 11 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
+
from fastapi.responses import FileResponse, PlainTextResponse
|
| 14 |
|
| 15 |
app = FastAPI(title="Sensorflow Studio Backend")
|
| 16 |
|
| 17 |
# State tracking
|
| 18 |
ACTIVE_TRAIN_PROC: Optional[subprocess.Popen] = None
|
| 19 |
+
ACTIVE_TRAIN_EXECUTION_ID: Optional[str] = None
|
| 20 |
TRAINING_LOGS_ACCUMULATED = ""
|
| 21 |
TRAINING_LOSSES: List[float] = []
|
| 22 |
TOTAL_EPOCHS = 10
|
| 23 |
CURRENT_EPOCH = 0
|
| 24 |
+
LAST_TRAIN_EXIT_CODE: Optional[int] = None
|
| 25 |
+
LAST_TRAIN_COMMAND: Optional[List[str]] = None
|
| 26 |
|
| 27 |
# Configuration
|
| 28 |
CONFIG_PATH = Path("runs/studio_config.json")
|
|
|
|
| 35 |
model_type: str = "yolov8"
|
| 36 |
pipeline_mode: str = "3d"
|
| 37 |
sam_checkpoint: str = "models/sam_vit_b.pth"
|
| 38 |
+
vendors: List[str] = ["alpamayo", "waymo", "a2d2"]
|
| 39 |
gate_thresholds_path: str = "runs/pipeline/gate_thresholds.json"
|
| 40 |
sequence_id: str = "seq_001"
|
| 41 |
|
|
|
|
| 108 |
|
| 109 |
@app.get("/api/precheck")
|
| 110 |
def precheck():
|
| 111 |
+
"""Script verification — existence alone is NOT success."""
|
| 112 |
+
from sensorflow.execution_ops import verify_script
|
| 113 |
+
|
| 114 |
+
reports = {
|
| 115 |
+
"train.py": verify_script("train.py"),
|
| 116 |
+
"infer.py": verify_script("infer.py"),
|
| 117 |
+
"autograder.py": verify_script("autograder.py"),
|
| 118 |
+
}
|
| 119 |
+
all_ok = all(
|
| 120 |
+
r.get("exists") and r.get("syntax_valid") and r.get("dry_run_ok")
|
| 121 |
+
for r in reports.values()
|
| 122 |
+
)
|
| 123 |
+
any_missing = any(not r.get("exists") for r in reports.values())
|
| 124 |
+
return {
|
| 125 |
+
"status": "ok" if all_ok else ("warning" if not any_missing else "failed"),
|
| 126 |
+
"message": (
|
| 127 |
+
"Scripts exist, syntax-valid, and --help dry-run succeeded."
|
| 128 |
+
if all_ok
|
| 129 |
+
else "Script verification incomplete — see reports (file Found alone is not enough)."
|
| 130 |
+
),
|
| 131 |
+
"scripts": reports,
|
| 132 |
+
"verified": all_ok,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.get("/api/health")
|
| 137 |
+
def health():
|
| 138 |
+
from sensorflow.execution_ops import backend_health
|
| 139 |
+
from sensorflow.execution_ledger import get_strict_mode
|
| 140 |
+
|
| 141 |
+
payload = backend_health()
|
| 142 |
+
payload["strict_mode"] = get_strict_mode()
|
| 143 |
+
payload["backend_connected"] = True
|
| 144 |
+
return payload
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@app.get("/api/strict-mode")
|
| 148 |
+
def get_strict_mode_api():
|
| 149 |
+
from sensorflow.execution_ledger import get_strict_mode
|
| 150 |
+
|
| 151 |
+
return {"enabled": get_strict_mode()}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@app.post("/api/strict-mode")
|
| 155 |
+
def set_strict_mode_api(params: dict):
|
| 156 |
+
from sensorflow.execution_ledger import set_strict_mode
|
| 157 |
+
|
| 158 |
+
enabled = bool(params.get("enabled", False))
|
| 159 |
+
return set_strict_mode(enabled)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.get("/api/executions")
|
| 163 |
+
def list_executions_api(
|
| 164 |
+
operation: Optional[str] = None,
|
| 165 |
+
status: Optional[str] = None,
|
| 166 |
+
limit: int = Query(50, ge=1, le=500),
|
| 167 |
+
):
|
| 168 |
+
from sensorflow.execution_ledger import list_executions
|
| 169 |
+
|
| 170 |
+
return {"status": "ok", "executions": list_executions(operation=operation, status=status, limit=limit)}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@app.get("/api/executions/{execution_id}")
|
| 174 |
+
def get_execution_api(execution_id: str):
|
| 175 |
+
from sensorflow.execution_ledger import load_execution
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
return load_execution(execution_id)
|
| 179 |
+
except FileNotFoundError:
|
| 180 |
+
raise HTTPException(status_code=404, detail=f"Execution not found: {execution_id}")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@app.get("/api/executions/{execution_id}/log")
|
| 184 |
+
def get_execution_log_api(execution_id: str):
|
| 185 |
+
from sensorflow.execution_ledger import get_log_text, load_execution
|
| 186 |
+
|
| 187 |
+
try:
|
| 188 |
+
load_execution(execution_id)
|
| 189 |
+
except FileNotFoundError:
|
| 190 |
+
raise HTTPException(status_code=404, detail=f"Execution not found: {execution_id}")
|
| 191 |
+
return PlainTextResponse(get_log_text(execution_id) or "(no log lines)")
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.post("/api/yaml/validate")
|
| 195 |
+
def validate_yaml_api(params: dict):
|
| 196 |
+
from sensorflow.execution_ops import validate_yaml_semantics
|
| 197 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize
|
| 198 |
+
|
| 199 |
+
path = params.get("path") or params.get("yaml_path") or "coco8.yaml"
|
| 200 |
+
record = create_execution(
|
| 201 |
+
"yaml_validate",
|
| 202 |
+
configuration_snapshot={"yaml_path": path},
|
| 203 |
+
input_artifacts=[path],
|
| 204 |
+
)
|
| 205 |
+
mark_running(record["execution_id"])
|
| 206 |
+
report = validate_yaml_semantics(path)
|
| 207 |
+
final = finalize(
|
| 208 |
+
record["execution_id"],
|
| 209 |
+
report.get("status") or "FAILED",
|
| 210 |
+
metrics={
|
| 211 |
+
"class_count": report.get("class_count"),
|
| 212 |
+
"image_counts": report.get("image_counts"),
|
| 213 |
+
"label_counts": report.get("label_counts"),
|
| 214 |
+
},
|
| 215 |
+
errors=report.get("errors"),
|
| 216 |
+
warnings=report.get("warnings"),
|
| 217 |
+
output_artifacts=[{"path": path, "hash": report.get("content_hash")}],
|
| 218 |
+
process_invoked=False,
|
| 219 |
+
outputs_valid=report.get("status") == "SUCCEEDED",
|
| 220 |
+
)
|
| 221 |
+
report["execution_id"] = final["execution_id"]
|
| 222 |
+
report["execution"] = {
|
| 223 |
+
"execution_id": final["execution_id"],
|
| 224 |
+
"status": final["status"],
|
| 225 |
+
"verified": final.get("verified"),
|
| 226 |
+
"duration_ms": final.get("duration_ms"),
|
| 227 |
+
}
|
| 228 |
+
return report
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
@app.get("/api/scripts/verify")
|
| 232 |
+
def scripts_verify_api():
|
| 233 |
+
from sensorflow.execution_ops import verify_script
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
return {
|
| 236 |
+
"status": "ok",
|
| 237 |
+
"scripts": {
|
| 238 |
+
"train.py": verify_script("train.py"),
|
| 239 |
+
"infer.py": verify_script("infer.py"),
|
| 240 |
+
"autograder.py": verify_script("autograder.py"),
|
| 241 |
+
},
|
| 242 |
}
|
| 243 |
|
| 244 |
+
|
| 245 |
+
@app.post("/api/dataset/load")
|
| 246 |
+
def dataset_load(params: dict):
|
| 247 |
+
"""Real Load & Preprocess with discovery evidence (not catalog KPIs)."""
|
| 248 |
+
from sensorflow.execution_ops import load_and_preprocess
|
| 249 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize
|
| 250 |
+
|
| 251 |
+
source_path = params.get("source_path") or load_config().source_path
|
| 252 |
+
yaml_path = params.get("yaml_path") or load_config().yaml_path
|
| 253 |
+
dataset_type = params.get("dataset_type") or load_config().dataset_type
|
| 254 |
+
|
| 255 |
+
record = create_execution(
|
| 256 |
+
"dataset_load",
|
| 257 |
+
configuration_snapshot={
|
| 258 |
+
"source_path": source_path,
|
| 259 |
+
"yaml_path": yaml_path,
|
| 260 |
+
"dataset_type": dataset_type,
|
| 261 |
+
},
|
| 262 |
+
input_artifacts=[source_path, yaml_path],
|
| 263 |
+
)
|
| 264 |
+
mark_running(record["execution_id"])
|
| 265 |
+
result = load_and_preprocess(source_path, yaml_path=yaml_path, dataset_type=dataset_type)
|
| 266 |
+
disc = result["discovery"]
|
| 267 |
+
final = finalize(
|
| 268 |
+
record["execution_id"],
|
| 269 |
+
result["status"],
|
| 270 |
+
records_discovered=disc.get("images_discovered", 0),
|
| 271 |
+
records_processed=disc.get("images_discovered", 0),
|
| 272 |
+
records_succeeded=disc.get("images_readable", 0),
|
| 273 |
+
records_failed=disc.get("images_corrupt", 0),
|
| 274 |
+
metrics=result["metrics"],
|
| 275 |
+
errors=disc.get("errors"),
|
| 276 |
+
warnings=disc.get("warnings"),
|
| 277 |
+
output_artifacts=[{"path": result["manifest_path"], "kind": "load_manifest"}],
|
| 278 |
+
process_invoked=False,
|
| 279 |
+
outputs_valid=disc.get("images_readable", 0) > 0 and result["status"] in (
|
| 280 |
+
"SUCCEEDED",
|
| 281 |
+
"PARTIAL_SUCCESS",
|
| 282 |
+
),
|
| 283 |
+
)
|
| 284 |
+
return {
|
| 285 |
+
"status": final["status"],
|
| 286 |
+
"execution_id": final["execution_id"],
|
| 287 |
+
"verified": final.get("verified"),
|
| 288 |
+
"duration_ms": final.get("duration_ms"),
|
| 289 |
+
"message": (
|
| 290 |
+
f"Loaded {disc.get('images_readable', 0)}/{disc.get('images_discovered', 0)} images "
|
| 291 |
+
f"from {source_path}"
|
| 292 |
+
if disc.get("images_readable")
|
| 293 |
+
else f"FAILED: 0 images loaded from {source_path}"
|
| 294 |
+
),
|
| 295 |
+
"discovery": disc,
|
| 296 |
+
"metrics": result["metrics"],
|
| 297 |
+
"yaml_validation": result.get("yaml_validation"),
|
| 298 |
+
"reconciliation": result.get("reconciliation"),
|
| 299 |
+
"manifest_path": result.get("manifest_path"),
|
| 300 |
+
"catalog_only": False,
|
| 301 |
+
"browsable": bool(result["metrics"].get("browsable")),
|
| 302 |
+
"events": final.get("events"),
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
@app.post("/api/train/start")
|
| 307 |
def start_train(params: TrainParams):
|
| 308 |
+
global ACTIVE_TRAIN_PROC, ACTIVE_TRAIN_EXECUTION_ID, TRAINING_LOGS_ACCUMULATED
|
| 309 |
+
global TRAINING_LOSSES, TOTAL_EPOCHS, CURRENT_EPOCH, LAST_TRAIN_EXIT_CODE, LAST_TRAIN_COMMAND
|
| 310 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize
|
| 311 |
+
|
| 312 |
if ACTIVE_TRAIN_PROC and ACTIVE_TRAIN_PROC.poll() is None:
|
| 313 |
raise HTTPException(status_code=400, detail="Training is already running.")
|
| 314 |
|
| 315 |
+
weights_path = Path(params.model)
|
| 316 |
+
# Allow ultralytics named weights (e.g. yolov8n.pt) that may download; still record path
|
| 317 |
+
yaml_path = Path(params.data)
|
| 318 |
+
if not yaml_path.exists():
|
| 319 |
+
record = create_execution(
|
| 320 |
+
"training",
|
| 321 |
+
configuration_snapshot=params.dict(),
|
| 322 |
+
input_artifacts=[params.data, params.model],
|
| 323 |
+
)
|
| 324 |
+
finalize(
|
| 325 |
+
record["execution_id"],
|
| 326 |
+
"FAILED",
|
| 327 |
+
errors=[f"Dataset YAML not found: {params.data}"],
|
| 328 |
+
process_invoked=False,
|
| 329 |
+
outputs_valid=False,
|
| 330 |
+
)
|
| 331 |
+
raise HTTPException(
|
| 332 |
+
status_code=400,
|
| 333 |
+
detail={"message": f"Dataset YAML not found: {params.data}", "execution_id": record["execution_id"]},
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
TRAINING_LOGS_ACCUMULATED = f"Starting YOLO training with base model {params.model}...\n"
|
| 337 |
TRAINING_LOSSES = []
|
| 338 |
TOTAL_EPOCHS = params.epochs
|
| 339 |
CURRENT_EPOCH = 0
|
| 340 |
+
LAST_TRAIN_EXIT_CODE = None
|
| 341 |
|
| 342 |
cmd = [
|
| 343 |
sys.executable, "train.py",
|
|
|
|
| 345 |
"--batch", str(params.batch),
|
| 346 |
"--device", params.device,
|
| 347 |
"--model", params.model,
|
| 348 |
+
"--data", params.data,
|
| 349 |
]
|
| 350 |
+
LAST_TRAIN_COMMAND = cmd
|
| 351 |
+
|
| 352 |
+
record = create_execution(
|
| 353 |
+
"training",
|
| 354 |
+
configuration_snapshot=params.dict(),
|
| 355 |
+
input_artifacts=[params.data, params.model],
|
| 356 |
+
command=cmd,
|
| 357 |
+
)
|
| 358 |
+
ACTIVE_TRAIN_EXECUTION_ID = record["execution_id"]
|
| 359 |
|
| 360 |
try:
|
| 361 |
ACTIVE_TRAIN_PROC = subprocess.Popen(
|
|
|
|
| 364 |
stderr=subprocess.STDOUT,
|
| 365 |
text=True,
|
| 366 |
bufsize=1,
|
| 367 |
+
preexec_fn=os.setsid if os.name != "nt" else None,
|
| 368 |
)
|
| 369 |
except Exception as e:
|
| 370 |
+
finalize(
|
| 371 |
+
record["execution_id"],
|
| 372 |
+
"FAILED",
|
| 373 |
+
errors=[f"Failed to spawn training process: {e}"],
|
| 374 |
+
process_invoked=False,
|
| 375 |
+
outputs_valid=False,
|
| 376 |
+
)
|
| 377 |
+
ACTIVE_TRAIN_EXECUTION_ID = None
|
| 378 |
raise HTTPException(status_code=500, detail=f"Failed to spawn training process: {str(e)}")
|
| 379 |
|
| 380 |
+
mark_running(record["execution_id"], process_id=ACTIVE_TRAIN_PROC.pid)
|
| 381 |
+
return {
|
| 382 |
+
"status": "started",
|
| 383 |
+
"execution_id": record["execution_id"],
|
| 384 |
+
"process_id": ACTIVE_TRAIN_PROC.pid,
|
| 385 |
+
"command": cmd,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
|
| 389 |
@app.post("/api/train/stop")
|
| 390 |
def stop_train():
|
| 391 |
+
global ACTIVE_TRAIN_PROC, ACTIVE_TRAIN_EXECUTION_ID, LAST_TRAIN_EXIT_CODE
|
| 392 |
+
from sensorflow.execution_ledger import finalize, append_log
|
| 393 |
+
|
| 394 |
if not ACTIVE_TRAIN_PROC or ACTIVE_TRAIN_PROC.poll() is not None:
|
| 395 |
+
return {"status": "not_running", "execution_id": ACTIVE_TRAIN_EXECUTION_ID}
|
| 396 |
|
| 397 |
+
exec_id = ACTIVE_TRAIN_EXECUTION_ID
|
| 398 |
try:
|
| 399 |
+
if os.name != "nt":
|
| 400 |
os.killpg(os.getpgid(ACTIVE_TRAIN_PROC.pid), signal.SIGTERM)
|
| 401 |
else:
|
| 402 |
ACTIVE_TRAIN_PROC.terminate()
|
| 403 |
ACTIVE_TRAIN_PROC.wait(timeout=3)
|
| 404 |
+
except Exception:
|
|
|
|
| 405 |
try:
|
| 406 |
ACTIVE_TRAIN_PROC.kill()
|
| 407 |
except Exception:
|
| 408 |
pass
|
| 409 |
|
| 410 |
+
LAST_TRAIN_EXIT_CODE = ACTIVE_TRAIN_PROC.poll()
|
| 411 |
+
if exec_id:
|
| 412 |
+
append_log(exec_id, "Training terminated by user")
|
| 413 |
+
finalize(
|
| 414 |
+
exec_id,
|
| 415 |
+
"CANCELLED",
|
| 416 |
+
exit_code=LAST_TRAIN_EXIT_CODE,
|
| 417 |
+
process_invoked=True,
|
| 418 |
+
outputs_valid=False,
|
| 419 |
+
errors=["Cancelled by user"],
|
| 420 |
+
)
|
| 421 |
+
ACTIVE_TRAIN_PROC = None
|
| 422 |
+
return {"status": "stopped", "execution_id": exec_id, "exit_code": LAST_TRAIN_EXIT_CODE}
|
| 423 |
+
|
| 424 |
|
| 425 |
@app.get("/api/train/status")
|
| 426 |
def get_train_status():
|
| 427 |
+
global ACTIVE_TRAIN_PROC, ACTIVE_TRAIN_EXECUTION_ID, TRAINING_LOGS_ACCUMULATED
|
| 428 |
+
global TRAINING_LOSSES, CURRENT_EPOCH, TOTAL_EPOCHS, LAST_TRAIN_EXIT_CODE, LAST_TRAIN_COMMAND
|
| 429 |
+
from sensorflow.execution_ledger import append_log, finalize, sha256_file
|
| 430 |
+
from sensorflow.execution_ops import artifact_info
|
| 431 |
+
|
| 432 |
running = False
|
| 433 |
+
just_finished = False
|
| 434 |
+
exit_code = LAST_TRAIN_EXIT_CODE
|
| 435 |
+
|
| 436 |
if ACTIVE_TRAIN_PROC:
|
|
|
|
|
|
|
|
|
|
| 437 |
import select
|
| 438 |
+
|
| 439 |
if ACTIVE_TRAIN_PROC.poll() is None:
|
| 440 |
running = True
|
|
|
|
| 441 |
while True:
|
| 442 |
r, _, _ = select.select([ACTIVE_TRAIN_PROC.stdout], [], [], 0.02)
|
| 443 |
if ACTIVE_TRAIN_PROC.stdout in r:
|
|
|
|
| 445 |
if not line:
|
| 446 |
break
|
| 447 |
TRAINING_LOGS_ACCUMULATED += line
|
| 448 |
+
if ACTIVE_TRAIN_EXECUTION_ID:
|
| 449 |
+
try:
|
| 450 |
+
append_log(ACTIVE_TRAIN_EXECUTION_ID, line.rstrip())
|
| 451 |
+
except Exception:
|
| 452 |
+
pass
|
| 453 |
if "epoch" in line.lower() or "/" in line:
|
| 454 |
try:
|
|
|
|
| 455 |
parts = line.split()
|
| 456 |
if len(parts) >= 3 and "/" in parts[0]:
|
| 457 |
ep_part = parts[0].split("/")
|
| 458 |
epoch_num = int(ep_part[0])
|
| 459 |
CURRENT_EPOCH = epoch_num
|
|
|
|
| 460 |
for p in parts[2:]:
|
| 461 |
try:
|
| 462 |
val = float(p)
|
| 463 |
if 0.0 < val < 10.0:
|
|
|
|
| 464 |
if len(TRAINING_LOSSES) < epoch_num:
|
| 465 |
TRAINING_LOSSES.append(val)
|
| 466 |
else:
|
|
|
|
| 473 |
else:
|
| 474 |
break
|
| 475 |
else:
|
|
|
|
| 476 |
remaining = ACTIVE_TRAIN_PROC.stdout.read()
|
| 477 |
if remaining:
|
| 478 |
TRAINING_LOGS_ACCUMULATED += remaining
|
| 479 |
+
exit_code = ACTIVE_TRAIN_PROC.poll()
|
| 480 |
+
LAST_TRAIN_EXIT_CODE = exit_code
|
| 481 |
+
just_finished = True
|
| 482 |
ACTIVE_TRAIN_PROC = None
|
| 483 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
progress = 0.0
|
| 485 |
+
if TOTAL_EPOCHS > 0 and CURRENT_EPOCH > 0:
|
| 486 |
+
progress = min(1.0, float(CURRENT_EPOCH) / float(TOTAL_EPOCHS))
|
| 487 |
+
elif not running and exit_code == 0 and CURRENT_EPOCH > 0:
|
| 488 |
+
progress = 1.0
|
| 489 |
+
# Never invent progress=1.0 on failure or empty parse
|
| 490 |
+
|
| 491 |
+
checkpoint = Path("runs/detect/coco_finetuned/weights/best.pt")
|
| 492 |
+
ckpt_info = artifact_info(checkpoint) if checkpoint.exists() else {"path": str(checkpoint), "exists": False}
|
| 493 |
+
|
| 494 |
+
if just_finished and ACTIVE_TRAIN_EXECUTION_ID:
|
| 495 |
+
status = "SUCCEEDED" if exit_code == 0 else "FAILED"
|
| 496 |
+
if exit_code is None:
|
| 497 |
+
status = "FAILED"
|
| 498 |
+
finalize(
|
| 499 |
+
ACTIVE_TRAIN_EXECUTION_ID,
|
| 500 |
+
status,
|
| 501 |
+
exit_code=exit_code,
|
| 502 |
+
metrics={
|
| 503 |
+
"epochs_requested": TOTAL_EPOCHS,
|
| 504 |
+
"epochs_observed": CURRENT_EPOCH,
|
| 505 |
+
"losses_parsed": len(TRAINING_LOSSES),
|
| 506 |
+
"losses": TRAINING_LOSSES,
|
| 507 |
+
"checkpoint": ckpt_info,
|
| 508 |
+
},
|
| 509 |
+
output_artifacts=[ckpt_info] if ckpt_info.get("exists") else [],
|
| 510 |
+
process_invoked=True,
|
| 511 |
+
outputs_valid=bool(ckpt_info.get("exists")) and exit_code == 0,
|
| 512 |
+
errors=[] if exit_code == 0 else [f"Training exit_code={exit_code}"],
|
| 513 |
+
)
|
| 514 |
|
| 515 |
return {
|
| 516 |
"running": running,
|
| 517 |
"progress": progress,
|
| 518 |
+
"logs": TRAINING_LOGS_ACCUMULATED[-20000:],
|
| 519 |
+
"losses": TRAINING_LOSSES, # only real parsed losses — never fabricated
|
| 520 |
+
"losses_fabricated": False,
|
| 521 |
+
"exit_code": exit_code,
|
| 522 |
+
"process_id": None if not running else (ACTIVE_TRAIN_PROC.pid if ACTIVE_TRAIN_PROC else None),
|
| 523 |
+
"command": LAST_TRAIN_COMMAND,
|
| 524 |
+
"execution_id": ACTIVE_TRAIN_EXECUTION_ID,
|
| 525 |
+
"epochs_observed": CURRENT_EPOCH,
|
| 526 |
+
"epochs_requested": TOTAL_EPOCHS,
|
| 527 |
+
"checkpoint": ckpt_info,
|
| 528 |
+
"status": (
|
| 529 |
+
"RUNNING" if running else (
|
| 530 |
+
"SUCCEEDED" if exit_code == 0 else (
|
| 531 |
+
"FAILED" if exit_code not in (None, 0) else "NOT_EXECUTED"
|
| 532 |
+
)
|
| 533 |
+
)
|
| 534 |
+
),
|
| 535 |
}
|
| 536 |
|
| 537 |
+
|
| 538 |
@app.post("/api/infer/run")
|
| 539 |
def run_infer(params: InferParams):
|
| 540 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize, append_log
|
| 541 |
+
from sensorflow.execution_ops import discover_images, artifact_info
|
| 542 |
+
|
| 543 |
+
weights = Path(params.weights)
|
| 544 |
+
source = Path(params.source)
|
| 545 |
+
out_dir = Path("runs/infer")
|
| 546 |
+
|
| 547 |
+
record = create_execution(
|
| 548 |
+
"inference",
|
| 549 |
+
configuration_snapshot=params.dict(),
|
| 550 |
+
input_artifacts=[params.source, params.weights],
|
| 551 |
+
command=[
|
| 552 |
+
sys.executable, "infer.py",
|
| 553 |
+
"--source", params.source,
|
| 554 |
+
"--weights", params.weights,
|
| 555 |
+
"--conf", str(params.conf),
|
| 556 |
+
"--iou", str(params.iou),
|
| 557 |
+
"--output", "runs/infer",
|
| 558 |
+
],
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
if not weights.exists() and not str(params.weights).endswith(".pt"):
|
| 562 |
+
finalize(
|
| 563 |
+
record["execution_id"],
|
| 564 |
+
"FAILED",
|
| 565 |
+
errors=[f"Model checkpoint not found: {params.weights}"],
|
| 566 |
+
process_invoked=False,
|
| 567 |
+
outputs_valid=False,
|
| 568 |
+
)
|
| 569 |
+
raise HTTPException(
|
| 570 |
+
status_code=400,
|
| 571 |
+
detail={"message": f"Model checkpoint not found: {params.weights}", "execution_id": record["execution_id"]},
|
| 572 |
+
)
|
| 573 |
+
if not weights.exists():
|
| 574 |
+
# Named ultralytics weight may auto-download; still warn if missing before run
|
| 575 |
+
pass
|
| 576 |
+
|
| 577 |
+
discovery = discover_images(params.source)
|
| 578 |
+
if discovery["images_discovered"] == 0:
|
| 579 |
+
finalize(
|
| 580 |
+
record["execution_id"],
|
| 581 |
+
"FAILED",
|
| 582 |
+
records_discovered=0,
|
| 583 |
+
records_processed=0,
|
| 584 |
+
records_succeeded=0,
|
| 585 |
+
records_failed=0,
|
| 586 |
+
errors=[f"No images found at source: {params.source}"],
|
| 587 |
+
metrics={"images_discovered": 0},
|
| 588 |
+
process_invoked=False,
|
| 589 |
+
outputs_valid=False,
|
| 590 |
+
)
|
| 591 |
+
return {
|
| 592 |
+
"status": "FAILED",
|
| 593 |
+
"execution_id": record["execution_id"],
|
| 594 |
+
"images": [],
|
| 595 |
+
"records_discovered": 0,
|
| 596 |
+
"records_succeeded": 0,
|
| 597 |
+
"records_failed": 0,
|
| 598 |
+
"message": f"No images found at source: {params.source}",
|
| 599 |
+
"model": params.weights,
|
| 600 |
+
"checkpoint": artifact_info(weights),
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
if not weights.exists():
|
| 604 |
+
finalize(
|
| 605 |
+
record["execution_id"],
|
| 606 |
+
"FAILED",
|
| 607 |
+
errors=[f"Missing model checkpoint: {params.weights}"],
|
| 608 |
+
records_discovered=discovery["images_discovered"],
|
| 609 |
+
process_invoked=False,
|
| 610 |
+
outputs_valid=False,
|
| 611 |
+
)
|
| 612 |
+
raise HTTPException(
|
| 613 |
+
status_code=400,
|
| 614 |
+
detail={
|
| 615 |
+
"message": f"Missing model checkpoint: {params.weights}",
|
| 616 |
+
"execution_id": record["execution_id"],
|
| 617 |
+
},
|
| 618 |
+
)
|
| 619 |
|
| 620 |
+
cmd = record["command"]
|
| 621 |
+
mark_running(record["execution_id"])
|
| 622 |
try:
|
| 623 |
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
| 624 |
+
except Exception as e:
|
| 625 |
+
finalize(
|
| 626 |
+
record["execution_id"],
|
| 627 |
+
"FAILED",
|
| 628 |
+
errors=[str(e)],
|
| 629 |
+
process_invoked=True,
|
| 630 |
+
outputs_valid=False,
|
| 631 |
+
)
|
| 632 |
+
raise HTTPException(status_code=500, detail=f"Inference failed to start: {e}")
|
| 633 |
+
|
| 634 |
+
if res.stdout:
|
| 635 |
+
append_log(record["execution_id"], res.stdout[-4000:])
|
| 636 |
+
if res.stderr:
|
| 637 |
+
append_log(record["execution_id"], res.stderr[-2000:])
|
| 638 |
|
|
|
|
|
|
|
| 639 |
images = []
|
| 640 |
if out_dir.exists():
|
| 641 |
images = [f.name for f in out_dir.glob("annotated_*") if f.is_file()]
|
| 642 |
+
preds_path = out_dir / "predictions.json"
|
| 643 |
+
pred_count = 0
|
| 644 |
+
if preds_path.exists():
|
| 645 |
+
try:
|
| 646 |
+
pred_count = len(json.loads(preds_path.read_text()))
|
| 647 |
+
except Exception:
|
| 648 |
+
pred_count = 0
|
| 649 |
+
|
| 650 |
+
succeeded = len(images)
|
| 651 |
+
failed = max(0, discovery["images_discovered"] - succeeded)
|
| 652 |
+
if res.returncode != 0:
|
| 653 |
+
status = "FAILED"
|
| 654 |
+
elif succeeded == 0:
|
| 655 |
+
status = "FAILED"
|
| 656 |
+
elif failed > 0:
|
| 657 |
+
status = "PARTIAL_SUCCESS"
|
| 658 |
+
else:
|
| 659 |
+
status = "SUCCEEDED"
|
| 660 |
+
|
| 661 |
+
final = finalize(
|
| 662 |
+
record["execution_id"],
|
| 663 |
+
status,
|
| 664 |
+
exit_code=res.returncode,
|
| 665 |
+
records_discovered=discovery["images_discovered"],
|
| 666 |
+
records_processed=discovery["images_discovered"],
|
| 667 |
+
records_succeeded=succeeded,
|
| 668 |
+
records_failed=failed,
|
| 669 |
+
metrics={
|
| 670 |
+
"model": params.weights,
|
| 671 |
+
"checkpoint": artifact_info(weights),
|
| 672 |
+
"inference_calls": discovery["images_discovered"],
|
| 673 |
+
"predictions_generated": pred_count,
|
| 674 |
+
"annotated_images": succeeded,
|
| 675 |
+
"conf": params.conf,
|
| 676 |
+
"iou": params.iou,
|
| 677 |
+
"output_dir": str(out_dir),
|
| 678 |
+
},
|
| 679 |
+
output_artifacts=[
|
| 680 |
+
{"path": str(out_dir), "kind": "infer_dir"},
|
| 681 |
+
artifact_info(preds_path),
|
| 682 |
+
],
|
| 683 |
+
errors=[] if res.returncode == 0 else [res.stderr or res.stdout or "infer non-zero exit"],
|
| 684 |
+
process_invoked=True,
|
| 685 |
+
outputs_valid=succeeded > 0 and res.returncode == 0,
|
| 686 |
+
)
|
| 687 |
|
| 688 |
+
return {
|
| 689 |
+
"status": final["status"],
|
| 690 |
+
"execution_id": final["execution_id"],
|
| 691 |
+
"verified": final.get("verified"),
|
| 692 |
+
"duration_ms": final.get("duration_ms"),
|
| 693 |
+
"images": sorted(images),
|
| 694 |
+
"model": params.weights,
|
| 695 |
+
"checkpoint": artifact_info(weights),
|
| 696 |
+
"records_discovered": discovery["images_discovered"],
|
| 697 |
+
"records_processed": discovery["images_discovered"],
|
| 698 |
+
"records_succeeded": succeeded,
|
| 699 |
+
"records_failed": failed,
|
| 700 |
+
"inference_calls": discovery["images_discovered"],
|
| 701 |
+
"predictions_generated": pred_count,
|
| 702 |
+
"output_dir": str(out_dir),
|
| 703 |
+
"exit_code": res.returncode,
|
| 704 |
+
"process_id": None,
|
| 705 |
+
"command": cmd,
|
| 706 |
+
}
|
| 707 |
|
| 708 |
@app.get("/api/images/{filename}")
|
| 709 |
def serve_image(filename: str):
|
|
|
|
| 714 |
|
| 715 |
@app.get("/api/grade")
|
| 716 |
def grade_predictions():
|
| 717 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize
|
| 718 |
+
from sensorflow.execution_ops import artifact_info
|
| 719 |
+
|
| 720 |
predictions_file = Path("runs/infer/predictions.json")
|
| 721 |
+
record = create_execution(
|
| 722 |
+
"auto_grader",
|
| 723 |
+
configuration_snapshot={"predictions": str(predictions_file)},
|
| 724 |
+
input_artifacts=[str(predictions_file)],
|
| 725 |
+
command=[sys.executable, "autograder.py", "--predictions", str(predictions_file)],
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
if not predictions_file.exists():
|
| 729 |
+
final = finalize(
|
| 730 |
+
record["execution_id"],
|
| 731 |
+
"NOT_EXECUTED",
|
| 732 |
+
errors=["predictions.json missing — run Auto-Labeler Inference first"],
|
| 733 |
+
process_invoked=False,
|
| 734 |
+
outputs_valid=False,
|
| 735 |
+
warnings=["Grader not executed: no predictions artifact"],
|
| 736 |
+
)
|
| 737 |
return {
|
| 738 |
+
"status": "NOT_EXECUTED",
|
| 739 |
+
"execution_id": final["execution_id"],
|
| 740 |
"total_predictions": 0,
|
| 741 |
"total_images": 0,
|
| 742 |
+
"quality_score": None,
|
| 743 |
+
"metrics": {},
|
| 744 |
"issues": [
|
| 745 |
{
|
| 746 |
"severity": "WARNING",
|
| 747 |
"type": "Missing predictions",
|
| 748 |
+
"description": "No predictions.json — grader was NOT executed.",
|
| 749 |
+
"recommendation": "Run Auto-Labeler Inference first.",
|
| 750 |
}
|
| 751 |
+
],
|
| 752 |
+
"message": "NOT_EXECUTED: missing predictions artifact",
|
| 753 |
}
|
| 754 |
|
| 755 |
+
mark_running(record["execution_id"])
|
| 756 |
+
cmd = record["command"]
|
|
|
|
|
|
|
|
|
|
| 757 |
try:
|
| 758 |
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
| 759 |
+
except Exception as e:
|
| 760 |
+
finalize(
|
| 761 |
+
record["execution_id"],
|
| 762 |
+
"FAILED",
|
| 763 |
+
errors=[str(e)],
|
| 764 |
+
process_invoked=True,
|
| 765 |
+
outputs_valid=False,
|
| 766 |
+
)
|
| 767 |
+
raise HTTPException(status_code=500, detail=f"Autograder failed to start: {e}")
|
| 768 |
|
|
|
|
| 769 |
report_file = Path("runs/infer/quality_report.json")
|
| 770 |
+
if res.returncode != 0 or not report_file.exists():
|
| 771 |
+
finalize(
|
| 772 |
+
record["execution_id"],
|
| 773 |
+
"FAILED",
|
| 774 |
+
exit_code=res.returncode,
|
| 775 |
+
errors=[res.stderr or res.stdout or "grader produced no report"],
|
| 776 |
+
process_invoked=True,
|
| 777 |
+
outputs_valid=False,
|
| 778 |
+
)
|
| 779 |
+
raise HTTPException(
|
| 780 |
+
status_code=500,
|
| 781 |
+
detail={
|
| 782 |
+
"message": f"Autograder failed: {res.stderr}",
|
| 783 |
+
"execution_id": record["execution_id"],
|
| 784 |
+
},
|
| 785 |
+
)
|
| 786 |
+
|
| 787 |
+
with open(report_file, "r") as f:
|
| 788 |
+
report_data = json.load(f)
|
| 789 |
+
|
| 790 |
+
issues = []
|
| 791 |
+
for issue_type, count in report_data.get("issue_summary", {}).items():
|
| 792 |
+
if count > 0:
|
| 793 |
+
severity = "WARNING" if issue_type in ["low_confidence", "overlapping_different_class"] else "INFO"
|
| 794 |
+
desc = f"Found {count} occurrences of {issue_type.replace('_', ' ')}."
|
| 795 |
+
rec = "Retrain model with more samples or tune confidence threshold."
|
| 796 |
+
if issue_type == "small_detection":
|
| 797 |
+
rec = "Verify that extremely small bounding boxes are correct."
|
| 798 |
+
elif issue_type == "class_imbalance":
|
| 799 |
+
rec = "Balance class frequency in dataset to prevent bias."
|
| 800 |
+
issues.append({
|
| 801 |
+
"severity": severity,
|
| 802 |
+
"type": issue_type.upper().replace("_", " "),
|
| 803 |
+
"description": desc,
|
| 804 |
+
"recommendation": rec,
|
| 805 |
+
})
|
| 806 |
+
|
| 807 |
+
for rec in report_data.get("recommendations", []):
|
| 808 |
+
issues.append({
|
| 809 |
+
"severity": "INFO",
|
| 810 |
+
"type": "RECOMMENDATION",
|
| 811 |
+
"description": rec,
|
| 812 |
+
"recommendation": "Review annotation guidelines.",
|
| 813 |
+
})
|
| 814 |
+
|
| 815 |
+
metrics = {
|
| 816 |
+
"total_predictions": report_data.get("total_predictions", 0),
|
| 817 |
+
"total_images": report_data.get("total_images", 0),
|
| 818 |
+
"quality_score": report_data.get("quality_score"),
|
| 819 |
+
"issue_summary": report_data.get("issue_summary", {}),
|
| 820 |
+
"precision": report_data.get("precision"),
|
| 821 |
+
"recall": report_data.get("recall"),
|
| 822 |
+
"f1": report_data.get("f1"),
|
| 823 |
+
"mAP": report_data.get("mAP"),
|
| 824 |
+
"tp": report_data.get("tp"),
|
| 825 |
+
"fp": report_data.get("fp"),
|
| 826 |
+
"fn": report_data.get("fn"),
|
| 827 |
+
"per_class": report_data.get("per_class"),
|
| 828 |
+
}
|
| 829 |
+
# Strip Nones so UI only shows grader-provided metrics
|
| 830 |
+
metrics = {k: v for k, v in metrics.items() if v is not None}
|
| 831 |
+
|
| 832 |
+
final = finalize(
|
| 833 |
+
record["execution_id"],
|
| 834 |
+
"SUCCEEDED",
|
| 835 |
+
exit_code=res.returncode,
|
| 836 |
+
records_discovered=metrics.get("total_images", 0),
|
| 837 |
+
records_processed=metrics.get("total_images", 0),
|
| 838 |
+
records_succeeded=metrics.get("total_images", 0),
|
| 839 |
+
metrics=metrics,
|
| 840 |
+
output_artifacts=[artifact_info(report_file), artifact_info(predictions_file)],
|
| 841 |
+
process_invoked=True,
|
| 842 |
+
outputs_valid=True,
|
| 843 |
+
)
|
| 844 |
|
| 845 |
+
return {
|
| 846 |
+
"status": final["status"],
|
| 847 |
+
"execution_id": final["execution_id"],
|
| 848 |
+
"verified": final.get("verified"),
|
| 849 |
+
"duration_ms": final.get("duration_ms"),
|
| 850 |
+
"total_predictions": metrics.get("total_predictions", 0),
|
| 851 |
+
"total_images": metrics.get("total_images", 0),
|
| 852 |
+
"quality_score": metrics.get("quality_score"),
|
| 853 |
+
"metrics": metrics,
|
| 854 |
+
"issues": issues,
|
| 855 |
+
"message": "Grader completed from predictions.json",
|
| 856 |
+
}
|
| 857 |
|
| 858 |
@app.post("/api/export")
|
| 859 |
def export_weights(params: ExportParams):
|
|
|
|
| 1324 |
|
| 1325 |
@app.get("/api/dataset/details")
|
| 1326 |
def get_dataset_details(type: str = "local"):
|
| 1327 |
+
metadata = dict(DATASET_METADATA_STORE.get(type, DATASET_METADATA_STORE["local"]))
|
| 1328 |
+
# Catalog KPIs are reference stats for the selected dataset type — not proof that
|
| 1329 |
+
# frames are on disk or browsable in Studio.
|
| 1330 |
+
metadata["browsable"] = False
|
| 1331 |
+
metadata["catalog_only"] = True
|
| 1332 |
+
metadata["browse_hint"] = (
|
| 1333 |
+
"These percentages are catalog estimates. Use Validate & Browse Images Path "
|
| 1334 |
+
"or run 3D Ingest, then open Pipeline Outputs to view real frames."
|
| 1335 |
+
)
|
| 1336 |
return {"status": "ok", "metadata": metadata}
|
| 1337 |
|
| 1338 |
@app.post("/api/dataset/preprocess")
|
| 1339 |
def preprocess_dataset(params: dict):
|
| 1340 |
+
"""Catalog-only path kept for backward compatibility.
|
| 1341 |
+
|
| 1342 |
+
Prefer POST /api/dataset/load for real discovery evidence.
|
| 1343 |
+
If source_path is provided, delegates to real load.
|
| 1344 |
+
"""
|
| 1345 |
+
if params.get("source_path") or params.get("real_load"):
|
| 1346 |
+
return dataset_load(params)
|
| 1347 |
+
|
| 1348 |
dataset_type = params.get("dataset_type", "local")
|
| 1349 |
+
meta = dict(DATASET_METADATA_STORE.get(dataset_type, DATASET_METADATA_STORE["local"]))
|
| 1350 |
+
meta["browsable"] = False
|
| 1351 |
+
meta["catalog_only"] = True
|
| 1352 |
+
meta["browse_hint"] = (
|
| 1353 |
+
"Catalog metadata only — NOT disk load. Use Load & Preprocess or "
|
| 1354 |
+
"Validate & Browse Images Path for verifiable counts."
|
| 1355 |
+
)
|
| 1356 |
+
from sensorflow.execution_ledger import create_execution, finalize
|
| 1357 |
+
|
| 1358 |
+
record = create_execution(
|
| 1359 |
+
"dataset_catalog_metadata",
|
| 1360 |
+
configuration_snapshot={"dataset_type": dataset_type},
|
| 1361 |
+
status="QUEUED",
|
| 1362 |
+
)
|
| 1363 |
+
finalize(
|
| 1364 |
+
record["execution_id"],
|
| 1365 |
+
"NOT_EXECUTED",
|
| 1366 |
+
warnings=["Catalog metadata echo only — no discovery/decode ran"],
|
| 1367 |
+
metrics={"catalog_only": True, "ingestion_pct_is_catalog": True},
|
| 1368 |
+
process_invoked=False,
|
| 1369 |
+
outputs_valid=False,
|
| 1370 |
+
)
|
| 1371 |
return {
|
| 1372 |
+
"status": "NOT_EXECUTED",
|
| 1373 |
+
"execution_id": record["execution_id"],
|
| 1374 |
+
"message": (
|
| 1375 |
+
f"Catalog metadata for {meta['name']} — not the same as loading "
|
| 1376 |
+
f"browsable frames. Use Load & Preprocess for real evidence."
|
| 1377 |
+
),
|
| 1378 |
"dataset_type": dataset_type,
|
| 1379 |
+
"total_frames": None,
|
| 1380 |
+
"browsable": False,
|
| 1381 |
+
"catalog_only": True,
|
| 1382 |
+
"metadata": meta,
|
| 1383 |
}
|
| 1384 |
|
| 1385 |
@app.post("/api/dataset/save-pipeline-tools")
|
|
|
|
| 1813 |
# -----------------------------------------------------------------------
|
| 1814 |
|
| 1815 |
class IngestParams(BaseModel):
|
| 1816 |
+
vendors: List[str] = ["alpamayo", "waymo", "a2d2"]
|
| 1817 |
sequence_id: str = "seq_001"
|
| 1818 |
+
source_path: Optional[str] = None
|
| 1819 |
+
max_frames: Optional[int] = None
|
| 1820 |
|
| 1821 |
class AutoLabelParams(BaseModel):
|
| 1822 |
sequence_id: str = "seq_001"
|
|
|
|
| 1837 |
@app.post("/api/dataset/ingest")
|
| 1838 |
def ingest_dataset(params: IngestParams):
|
| 1839 |
from sensorflow.dataset_fusion_engine import DatasetFusionEngine
|
| 1840 |
+
config = load_config()
|
| 1841 |
+
source_path = params.source_path if params.source_path is not None else config.source_path
|
| 1842 |
try:
|
| 1843 |
engine = DatasetFusionEngine()
|
| 1844 |
+
sequence = engine.ingest(
|
| 1845 |
+
params.vendors,
|
| 1846 |
+
params.sequence_id,
|
| 1847 |
+
source_path=source_path,
|
| 1848 |
+
max_frames=params.max_frames,
|
| 1849 |
+
)
|
| 1850 |
manifest_path = engine.save_manifest(sequence)
|
| 1851 |
+
except FileNotFoundError as e:
|
| 1852 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 1853 |
except Exception as e:
|
| 1854 |
raise HTTPException(status_code=500, detail=f"Ingest failed: {str(e)}")
|
| 1855 |
|
|
|
|
| 1856 |
config.sequence_id = params.sequence_id
|
| 1857 |
config.vendors = params.vendors
|
| 1858 |
+
if params.source_path is not None:
|
| 1859 |
+
config.source_path = params.source_path
|
| 1860 |
with open(CONFIG_PATH, "w") as f:
|
| 1861 |
json.dump(config.model_dump(), f, indent=2)
|
| 1862 |
|
| 1863 |
+
frame_count = len(sequence.frames)
|
| 1864 |
+
demo_stub = bool(sequence.taxonomy_manifest.get("demo_stub"))
|
| 1865 |
+
return {
|
| 1866 |
+
"status": "ok",
|
| 1867 |
+
"manifest": str(manifest_path),
|
| 1868 |
+
"sequence_id": params.sequence_id,
|
| 1869 |
+
"frames": frame_count,
|
| 1870 |
+
"demo_stub": demo_stub,
|
| 1871 |
+
"vendor": sequence.vendor,
|
| 1872 |
+
"source_path": source_path,
|
| 1873 |
+
"message": (
|
| 1874 |
+
f"demo stub: {frame_count} frames"
|
| 1875 |
+
if demo_stub
|
| 1876 |
+
else f"ingested {frame_count} frames from {source_path}"
|
| 1877 |
+
),
|
| 1878 |
+
}
|
| 1879 |
|
| 1880 |
|
| 1881 |
@app.get("/api/dataset/ingest/status")
|
|
|
|
| 1890 |
status["manifest_exists"] = True
|
| 1891 |
status["frames"] = len(manifest_data.get("frames", []))
|
| 1892 |
status["vendor"] = manifest_data.get("vendor", "unknown")
|
| 1893 |
+
tax = manifest_data.get("taxonomy_manifest") or {}
|
| 1894 |
+
status["demo_stub"] = bool(tax.get("demo_stub", status.get("demo_stub", False)))
|
| 1895 |
+
status["stub_note"] = tax.get("stub_note")
|
| 1896 |
return {"status": "ok", "sequence_id": sequence_id, **status}
|
| 1897 |
|
| 1898 |
|
|
|
|
| 1900 |
def auto_label(params: AutoLabelParams):
|
| 1901 |
from sensorflow.perception_automator import PerceptionAutomator
|
| 1902 |
from sensorflow.schemas.unified_frame import UnifiedSequence
|
| 1903 |
+
from sensorflow.execution_ledger import create_execution, mark_running, finalize
|
| 1904 |
+
from sensorflow.execution_ops import artifact_info
|
| 1905 |
|
| 1906 |
manifest = Path("runs/pipeline") / params.sequence_id / "manifest.json"
|
| 1907 |
+
record = create_execution(
|
| 1908 |
+
"auto_label_3d",
|
| 1909 |
+
configuration_snapshot=params.dict(),
|
| 1910 |
+
input_artifacts=[str(manifest), params.sam_checkpoint],
|
| 1911 |
+
)
|
| 1912 |
+
|
| 1913 |
if not manifest.exists():
|
| 1914 |
+
finalize(
|
| 1915 |
+
record["execution_id"],
|
| 1916 |
+
"FAILED",
|
| 1917 |
+
errors=["Manifest not found. Run ingest first."],
|
| 1918 |
+
process_invoked=False,
|
| 1919 |
+
outputs_valid=False,
|
| 1920 |
+
)
|
| 1921 |
+
raise HTTPException(
|
| 1922 |
+
status_code=400,
|
| 1923 |
+
detail={"message": "Manifest not found. Run ingest first.", "execution_id": record["execution_id"]},
|
| 1924 |
+
)
|
| 1925 |
+
|
| 1926 |
+
ckpt = Path(params.sam_checkpoint)
|
| 1927 |
+
sam_ran = False
|
| 1928 |
+
model_name = "sam_vit_b" if not params.no_sam else "none (no_sam)"
|
| 1929 |
+
if params.no_sam:
|
| 1930 |
+
warnings = ["no_sam=true — SAM not invoked; proposals may be GT/synthetic fallback"]
|
| 1931 |
+
elif not ckpt.exists():
|
| 1932 |
+
warnings = [f"SAM checkpoint missing: {params.sam_checkpoint} — model will not run"]
|
| 1933 |
+
else:
|
| 1934 |
+
warnings = []
|
| 1935 |
|
| 1936 |
+
mark_running(record["execution_id"])
|
| 1937 |
try:
|
| 1938 |
sequence = UnifiedSequence.load(manifest)
|
| 1939 |
+
expected_frames = len(sequence.frames)
|
| 1940 |
output_dir = manifest.parent / "proposals"
|
| 1941 |
automator = PerceptionAutomator(
|
| 1942 |
sam_checkpoint=params.sam_checkpoint,
|
|
|
|
| 1944 |
use_sam=not params.no_sam,
|
| 1945 |
)
|
| 1946 |
automator.run_sequence(sequence, output_dir)
|
| 1947 |
+
sam_ran = bool(getattr(automator, "use_sam", False) and automator._mask_generator is not None)
|
| 1948 |
except Exception as e:
|
| 1949 |
+
finalize(
|
| 1950 |
+
record["execution_id"],
|
| 1951 |
+
"FAILED",
|
| 1952 |
+
errors=[str(e)],
|
| 1953 |
+
process_invoked=False,
|
| 1954 |
+
outputs_valid=False,
|
| 1955 |
+
)
|
| 1956 |
+
raise HTTPException(
|
| 1957 |
+
status_code=500,
|
| 1958 |
+
detail={"message": f"Auto-label failed: {str(e)}", "execution_id": record["execution_id"]},
|
| 1959 |
+
)
|
| 1960 |
|
| 1961 |
proposals_dir = manifest.parent / "proposals"
|
| 1962 |
+
proposal_files = list(proposals_dir.glob("*.json")) if proposals_dir.exists() else []
|
| 1963 |
+
num_frames = len(proposal_files)
|
| 1964 |
+
pred_count = 0
|
| 1965 |
+
for pf in proposal_files:
|
| 1966 |
+
try:
|
| 1967 |
+
data = json.loads(pf.read_text())
|
| 1968 |
+
if isinstance(data, list):
|
| 1969 |
+
pred_count += len(data)
|
| 1970 |
+
elif isinstance(data, dict):
|
| 1971 |
+
pred_count += len(data.get("proposals") or data.get("objects") or [])
|
| 1972 |
+
except Exception:
|
| 1973 |
+
pass
|
| 1974 |
+
|
| 1975 |
+
demo_stub = bool(sequence.taxonomy_manifest.get("demo_stub"))
|
| 1976 |
+
if params.no_sam or not sam_ran:
|
| 1977 |
+
status = "NOT_EXECUTED" if num_frames == 0 else "PARTIAL_SUCCESS"
|
| 1978 |
+
if params.no_sam:
|
| 1979 |
+
warnings.append("Model not run (no_sam); do not treat as SAM SUCCESS")
|
| 1980 |
+
elif not sam_ran:
|
| 1981 |
+
warnings.append("SAM unavailable — fallback proposals only")
|
| 1982 |
+
elif num_frames == 0:
|
| 1983 |
+
status = "FAILED"
|
| 1984 |
+
elif num_frames < expected_frames:
|
| 1985 |
+
status = "PARTIAL_SUCCESS"
|
| 1986 |
+
else:
|
| 1987 |
+
status = "SUCCEEDED"
|
| 1988 |
+
|
| 1989 |
+
final = finalize(
|
| 1990 |
+
record["execution_id"],
|
| 1991 |
+
status,
|
| 1992 |
+
records_discovered=expected_frames,
|
| 1993 |
+
records_processed=num_frames,
|
| 1994 |
+
records_succeeded=num_frames,
|
| 1995 |
+
records_failed=max(0, expected_frames - num_frames),
|
| 1996 |
+
metrics={
|
| 1997 |
+
"model": model_name,
|
| 1998 |
+
"checkpoint": artifact_info(ckpt),
|
| 1999 |
+
"sam_ran": sam_ran,
|
| 2000 |
+
"no_sam": params.no_sam,
|
| 2001 |
+
"frames_expected": expected_frames,
|
| 2002 |
+
"frames_processed": num_frames,
|
| 2003 |
+
"predictions_generated": pred_count,
|
| 2004 |
+
"inference_calls": num_frames if sam_ran else 0,
|
| 2005 |
+
"output_dir": str(proposals_dir),
|
| 2006 |
+
"demo_stub": demo_stub,
|
| 2007 |
+
},
|
| 2008 |
+
output_artifacts=[{"path": str(proposals_dir), "kind": "proposals_dir", "files": num_frames}],
|
| 2009 |
+
warnings=warnings,
|
| 2010 |
+
process_invoked=sam_ran,
|
| 2011 |
+
outputs_valid=num_frames > 0,
|
| 2012 |
+
)
|
| 2013 |
+
|
| 2014 |
+
return {
|
| 2015 |
+
"status": final["status"],
|
| 2016 |
+
"execution_id": final["execution_id"],
|
| 2017 |
+
"verified": final.get("verified"),
|
| 2018 |
+
"duration_ms": final.get("duration_ms"),
|
| 2019 |
+
"proposals_dir": str(proposals_dir),
|
| 2020 |
+
"frames_processed": num_frames,
|
| 2021 |
+
"frames_expected": expected_frames,
|
| 2022 |
+
"predictions_generated": pred_count,
|
| 2023 |
+
"inference_calls": num_frames if sam_ran else 0,
|
| 2024 |
+
"model": model_name,
|
| 2025 |
+
"checkpoint": artifact_info(ckpt),
|
| 2026 |
+
"sam_ran": sam_ran,
|
| 2027 |
+
"demo_stub": demo_stub,
|
| 2028 |
+
"message": (
|
| 2029 |
+
f"{'NOT_EXECUTED / fallback' if not sam_ran else 'SAM'}: "
|
| 2030 |
+
f"processed {num_frames}/{expected_frames} frames, {pred_count} predictions → {proposals_dir}"
|
| 2031 |
+
),
|
| 2032 |
+
"events": final.get("events"),
|
| 2033 |
+
}
|
| 2034 |
|
| 2035 |
|
| 2036 |
@app.post("/api/perception/track")
|
|
|
|
| 2121 |
|
| 2122 |
seq_state = state.get(sequence_id, {})
|
| 2123 |
base = Path("runs/pipeline") / sequence_id
|
| 2124 |
+
manifest = base / "manifest.json"
|
| 2125 |
+
proposals_dir = base / "proposals"
|
| 2126 |
+
tracks = base / "tracks.json"
|
| 2127 |
+
benchmark = base / "benchmark" / "metric_card.json"
|
| 2128 |
+
quality_report = base / "benchmark" / "quality_report.json"
|
| 2129 |
+
|
| 2130 |
+
ingest_complete = manifest.exists()
|
| 2131 |
+
perception_complete = proposals_dir.exists() and any(proposals_dir.glob("*.json"))
|
| 2132 |
+
tracking_complete = tracks.exists()
|
| 2133 |
+
benchmark_complete = benchmark.exists()
|
| 2134 |
+
|
| 2135 |
+
frames_ingested = None
|
| 2136 |
+
frames_processed = None
|
| 2137 |
+
demo_stub = None
|
| 2138 |
+
if ingest_complete:
|
| 2139 |
+
try:
|
| 2140 |
+
with open(manifest) as f:
|
| 2141 |
+
manifest_data = json.load(f)
|
| 2142 |
+
frames_ingested = len(manifest_data.get("frames", []))
|
| 2143 |
+
demo_stub = bool((manifest_data.get("taxonomy_manifest") or {}).get("demo_stub"))
|
| 2144 |
+
except Exception:
|
| 2145 |
+
frames_ingested = seq_state.get("frames")
|
| 2146 |
+
demo_stub = seq_state.get("demo_stub")
|
| 2147 |
+
if perception_complete:
|
| 2148 |
+
frames_processed = len(list(proposals_dir.glob("*.json")))
|
| 2149 |
+
|
| 2150 |
+
# Completion stages: True when done, None when not run (UI must not show FAIL).
|
| 2151 |
+
# Launch gate: True/False only after evaluation; None beforehand.
|
| 2152 |
+
launch_gate_passed = seq_state.get("launch_gate_passed")
|
| 2153 |
+
if "launch_gate_passed" not in seq_state:
|
| 2154 |
+
launch_gate_passed = None
|
| 2155 |
+
|
| 2156 |
+
quality_passed = None
|
| 2157 |
+
if quality_report.exists():
|
| 2158 |
+
try:
|
| 2159 |
+
with open(quality_report) as f:
|
| 2160 |
+
quality_passed = bool(json.load(f).get("passed"))
|
| 2161 |
+
except Exception:
|
| 2162 |
+
quality_passed = benchmark_complete
|
| 2163 |
+
|
| 2164 |
return {
|
| 2165 |
"status": "ok",
|
| 2166 |
"sequence_id": sequence_id,
|
| 2167 |
+
"ingest_complete": True if ingest_complete else None,
|
| 2168 |
+
"perception_complete": True if perception_complete else None,
|
| 2169 |
+
"tracking_complete": True if tracking_complete else None,
|
| 2170 |
+
"benchmark_complete": True if benchmark_complete else None,
|
| 2171 |
+
"quality_gate_passed": quality_passed,
|
| 2172 |
+
"launch_gate_passed": launch_gate_passed,
|
| 2173 |
+
"frames_ingested": frames_ingested,
|
| 2174 |
+
"frames_processed": frames_processed,
|
| 2175 |
+
"demo_stub": demo_stub,
|
| 2176 |
"state": seq_state,
|
| 2177 |
}
|
| 2178 |
|
| 2179 |
|
| 2180 |
+
@app.get("/api/pipeline/sequences")
|
| 2181 |
+
def pipeline_sequences():
|
| 2182 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2183 |
+
return {"status": "ok", "sequences": artifacts.list_sequences()}
|
| 2184 |
+
|
| 2185 |
+
|
| 2186 |
+
@app.get("/api/pipeline/artifacts")
|
| 2187 |
+
def pipeline_artifacts(sequence_id: str = "seq_001"):
|
| 2188 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2189 |
+
return {"status": "ok", **artifacts.artifacts_summary(sequence_id)}
|
| 2190 |
+
|
| 2191 |
+
|
| 2192 |
+
@app.get("/api/pipeline/frames")
|
| 2193 |
+
def pipeline_frames(sequence_id: str = "seq_001"):
|
| 2194 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2195 |
+
return artifacts.list_frames(sequence_id)
|
| 2196 |
+
|
| 2197 |
+
|
| 2198 |
+
@app.get("/api/pipeline/frame")
|
| 2199 |
+
def pipeline_frame(sequence_id: str, frame_id: str):
|
| 2200 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2201 |
+
try:
|
| 2202 |
+
return artifacts.get_frame(sequence_id, frame_id)
|
| 2203 |
+
except FileNotFoundError as e:
|
| 2204 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 2205 |
+
|
| 2206 |
+
|
| 2207 |
+
@app.get("/api/pipeline/file")
|
| 2208 |
+
def pipeline_file(path: str):
|
| 2209 |
+
"""Serve a local run/data artifact with path-traversal protection."""
|
| 2210 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2211 |
+
try:
|
| 2212 |
+
file_path, media_type = artifacts.resolve_file(path)
|
| 2213 |
+
except PermissionError as e:
|
| 2214 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 2215 |
+
except FileNotFoundError as e:
|
| 2216 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 2217 |
+
return FileResponse(file_path, media_type=media_type)
|
| 2218 |
+
|
| 2219 |
+
|
| 2220 |
+
@app.get("/api/dataset/browse")
|
| 2221 |
+
def dataset_browse(source_path: str = "data", limit: int = 48):
|
| 2222 |
+
"""Validate Images Path by listing browsable local frames."""
|
| 2223 |
+
from sensorflow import pipeline_artifacts as artifacts
|
| 2224 |
+
return artifacts.scan_source_images(source_path, limit=limit)
|
| 2225 |
+
|
| 2226 |
+
|
| 2227 |
@app.get("/api/mitl/queue")
|
| 2228 |
def get_mitl_queue(sequence_id: str = "seq_001"):
|
| 2229 |
from sensorflow.mitl_copilot import MitlCopilot
|
|
|
|
| 2306 |
from sensorflow.rotr.api import router as rotr_router
|
| 2307 |
app.include_router(rotr_router)
|
| 2308 |
|
| 2309 |
+
# In-app help chatbot (FAQ / page-guide matcher; optional Ollama enrichment).
|
| 2310 |
+
from sensorflow.help.api import router as help_router
|
| 2311 |
+
app.include_router(help_router)
|
| 2312 |
+
|
| 2313 |
+
# Product version + About catalog (GET /api/about, GET /api/version).
|
| 2314 |
+
from sensorflow.about.api import router as about_router
|
| 2315 |
+
app.include_router(about_router)
|
| 2316 |
+
|
| 2317 |
# Mount static folder
|
| 2318 |
static_dir = Path(__file__).parent / "static"
|
| 2319 |
if static_dir.exists():
|
docs/execution-integrity-audit.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sensorflow Studio — Execution Integrity & Observability Audit
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-08-14
|
| 4 |
+
**Scope:** Legacy Studio (`static/`, `app_backend.py`, `sensorflow/`, `train.py`, `infer.py`, `autograder.py`)
|
| 5 |
+
**Mirror:** `hf-space/` copies are effectively identical for these surfaces — findings apply to both.
|
| 6 |
+
**Prime directive:** A UI state is NOT evidence. PASS / Done / 100% / Found / `success:true` / toasts / YAML alone / file existence alone are not proof of execution.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Stage integrity table
|
| 11 |
+
|
| 12 |
+
| COMPONENT | CURRENT BEHAVIOR | ACTUAL EXECUTION? | EVIDENCE | RISK | REQUIRED FIX |
|
| 13 |
+
|-----------|------------------|-------------------|----------|------|--------------|
|
| 14 |
+
| 1 Dataset Configuration / Load & Preprocess | Save config writes JSON; Pre-Check = script file existence; “Load Catalog Metadata” returns hardcoded KPIs (`ingestion_pct: "100.0%"`) after `sleep(0.3)`; Validate & Browse is real disk scan. Toast “saved & verified” without path checks. | **Partial** — browse real; catalog preprocess is not load | `app_backend.py` precheck/details/preprocess; `static/app.js` save/browse handlers | **High** | Real Load & Preprocess with discovery counts + ledger; never show 100% Loaded from catalog |
|
| 15 |
+
| 2 3D Ingest & Fusion | Calls fusion engine; vendor adapters default `demo_stub`; local path can be real. UI PASS when `manifest.json` exists. | **Yes local**; **stub vendors** | ingest API + adapters; gate badges | **Medium** | PASS/DONE only with provenance; surface `demo_stub` + execution_id |
|
| 16 |
+
| 3 3D Perception / Auto-label (SAM) | Frontend hardcodes `no_sam: true`. Automator falls back to GT/synthetic masks; still `status: ok` → PASS. | **No real SAM** on Studio UI path | `static/app.js` perception handler; `perception_automator.py` | **Critical** | Record model/checkpoint/counts; NOT_EXECUTED if model not run; refuse fake SUCCESS |
|
| 17 |
+
| 4 Temporal Tracking | Real Kalman+Hungarian over proposals. | **Yes** (inputs may be fake) | track API | **Medium** | Propagate contamination; ledger entry |
|
| 18 |
+
| 5 Quality Gate | Real metric math vs sequence GT; contaminated proposals inflate pass. | **Yes math**; honesty depends on inputs | quality gate API | **High** | Fail closed on stub/GT-fallback; evidence card |
|
| 19 |
+
| 6 Launch Gate | Threshold check on metric card. | **Yes** (inherited) | launch gate evaluator | **Medium** | Same provenance gate |
|
| 20 |
+
| 7 Model Setup (2D) | UI model card only; no weight verification. | **No** | model cards in `app.js` | **Low–Medium** | Verify weights exist before train/infer |
|
| 21 |
+
| 8 Training Execution | Spawns real `train.py`. Status fabricates losses; forces progress=1.0; ignores exit_code. UI “Process finished” with no verdict. | **Partial** | train start/status; `train.py` | **Critical** | PID, command, exit_code, checkpoint hash, ledger |
|
| 22 |
+
| 9 Auto-Labeler Inference | Runs `infer.py` with check=True. Empty source exits 0 → ok + empty images. UI always success toast. | **Partial** | infer API; `infer.py` | **High** | Counts + checkpoint + exit_code; NOT_EXECUTED/FAILED if 0 images / missing weights |
|
| 23 |
+
| 10 Auto-Grader Quality | Real autograder when predictions exist; empty issues → “Perfect score!”. | **Yes** (heuristic) | grade API; `autograder.py` | **Medium** | Metrics only from grader output; ledger |
|
| 24 |
+
| 11 MITL Review | Hardcoded Unsplash samples; claims HF ingest; Mock Triage Auditor. | **No** | nvidia/load; mitl/evaluate | **Critical** | Label fixtures; never SUCCEEDED for mock ingest |
|
| 25 |
+
| 12 Model Benchmarking | `fallback_mock` hardcoded metrics with `status: ok`. | **No** (unless live card) | `/api/benchmark/compare` | **Critical** | Refuse OK without live metrics; stub = NOT_EXECUTED |
|
| 26 |
+
| 13 Export & Deploy | May run Ultralytics export; returns path without exists(); UI “✓ complete” without `res.ok`. | **Partial** | export API | **High** | Verify artifact; ledger |
|
| 27 |
+
| 14 MCP Settings | Real R/W when file exists; invented servers + “changes simulated”. | **Partial** | mcp APIs | **Medium** | Honest status; evidence stub |
|
| 28 |
+
| 15 SSAM Safety | Hardcoded CA intersections presented as statewide FHWA dataset. | **No real lake** | ssam endpoints + catalog | **High** | Label synthetic demo; stub evidence |
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## High-risk fake-success occurrences
|
| 33 |
+
|
| 34 |
+
### Backend (`app_backend.py`)
|
| 35 |
+
|
| 36 |
+
| Location | Pattern |
|
| 37 |
+
|----------|---------|
|
| 38 |
+
| `/api/precheck` | Success = `train.py` / `infer.py` / `autograder.py` **file existence only** |
|
| 39 |
+
| `/api/train/status` | Fabricates decreasing losses; forces `progress=1.0`; ignores process `returncode` |
|
| 40 |
+
| `/api/infer/run` | Empty run still `status: ok` when script exits 0 with 0 images |
|
| 41 |
+
| `/api/export` | Returns export path without verifying file exists |
|
| 42 |
+
| `/api/nvidia/load` | Hardcoded samples; claims HF ingest success |
|
| 43 |
+
| MITL evaluate | Mock Triage Auditor returned as success-shaped response |
|
| 44 |
+
| `/api/benchmark/compare` | `fallback_mock` hardcoded metrics, `status: ok` |
|
| 45 |
+
| `DATASET_METADATA_STORE` | Hardcoded `ingestion_pct: "100.0%"` catalog KPIs |
|
| 46 |
+
| `/api/dataset/preprocess` | `time.sleep(0.3)` + catalog echo — no disk load |
|
| 47 |
+
| MCP get/toggle | Invented config / “changes simulated” with ok/warning |
|
| 48 |
+
| SSAM store | Hardcoded intersections as statewide dataset |
|
| 49 |
+
| `/api/pipeline/status` | File existence ⇒ `*_complete: true` |
|
| 50 |
+
|
| 51 |
+
### Frontend (`static/app.js`)
|
| 52 |
+
|
| 53 |
+
| Location | Pattern |
|
| 54 |
+
|----------|---------|
|
| 55 |
+
| Gate badges | Completion stages → **PASS** from file-complete; never FAIL |
|
| 56 |
+
| Config save | “verified successfully” without `res.ok` / path validation |
|
| 57 |
+
| Training poll | “Process finished” with no exit_code / success check |
|
| 58 |
+
| Infer | Always success toast (even empty / `!res.ok`) |
|
| 59 |
+
| Grader | Empty issues → “Perfect score!” |
|
| 60 |
+
| Export | “✓ Export complete” without `res.ok` |
|
| 61 |
+
| Perception | **`no_sam: true` hardcoded** |
|
| 62 |
+
|
| 63 |
+
### Scripts / sensorflow
|
| 64 |
+
|
| 65 |
+
| Location | Pattern |
|
| 66 |
+
|----------|---------|
|
| 67 |
+
| `train.py` dgx-spark | Prints remote offload; uses local device |
|
| 68 |
+
| `infer.py` | No images → print and **exit 0** |
|
| 69 |
+
| `perception_automator.py` | Silent SAM off; synthetic masks/LiDAR; GT class copy |
|
| 70 |
+
| Vendor adapters | Built-in ~3-frame `demo_stub` lakes |
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Cross-cutting patterns
|
| 75 |
+
|
| 76 |
+
1. **`status: "ok"` / success toast without evidence**
|
| 77 |
+
2. **File existence = done/PASS**
|
| 78 |
+
3. **Hardcoded metrics / “100%” catalog KPIs**
|
| 79 |
+
4. **Simulated delays / stubs claiming complete**
|
| 80 |
+
5. **Subprocess integrity gaps** (exit ignored, empty exit 0)
|
| 81 |
+
6. **GT contamination loop** (`no_sam` → synthetic/GT proposals → quality/launch PASS)
|
| 82 |
+
7. **Frontend rarely checks `res.ok`**
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## What is relatively honest today
|
| 87 |
+
|
| 88 |
+
- Validate & Browse (`/api/dataset/browse`) — real filesystem scan
|
| 89 |
+
- Local ingest (non-stub) — real frames → manifest
|
| 90 |
+
- Tracking / quality math / launch thresholds / autograder heuristics / YOLO train+infer — real when inputs and deps exist
|
| 91 |
+
- Catalog preprocess messaging partially hardened (`catalog_only`, `browse_hint`) but KPIs still read as Loaded
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## Remediation shipped in `feat/execution-integrity`
|
| 96 |
+
|
| 97 |
+
**Implemented:** Execution ledger + APIs; real Dataset Load & Preprocess; Auto-Label / Infer / Train / Grader evidence wiring; YAML semantic validation; script verification; backend health; Strict Mode; Evidence Cards + Global Execution Console; failure + deterministic tests; remaining stages show honest `NOT_EXECUTED` / unverified stubs.
|
| 98 |
+
|
| 99 |
+
**Deferred (honest stubs only):** Full MITL/HF ingest, real SSAM lake, real MCP process probes, SAM GPU path when checkpoint absent, full 15-stage process verification beyond Dataset/Auto-Label/Train/Grader.
|
hf-space/.dockerignore
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
node_modules/
|
| 2 |
-
.venv/
|
| 3 |
-
.git/
|
| 4 |
-
runs/
|
| 5 |
-
dist/
|
| 6 |
-
models/
|
| 7 |
-
*.pt
|
| 8 |
-
__pycache__/
|
| 9 |
-
**/__pycache__/
|
| 10 |
-
.pytest_cache/
|
| 11 |
-
.claude/
|
| 12 |
-
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/.gitignore
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
# Virtual environments
|
| 2 |
-
.venv/
|
| 3 |
-
venv/
|
| 4 |
-
env/
|
| 5 |
-
ENV/
|
| 6 |
-
|
| 7 |
-
# Python
|
| 8 |
-
__pycache__/
|
| 9 |
-
*.py[cod]
|
| 10 |
-
*$py.class
|
| 11 |
-
*.so
|
| 12 |
-
.Python
|
| 13 |
-
build/
|
| 14 |
-
develop-eggs/
|
| 15 |
-
dist/
|
| 16 |
-
downloads/
|
| 17 |
-
eggs/
|
| 18 |
-
.eggs/
|
| 19 |
-
lib/
|
| 20 |
-
lib64/
|
| 21 |
-
parts/
|
| 22 |
-
sdist/
|
| 23 |
-
var/
|
| 24 |
-
wheels/
|
| 25 |
-
*.egg-info/
|
| 26 |
-
.installed.cfg
|
| 27 |
-
*.egg
|
| 28 |
-
|
| 29 |
-
# Testing
|
| 30 |
-
.pytest_cache/
|
| 31 |
-
.coverage
|
| 32 |
-
htmlcov/
|
| 33 |
-
|
| 34 |
-
# Node
|
| 35 |
-
node_modules/
|
| 36 |
-
package-lock.json
|
| 37 |
-
|
| 38 |
-
# IDE
|
| 39 |
-
.vscode/
|
| 40 |
-
.idea/
|
| 41 |
-
*.swp
|
| 42 |
-
*.swo
|
| 43 |
-
*~
|
| 44 |
-
|
| 45 |
-
# OS
|
| 46 |
-
.DS_Store
|
| 47 |
-
Thumbs.db
|
| 48 |
-
|
| 49 |
-
# Large model files (download on deployment)
|
| 50 |
-
*.pt
|
| 51 |
-
|
| 52 |
-
# Logs
|
| 53 |
-
*.log
|
| 54 |
-
logs/
|
| 55 |
-
|
| 56 |
-
# Local development
|
| 57 |
-
.env
|
| 58 |
-
.env.local
|
| 59 |
-
*.local.json
|
| 60 |
-
|
| 61 |
-
# Temporary
|
| 62 |
-
/tmp/
|
| 63 |
-
temp/
|
| 64 |
-
|
| 65 |
-
# Generated runtime/test artifacts
|
| 66 |
-
runs/labeleval/
|
| 67 |
-
runs/megaeval/
|
| 68 |
-
runs/pipeline/frame_test/
|
| 69 |
-
runs/pipeline/qg_test/
|
| 70 |
-
runs/pipeline/roundtrip/
|
| 71 |
-
runs/pipeline/test_seq/
|
| 72 |
-
runs/pipeline/test_waymo/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/ANTIGRAVITY_PROMPT.md
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
# Accident Analysis Feature Implementation Prompt for Ollama
|
| 2 |
-
|
| 3 |
-
**Implementation Model**: `qwen3-coder:30b` (ollama-spark-coding)
|
| 4 |
-
|
| 5 |
-
**MCP Host**: `http://dgx-spark.tail16d8d9.ts.net:11434`
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## Quick Summary
|
| 10 |
-
|
| 11 |
-
Implement a production-grade accident/crash analysis feature using two available toolkits:
|
| 12 |
-
1. **CrashDataAnalyzer** (`crash_analyzer.py`) — SWITRS data analysis
|
| 13 |
-
2. **DataExtractionPipeline** (`data_extractor.py`) — ETL for any source
|
| 14 |
-
|
| 15 |
-
Build 6 layers: Ingestion → Validation → Cleaning → Analysis → Insights → API
|
| 16 |
-
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
-
## Direct Prompt for qwen3-coder:30b
|
| 20 |
-
|
| 21 |
-
Copy and submit to `qwen3-coder:30b` via Ollama MCP:
|
| 22 |
-
|
| 23 |
-
```
|
| 24 |
-
=== ACCIDENT ANALYSIS FEATURE - IMPLEMENTATION ===
|
| 25 |
-
|
| 26 |
-
Build a production accident/crash analysis platform in Python 3.9+.
|
| 27 |
-
|
| 28 |
-
AVAILABLE TOOLKITS (in repository):
|
| 29 |
-
- crash_analyzer.py: CrashDataAnalyzer for SWITRS data analysis
|
| 30 |
-
- data_extractor.py: DataExtractionPipeline, DataValidator, DataCleaner
|
| 31 |
-
- Examples: example_crash_analysis.py, example_data_extraction.py
|
| 32 |
-
|
| 33 |
-
BUILD 6 LAYERS (in order):
|
| 34 |
-
|
| 35 |
-
LAYER 1: DATA INGESTION (accident_importer.py)
|
| 36 |
-
- Class: AccidentDataImporter
|
| 37 |
-
- Load from: CSV, APIs (paginated), SQL databases, JSON/Parquet
|
| 38 |
-
- Auto-detect format, handle errors, standardize schema
|
| 39 |
-
- Base: DataExtractionPipeline
|
| 40 |
-
|
| 41 |
-
LAYER 2: DATA VALIDATION (accident_validator.py)
|
| 42 |
-
- Class: AccidentDataValidator
|
| 43 |
-
- Validate: required fields, types, coordinate ranges, date ranges
|
| 44 |
-
- Detect: outliers, missing values, anomalies
|
| 45 |
-
- Return: quality report with score
|
| 46 |
-
- Base: DataValidator
|
| 47 |
-
|
| 48 |
-
LAYER 3: DATA CLEANING (accident_cleaner.py)
|
| 49 |
-
- Class: AccidentDataCleaner
|
| 50 |
-
- Standardize: column names (snake_case), severity, vehicle types
|
| 51 |
-
- Handle: missing values, duplicates, datetime, coordinates
|
| 52 |
-
- Base: DataCleaner
|
| 53 |
-
|
| 54 |
-
LAYER 4: ANALYSIS ENGINE (accident_analysis.py)
|
| 55 |
-
- Classes: MetadataAnalyzer, GeospatialAnalyzer, DemographicsAnalyzer
|
| 56 |
-
- Orchestrator: AccidentAnalysisEngine
|
| 57 |
-
- Analyze: temporal patterns, hotspots, demographics
|
| 58 |
-
- Cache results in memory
|
| 59 |
-
- Base: CrashDataAnalyzer
|
| 60 |
-
|
| 61 |
-
LAYER 5: INSIGHTS & REPORTING (accident_insights.py, accident_report.py)
|
| 62 |
-
- AccidentInsights: deadliest times/places/groups, risk scores
|
| 63 |
-
- AccidentReport: HTML/PDF/JSON export, visualizations
|
| 64 |
-
|
| 65 |
-
LAYER 6: API & INTEGRATION (main.py, routes/, models/)
|
| 66 |
-
- FastAPI endpoints:
|
| 67 |
-
POST /api/analyze (upload CSV)
|
| 68 |
-
GET /api/hotspots (geographic)
|
| 69 |
-
GET /api/insights (findings)
|
| 70 |
-
GET /api/report (download)
|
| 71 |
-
WebSocket /ws/stream (real-time)
|
| 72 |
-
|
| 73 |
-
PRODUCTION REQUIREMENTS:
|
| 74 |
-
✓ Complete working code (no TODOs)
|
| 75 |
-
✓ Type hints on all functions
|
| 76 |
-
✓ Google-style docstrings
|
| 77 |
-
✓ Error handling (try/except with logging)
|
| 78 |
-
✓ 80%+ test coverage (pytest)
|
| 79 |
-
✓ Performance: <5s for 10k records
|
| 80 |
-
✓ Security: input validation, SQL injection protection
|
| 81 |
-
✓ Logging: DEBUG/INFO/ERROR/CRITICAL levels
|
| 82 |
-
✓ Config: environment variables
|
| 83 |
-
|
| 84 |
-
OUTPUT EACH LAYER:
|
| 85 |
-
1. Complete working Python code
|
| 86 |
-
2. Unit tests (pytest)
|
| 87 |
-
3. Integration tests
|
| 88 |
-
4. Full docstrings & type hints
|
| 89 |
-
5. Error handling with examples
|
| 90 |
-
6. Usage examples
|
| 91 |
-
|
| 92 |
-
Start with LAYER 1. Use existing toolkits. Follow PEP 8.
|
| 93 |
-
```
|
| 94 |
-
|
| 95 |
-
---
|
| 96 |
-
|
| 97 |
-
## MCP Server Configuration
|
| 98 |
-
|
| 99 |
-
```json
|
| 100 |
-
{
|
| 101 |
-
"mcpServers": {
|
| 102 |
-
"ollama-spark-coding": {
|
| 103 |
-
"command": "npx",
|
| 104 |
-
"args": ["-y", "@iflow-mcp/ollama-mcp"],
|
| 105 |
-
"env": {
|
| 106 |
-
"OLLAMA_HOST": "http://dgx-spark.tail16d8d9.ts.net:11434",
|
| 107 |
-
"OLLAMA_MODEL": "qwen3-coder:30b"
|
| 108 |
-
}
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
```
|
| 113 |
-
|
| 114 |
-
---
|
| 115 |
-
|
| 116 |
-
## Model Selection
|
| 117 |
-
|
| 118 |
-
**Primary**: `qwen3-coder:30b` (code generation specialist)
|
| 119 |
-
- Best for Python implementation across 6 layers
|
| 120 |
-
- Understands complex architecture and APIs
|
| 121 |
-
- Large context window for full specs
|
| 122 |
-
|
| 123 |
-
**Optional**: `reasoning-core` (planning/validation)
|
| 124 |
-
- Good for architecture review
|
| 125 |
-
- Validates implementation decisions
|
| 126 |
-
|
| 127 |
-
**Optional**: `gemma4:26b` (documentation)
|
| 128 |
-
- Generate docs, examples, deployment guides
|
| 129 |
-
|
| 130 |
-
---
|
| 131 |
-
|
| 132 |
-
## 6-Layer Architecture
|
| 133 |
-
|
| 134 |
-
```
|
| 135 |
-
Data Source (CSV/API/DB)
|
| 136 |
-
↓
|
| 137 |
-
LAYER 1: Data Ingestion
|
| 138 |
-
↓
|
| 139 |
-
LAYER 2: Data Validation (Quality Check)
|
| 140 |
-
↓
|
| 141 |
-
LAYER 3: Data Cleaning (Normalization)
|
| 142 |
-
↓
|
| 143 |
-
LAYER 4: Analysis Engine (Metadata/Geo/Demographics)
|
| 144 |
-
↓
|
| 145 |
-
LAYER 5: Insights & Reports (HTML/PDF/JSON)
|
| 146 |
-
↓
|
| 147 |
-
LAYER 6: REST API (FastAPI endpoints)
|
| 148 |
-
```
|
| 149 |
-
|
| 150 |
-
---
|
| 151 |
-
|
| 152 |
-
## Key Requirements
|
| 153 |
-
|
| 154 |
-
- **Language**: Python 3.9+
|
| 155 |
-
- **Framework**: FastAPI (API layer)
|
| 156 |
-
- **Testing**: pytest (80%+ coverage)
|
| 157 |
-
- **Data**: SWITRS format (accidents.csv, vehicles.csv, parties.csv)
|
| 158 |
-
- **Performance**: <5 seconds for 10k records
|
| 159 |
-
- **Code Quality**: Type hints, docstrings, error handling
|
| 160 |
-
|
| 161 |
-
---
|
| 162 |
-
|
| 163 |
-
## Timeline
|
| 164 |
-
|
| 165 |
-
- Layer 1 (Ingestion): 1-2 days
|
| 166 |
-
- Layer 2 (Validation): 1 day
|
| 167 |
-
- Layer 3 (Cleaning): 1 day
|
| 168 |
-
- Layer 4 (Analysis): 2-3 days
|
| 169 |
-
- Layer 5 (Insights): 1-2 days
|
| 170 |
-
- Layer 6 (API): 2 days
|
| 171 |
-
- Tests & Deployment: 2 days
|
| 172 |
-
|
| 173 |
-
**Total**: 11-17 days
|
| 174 |
-
|
| 175 |
-
---
|
| 176 |
-
|
| 177 |
-
## Success Criteria
|
| 178 |
-
|
| 179 |
-
✅ Extract from 3+ sources (CSV, API, database)
|
| 180 |
-
✅ Validate with >95% accuracy
|
| 181 |
-
✅ Analyze temporal, geospatial, demographic patterns
|
| 182 |
-
✅ Identify hotspots and high-risk groups
|
| 183 |
-
✅ Generate HTML/PDF/JSON reports
|
| 184 |
-
✅ Provide REST API
|
| 185 |
-
✅ Support real-time analysis
|
| 186 |
-
✅ Handle 1GB+ datasets
|
| 187 |
-
✅ 80%+ test coverage
|
| 188 |
-
✅ Production-ready code
|
| 189 |
-
|
| 190 |
-
---
|
| 191 |
-
|
| 192 |
-
**Ready for Ollama Implementation**: qwen3-coder:30b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/ARCHITECTURE.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
# Sensorflow Studio — Aggregate-First Evaluation Architecture (megaeval)
|
| 2 |
-
|
| 3 |
-
The `sensorflow/megaeval/` layer replaces the "evaluation-record browser" model
|
| 4 |
-
(annotation as the primary unit) with an **aggregate-first, risk-first,
|
| 5 |
-
cohort-first, drill-down-on-demand** design.
|
| 6 |
-
|
| 7 |
-
## Mental model → code
|
| 8 |
-
|
| 9 |
-
| Concept | Meaning | Module / storage |
|
| 10 |
-
| --- | --- | --- |
|
| 11 |
-
| Dataset | immutable source population | `population.py` → `runs/megaeval/populations/{pop}/part-*.npz` |
|
| 12 |
-
| Evaluation Run | model+config being evaluated (async job, first-class) | `runs.py` → `runs/megaeval/runs/{run}/run.json` |
|
| 13 |
-
| Population | all objects evaluated in a run | headline row of the metric cube |
|
| 14 |
-
| Cohort | slice by class/weather/lighting/road/scenario/sensor/distance/speed/occlusion | cube cells + `POST /api/evaluations/query` |
|
| 15 |
-
| Container | scene/frame/segment holding related objects | `containers.npz` aggregate table |
|
| 16 |
-
| Annotation | individual object — forensic view only | per-object `objects-part-*.npz`, loaded one partition at a time |
|
| 17 |
-
|
| 18 |
-
## Data flow
|
| 19 |
-
|
| 20 |
-
```
|
| 21 |
-
generate population (vectorized numpy, partitioned npz)
|
| 22 |
-
│
|
| 23 |
-
create run (queued) ──► worker pool (4 threads) processes partitions:
|
| 24 |
-
│ model simulation → PARTIAL sufficient statistics
|
| 25 |
-
│ (n, tp, fp, fn, sum_iou, conf sums, histograms …)
|
| 26 |
-
│ grouped by the 9 cube dimensions, per partition
|
| 27 |
-
▼
|
| 28 |
-
reduce (concat + groupby-sum) ──► materialize METRIC CUBE (cube.npz)
|
| 29 |
-
│ + error index, container table, sketches,
|
| 30 |
-
│ container embeddings
|
| 31 |
-
▼
|
| 32 |
-
publish ──► queries served by QueryRouter: cache → cube → (rare) record scan
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
Aggregation **never** scans raw records at query time; queries like
|
| 36 |
-
"pedestrian recall at night in rain for model v42" are cube lookups.
|
| 37 |
-
|
| 38 |
-
## Single-node stand-ins for distributed infrastructure
|
| 39 |
-
|
| 40 |
-
This repo runs on one laptop; the seams are shaped so each piece maps to the
|
| 41 |
-
production-scale equivalent:
|
| 42 |
-
|
| 43 |
-
| Here (single node) | Production analogue |
|
| 44 |
-
| --- | --- |
|
| 45 |
-
| npz column partitions (`part-0000.npz…`, hash-partitioned by container) | Parquet files in an Iceberg/Delta table, partitioned by scene |
|
| 46 |
-
| `ThreadPoolExecutor` workers over partitions | Spark executors / Flink task slots |
|
| 47 |
-
| partial stats per partition + `reduce_partials` | map-side combine + shuffle reduce |
|
| 48 |
-
| `QueryRouter` (cache → cube → scan) with latency+source reporting | OLAP engine (Druid/Pinot/ClickHouse) over the lakehouse |
|
| 49 |
-
| `QueryCache` keyed by hash(dataset+model+filters+group_by+metrics) | result cache / materialized views |
|
| 50 |
-
| in-process SSE progress events | job-status topic (Kafka) + progress service |
|
| 51 |
-
| numpy cosine similarity over container embeddings | vector DB (hybrid retrieval) |
|
| 52 |
-
|
| 53 |
-
## Honesty rules encoded in the API
|
| 54 |
-
|
| 55 |
-
- Cube counts and derived ratios are **exact** (`meta.exact = true`).
|
| 56 |
-
- Percentiles/distributions come from fixed-bin quantile histograms;
|
| 57 |
-
cardinalities from HyperLogLog — always labeled approximate.
|
| 58 |
-
- Precision/recall from human review are **sampling estimates with 95% CIs**
|
| 59 |
-
(stratified risk-weighted design: Wilson per stratum, weighted normal-approx
|
| 60 |
-
combination), reported with n reviewed / frame size / method.
|
| 61 |
-
- Every query response reports `source` (cache/cube/scan), `cache_hit`,
|
| 62 |
-
and `latency_ms` so aggregate-first behavior is observable in the UI.
|
| 63 |
-
- Evaluation runs are reproducible: lineage (dataset, model+checkpoint, label
|
| 64 |
-
version, evaluator code version, metric version, thresholds, sampling config,
|
| 65 |
-
seed, hardware) is stored on the run and derives the deterministic seed.
|
| 66 |
-
|
| 67 |
-
## Performance SLOs (measured locally, 320k-object population, 67k cube cells)
|
| 68 |
-
|
| 69 |
-
| Query | SLO | Measured |
|
| 70 |
-
| --- | --- | --- |
|
| 71 |
-
| aggregate dashboard (group by class) | < 2 s | ~75 ms |
|
| 72 |
-
| filtered cohort query | < 3 s | ~350 ms first / sub-ms cached |
|
| 73 |
-
| cached query | < 100 ms | ~0.1 ms |
|
| 74 |
-
| object investigation (one container, forensic) | < 1 s | ~50–200 ms |
|
| 75 |
-
|
| 76 |
-
The legacy `sensorflow/evaluation/` layer (label-evaluation pipeline, triage,
|
| 77 |
-
HITL, training flywheel) remains unchanged; megaeval integrates with its
|
| 78 |
-
regression tracker and alerting rather than duplicating them.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/CRASH_DATA_GUIDE.md
DELETED
|
@@ -1,378 +0,0 @@
|
|
| 1 |
-
# Crash Data Analysis Guide
|
| 2 |
-
|
| 3 |
-
Complete guide to analyzing crash data with metadata, geospatial, and vehicle/traveler dimensions.
|
| 4 |
-
|
| 5 |
-
## Quick Start
|
| 6 |
-
|
| 7 |
-
```python
|
| 8 |
-
from crash_analyzer import load_switrs_data
|
| 9 |
-
|
| 10 |
-
# Load SWITRS CSV data
|
| 11 |
-
analyzer = load_switrs_data('accidents.csv', 'vehicles.csv', 'parties.csv')
|
| 12 |
-
|
| 13 |
-
# Generate full report
|
| 14 |
-
report = analyzer.generate_summary_report()
|
| 15 |
-
```
|
| 16 |
-
|
| 17 |
-
## Data Source: SWITRS
|
| 18 |
-
|
| 19 |
-
**Reference**: https://tims.berkeley.edu/help/crashdata/switrs-geocoding.php
|
| 20 |
-
|
| 21 |
-
SWITRS (Statewide Integrated Traffic Records System) provides comprehensive California highway crash data with three main record types:
|
| 22 |
-
|
| 23 |
-
### 1. Accidents Table (Core Records)
|
| 24 |
-
Primary crash incident information.
|
| 25 |
-
|
| 26 |
-
| Field | Type | Description | Example |
|
| 27 |
-
|---|---|---|---|
|
| 28 |
-
| `case_id` | int | Unique crash identifier | 123456 |
|
| 29 |
-
| `accident_date` | date | Date of crash | 2024-01-15 |
|
| 30 |
-
| `accident_time` | time | Time of crash (24-hr) | 14:30 |
|
| 31 |
-
| `latitude` | float | Geocoded latitude | 37.7749 |
|
| 32 |
-
| `longitude` | float | Geocoded longitude | -122.4194 |
|
| 33 |
-
| `county` | string | County name | "San Francisco" |
|
| 34 |
-
| `district` | string | CHP district | "SF" |
|
| 35 |
-
| `severity` | string | Crash severity | F/I/P (Fatal/Injury/PropertyDamage) |
|
| 36 |
-
| `collision_type` | string | Type of collision | "Rear End", "Broadside", etc. |
|
| 37 |
-
| `weather_1` | string | Primary weather | "Clear", "Rain", "Fog" |
|
| 38 |
-
| `weather_2` | string | Secondary weather | Optional |
|
| 39 |
-
| `surface_condition` | string | Road surface | "Dry", "Wet", "Snow" |
|
| 40 |
-
| `traffic_control_device` | string | Traffic control status | "Functioning", "Not Functioning" |
|
| 41 |
-
| `fatality_count` | int | Number of fatalities | 0-5 |
|
| 42 |
-
| `injury_count` | int | Number of injuries | 0-10 |
|
| 43 |
-
|
| 44 |
-
### 2. Vehicles Table (Vehicle Details)
|
| 45 |
-
Vehicle information for each crash.
|
| 46 |
-
|
| 47 |
-
| Field | Type | Description |
|
| 48 |
-
|---|---|---|
|
| 49 |
-
| `case_id` | int | Link to accidents table |
|
| 50 |
-
| `vehicle_id` | int | Unique vehicle in crash |
|
| 51 |
-
| `vehicle_type` | string | Car, Truck, Motorcycle, SUV, Van, etc. |
|
| 52 |
-
| `vehicle_year` | int | Vehicle model year |
|
| 53 |
-
| `vehicle_make` | string | Manufacturer (Toyota, Ford, etc.) |
|
| 54 |
-
| `vehicle_model` | string | Model name |
|
| 55 |
-
| `vehicle_color` | string | Color |
|
| 56 |
-
| `damage_extent` | string | Severe, Moderate, Minor |
|
| 57 |
-
| `direction_of_travel` | string | N, S, E, W, etc. |
|
| 58 |
-
|
| 59 |
-
### 3. Parties Table (Occupants & Pedestrians)
|
| 60 |
-
Information about crash participants (drivers, passengers, pedestrians).
|
| 61 |
-
|
| 62 |
-
| Field | Type | Description |
|
| 63 |
-
|---|---|---|
|
| 64 |
-
| `case_id` | int | Link to accidents table |
|
| 65 |
-
| `party_number` | int | Sequence in crash |
|
| 66 |
-
| `party_type` | string | Driver, Passenger, Pedestrian, Cyclist |
|
| 67 |
-
| `age` | int | Age in years |
|
| 68 |
-
| `gender` | string | M/F |
|
| 69 |
-
| `sobriety` | int | 1=Sober, 2=Drinking, 3=Intoxicated, 4=Unknown |
|
| 70 |
-
| `injury_level` | int | 0=None, 1=Possible, 2=Probable, 3=Severe, 4=Fatal |
|
| 71 |
-
| `safety_equipment_used` | string | Seatbelt, Helmet, etc. |
|
| 72 |
-
| `ejection` | string | Yes/No/Unknown |
|
| 73 |
-
| `primary_collision_factor` | string | Cause (Speeding, DUI, etc.) |
|
| 74 |
-
|
| 75 |
-
---
|
| 76 |
-
|
| 77 |
-
## Analysis Dimensions
|
| 78 |
-
|
| 79 |
-
### 1. METADATA ANALYSIS
|
| 80 |
-
|
| 81 |
-
**Time-Based Patterns**
|
| 82 |
-
```python
|
| 83 |
-
# Identify dangerous times
|
| 84 |
-
temporal = analyzer.analyze_temporal_patterns()
|
| 85 |
-
# Returns: crashes by hour, day, month, year + deadliest times
|
| 86 |
-
```
|
| 87 |
-
|
| 88 |
-
**What to Look For**:
|
| 89 |
-
- Rush hour peaks (7-9am, 4-6pm)
|
| 90 |
-
- Weekend spikes
|
| 91 |
-
- Holiday effect (New Year, July 4th)
|
| 92 |
-
- Seasonal trends (winter weather, spring break)
|
| 93 |
-
|
| 94 |
-
**Severity Analysis**
|
| 95 |
-
```python
|
| 96 |
-
severity = analyzer.analyze_severity()
|
| 97 |
-
# Returns: distribution of Fatal/Injury/Property Damage
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
**What to Report**:
|
| 101 |
-
- Fatality rate (total deaths / total crashes)
|
| 102 |
-
- Injury-to-crash ratio
|
| 103 |
-
- Crash trend over time (increasing/decreasing)
|
| 104 |
-
|
| 105 |
-
**Collision Types**
|
| 106 |
-
```python
|
| 107 |
-
collisions = analyzer.analyze_collision_types()
|
| 108 |
-
# Returns: most common collision types and deadliest types
|
| 109 |
-
```
|
| 110 |
-
|
| 111 |
-
**Key Metrics**:
|
| 112 |
-
- Most common collision type
|
| 113 |
-
- Deadliest collision type (by fatality rate)
|
| 114 |
-
- Preventable vs. unpreventable patterns
|
| 115 |
-
|
| 116 |
-
---
|
| 117 |
-
|
| 118 |
-
### 2. GEOSPATIAL ANALYSIS
|
| 119 |
-
|
| 120 |
-
**Geographic Summaries**
|
| 121 |
-
```python
|
| 122 |
-
geo = analyzer.get_geographic_summary()
|
| 123 |
-
# Returns: crashes by county/district with severity breakdown
|
| 124 |
-
```
|
| 125 |
-
|
| 126 |
-
**Geographic Hotspot Detection**
|
| 127 |
-
```python
|
| 128 |
-
hotspots = analyzer.identify_hotspots()
|
| 129 |
-
# Returns: grid-based clustering of high-crash areas
|
| 130 |
-
```
|
| 131 |
-
|
| 132 |
-
**Interpretation**:
|
| 133 |
-
- Cluster size = grid cell crashes
|
| 134 |
-
- Coordinates = center of high-risk area
|
| 135 |
-
- Fatal count = number of fatal crashes in area
|
| 136 |
-
- Focus enforcement/infrastructure improvements on hotspots
|
| 137 |
-
|
| 138 |
-
**Use Cases**:
|
| 139 |
-
- Identify intersections needing traffic signal upgrades
|
| 140 |
-
- Plan emergency response deployments
|
| 141 |
-
- Target enforcement activities
|
| 142 |
-
- Prioritize road maintenance
|
| 143 |
-
|
| 144 |
-
**Visualization Recommendations**:
|
| 145 |
-
- Heatmap of crash density (Folium, Kepler.gl)
|
| 146 |
-
- Cluster markers on interactive map
|
| 147 |
-
- Severity overlay (color-coded pins)
|
| 148 |
-
- Temporal animation (crashes by month)
|
| 149 |
-
|
| 150 |
-
---
|
| 151 |
-
|
| 152 |
-
### 3. VEHICLE/TRAVELER ANALYSIS
|
| 153 |
-
|
| 154 |
-
**Vehicle Risk Analysis**
|
| 155 |
-
```python
|
| 156 |
-
vehicles_analysis = analyzer.analyze_vehicle_types()
|
| 157 |
-
# Returns: crashes per vehicle type + fatality rates
|
| 158 |
-
```
|
| 159 |
-
|
| 160 |
-
**Insights**:
|
| 161 |
-
- Which vehicle types are overrepresented in crashes?
|
| 162 |
-
- Which have highest fatality rates?
|
| 163 |
-
- Older vs. newer vehicles
|
| 164 |
-
- Motorcycle vs. car comparison
|
| 165 |
-
|
| 166 |
-
**Traveler Demographics**
|
| 167 |
-
```python
|
| 168 |
-
demographics = analyzer.analyze_demographics()
|
| 169 |
-
# Returns: age distribution, gender, sobriety, injury levels
|
| 170 |
-
```
|
| 171 |
-
|
| 172 |
-
**Key Demographics**:
|
| 173 |
-
- Age group risk (teenagers, elderly highest risk)
|
| 174 |
-
- Gender patterns
|
| 175 |
-
- Impairment prevalence
|
| 176 |
-
- Safety equipment usage (seatbelt, helmet)
|
| 177 |
-
|
| 178 |
-
**Injury Patterns**
|
| 179 |
-
```python
|
| 180 |
-
injuries = analyzer.analyze_injury_patterns()
|
| 181 |
-
# Returns: injury severity by age group, party type
|
| 182 |
-
```
|
| 183 |
-
|
| 184 |
-
**Focus Areas**:
|
| 185 |
-
- Vulnerable road users (pedestrians, cyclists)
|
| 186 |
-
- Age-specific injury severity
|
| 187 |
-
- Occupant vs. pedestrian injury rates
|
| 188 |
-
- Effectiveness of safety equipment
|
| 189 |
-
|
| 190 |
-
---
|
| 191 |
-
|
| 192 |
-
## Data Preparation
|
| 193 |
-
|
| 194 |
-
### Step 1: Obtain SWITRS Data
|
| 195 |
-
Options:
|
| 196 |
-
- **TIMS Website**: https://tims.berkeley.edu (public access, some limitations)
|
| 197 |
-
- **California Highway Patrol**: Direct request
|
| 198 |
-
- **Local Law Enforcement**: May have county-specific data
|
| 199 |
-
|
| 200 |
-
### Step 2: Data Format
|
| 201 |
-
- Expected format: **CSV files** (one file per table type)
|
| 202 |
-
- Delimiter: Comma (`,`)
|
| 203 |
-
- Character encoding: UTF-8
|
| 204 |
-
- Missing values: Blank or `NULL`
|
| 205 |
-
|
| 206 |
-
### Step 3: Clean Data
|
| 207 |
-
```python
|
| 208 |
-
import pandas as pd
|
| 209 |
-
|
| 210 |
-
accidents = pd.read_csv('accidents.csv')
|
| 211 |
-
|
| 212 |
-
# Remove duplicates
|
| 213 |
-
accidents = accidents.drop_duplicates(subset=['case_id'])
|
| 214 |
-
|
| 215 |
-
# Handle missing geolocation
|
| 216 |
-
accidents = accidents.dropna(subset=['latitude', 'longitude'])
|
| 217 |
-
|
| 218 |
-
# Validate date format
|
| 219 |
-
accidents['accident_date'] = pd.to_datetime(accidents['accident_date'], errors='coerce')
|
| 220 |
-
```
|
| 221 |
-
|
| 222 |
-
### Step 4: Load into Analyzer
|
| 223 |
-
```python
|
| 224 |
-
from crash_analyzer import CrashDataAnalyzer
|
| 225 |
-
|
| 226 |
-
analyzer = CrashDataAnalyzer(accidents, vehicles, parties)
|
| 227 |
-
```
|
| 228 |
-
|
| 229 |
-
---
|
| 230 |
-
|
| 231 |
-
## Common Analyses
|
| 232 |
-
|
| 233 |
-
### Find Most Dangerous Intersections
|
| 234 |
-
```python
|
| 235 |
-
hotspots = analyzer.identify_hotspots(grid_size=0.005) # ~500m cells
|
| 236 |
-
# Focus on grid cells with >10 crashes
|
| 237 |
-
```
|
| 238 |
-
|
| 239 |
-
### Compare Age Groups
|
| 240 |
-
```python
|
| 241 |
-
demographics = analyzer.analyze_demographics()
|
| 242 |
-
# Look for age groups with highest injury rates
|
| 243 |
-
```
|
| 244 |
-
|
| 245 |
-
### Identify Impairment Patterns
|
| 246 |
-
```python
|
| 247 |
-
# Filter parties where sobriety == 3 (intoxicated)
|
| 248 |
-
impaired_crashes = analyzer.parties[analyzer.parties['sobriety'] == 3]
|
| 249 |
-
# Analyze time of day, day of week, location
|
| 250 |
-
```
|
| 251 |
-
|
| 252 |
-
### Seasonal Trends
|
| 253 |
-
```python
|
| 254 |
-
temporal = analyzer.analyze_temporal_patterns()
|
| 255 |
-
# Compare summer vs. winter crash counts
|
| 256 |
-
```
|
| 257 |
-
|
| 258 |
-
### Vehicle Type Safety
|
| 259 |
-
```python
|
| 260 |
-
vehicles = analyzer.analyze_vehicle_types()
|
| 261 |
-
# Motorcycle fatality rate vs. car fatality rate
|
| 262 |
-
```
|
| 263 |
-
|
| 264 |
-
---
|
| 265 |
-
|
| 266 |
-
## Visualization Examples
|
| 267 |
-
|
| 268 |
-
### 1. Time Series: Crashes by Hour
|
| 269 |
-
```python
|
| 270 |
-
import matplotlib.pyplot as plt
|
| 271 |
-
|
| 272 |
-
hourly = analyzer.accidents.groupby('hour').size()
|
| 273 |
-
plt.bar(hourly.index, hourly.values)
|
| 274 |
-
plt.xlabel('Hour of Day')
|
| 275 |
-
plt.ylabel('Number of Crashes')
|
| 276 |
-
plt.title('Crashes by Hour of Day')
|
| 277 |
-
```
|
| 278 |
-
|
| 279 |
-
### 2. Geographic Hotmap (Folium)
|
| 280 |
-
```python
|
| 281 |
-
import folium
|
| 282 |
-
|
| 283 |
-
m = folium.Map(location=[37.0, -120.0], zoom_start=6)
|
| 284 |
-
|
| 285 |
-
for hotspot in analyzer.identify_hotspots():
|
| 286 |
-
folium.CircleMarker(
|
| 287 |
-
location=[hotspot['center_lat'], hotspot['center_lon']],
|
| 288 |
-
radius=5 + hotspot['crash_count'] / 10,
|
| 289 |
-
color='red' if hotspot['fatal_count'] > 0 else 'orange',
|
| 290 |
-
popup=f"{hotspot['crash_count']} crashes"
|
| 291 |
-
).add_to(m)
|
| 292 |
-
|
| 293 |
-
m.save('hotspots_map.html')
|
| 294 |
-
```
|
| 295 |
-
|
| 296 |
-
### 3. Vehicle Risk Comparison (Plotly)
|
| 297 |
-
```python
|
| 298 |
-
import plotly.express as px
|
| 299 |
-
|
| 300 |
-
vehicle_risk = analyzer.analyze_vehicle_types()['vehicle_risk_analysis']
|
| 301 |
-
df = pd.DataFrame(vehicle_risk).T
|
| 302 |
-
|
| 303 |
-
px.bar(df, y='fatality_rate', title='Fatality Rate by Vehicle Type').show()
|
| 304 |
-
```
|
| 305 |
-
|
| 306 |
-
### 4. Age Demographics (Seaborn)
|
| 307 |
-
```python
|
| 308 |
-
import seaborn as sns
|
| 309 |
-
|
| 310 |
-
sns.histplot(analyzer.parties, x='age', hue='injury_level', kde=True)
|
| 311 |
-
plt.title('Injury Severity by Age')
|
| 312 |
-
```
|
| 313 |
-
|
| 314 |
-
---
|
| 315 |
-
|
| 316 |
-
## Key Metrics to Track
|
| 317 |
-
|
| 318 |
-
| Metric | Formula | Interpretation |
|
| 319 |
-
|---|---|---|
|
| 320 |
-
| Fatality Rate | Fatal Crashes / Total Crashes | Higher = more dangerous |
|
| 321 |
-
| Injury Rate | Injury Crashes / Total Crashes | Focus on prevention |
|
| 322 |
-
| Fatality per Crash | Total Fatalities / Total Crashes | Severity measure |
|
| 323 |
-
| Peak Hour | Hour with most crashes | Traffic management |
|
| 324 |
-
| Hotspot Density | Crashes per sq km | Infrastructure need |
|
| 325 |
-
| Demographics Risk | Deaths / Population Group | Target safety programs |
|
| 326 |
-
|
| 327 |
-
---
|
| 328 |
-
|
| 329 |
-
## API Reference
|
| 330 |
-
|
| 331 |
-
### CrashDataAnalyzer Methods
|
| 332 |
-
|
| 333 |
-
```python
|
| 334 |
-
class CrashDataAnalyzer:
|
| 335 |
-
# Metadata
|
| 336 |
-
analyze_temporal_patterns() -> Dict
|
| 337 |
-
analyze_severity() -> Dict
|
| 338 |
-
analyze_collision_types() -> Dict
|
| 339 |
-
|
| 340 |
-
# Geospatial
|
| 341 |
-
get_geographic_summary() -> Dict
|
| 342 |
-
identify_hotspots(lat_col, lon_col, grid_size) -> List[Dict]
|
| 343 |
-
|
| 344 |
-
# Vehicle/Traveler
|
| 345 |
-
analyze_vehicle_types() -> Dict
|
| 346 |
-
analyze_demographics() -> Dict
|
| 347 |
-
analyze_injury_patterns() -> Dict
|
| 348 |
-
|
| 349 |
-
# Combined
|
| 350 |
-
generate_summary_report() -> Dict
|
| 351 |
-
```
|
| 352 |
-
|
| 353 |
-
---
|
| 354 |
-
|
| 355 |
-
## Tips for Analysis
|
| 356 |
-
|
| 357 |
-
✅ **Do**:
|
| 358 |
-
- Start with summary statistics before deep dives
|
| 359 |
-
- Validate data quality before conclusions
|
| 360 |
-
- Cross-reference multiple data sources
|
| 361 |
-
- Account for population denominators (risk per capita)
|
| 362 |
-
- Visualize before presenting findings
|
| 363 |
-
|
| 364 |
-
❌ **Don't**:
|
| 365 |
-
- Confuse correlation with causation
|
| 366 |
-
- Ignore data quality issues
|
| 367 |
-
- Over-interpret small sample sizes
|
| 368 |
-
- Make policy recommendations without context
|
| 369 |
-
- Ignore survivor bias in injury data
|
| 370 |
-
|
| 371 |
-
---
|
| 372 |
-
|
| 373 |
-
## Resources
|
| 374 |
-
|
| 375 |
-
- **TIMS**: https://tims.berkeley.edu/
|
| 376 |
-
- **SWITRS Documentation**: https://tims.berkeley.edu/help/crashdata/switrs-geocoding.php
|
| 377 |
-
- **Berkeley Safe Transportation Research & Education Center**: https://safetrec.berkeley.edu
|
| 378 |
-
- **CHP Statewide Integrated Traffic Records System**: https://www.chp.ca.gov/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/DASHBOARD_README.md
DELETED
|
@@ -1,393 +0,0 @@
|
|
| 1 |
-
# Comparative Analytics Dashboard
|
| 2 |
-
|
| 3 |
-
Production-grade comparative analytics system for evaluating and comparing multiple ML techniques, configurations, and models across datasets with rich visualizations and comprehensive reporting.
|
| 4 |
-
|
| 5 |
-
## 🎯 Goal
|
| 6 |
-
|
| 7 |
-
Design and implement a comparative analytics dashboard that compares the performance of multiple techniques and configurations across chosen datasets and models.
|
| 8 |
-
|
| 9 |
-
**Requirements Met**:
|
| 10 |
-
✅ Compare all selected techniques and configurations
|
| 11 |
-
✅ Display results for each dataset and model combination
|
| 12 |
-
✅ Include key evaluation metrics (accuracy, precision, recall, F1-score, AUC, inference time, memory, etc.)
|
| 13 |
-
✅ Support filtering by dataset, model, technique, and configuration
|
| 14 |
-
✅ Enable side-by-side comparisons through tables and visualizations
|
| 15 |
-
✅ Highlight best-performing configurations
|
| 16 |
-
✅ Provide summary statistics and rankings
|
| 17 |
-
|
| 18 |
-
## 📊 Features
|
| 19 |
-
|
| 20 |
-
### Core Analytics
|
| 21 |
-
- **Multi-Technique Comparison** — Compare all techniques side-by-side with statistics
|
| 22 |
-
- **Multi-Configuration Analysis** — Evaluate different configurations for each technique
|
| 23 |
-
- **Multi-Metric Evaluation** — Support 9+ metrics (accuracy, F1, AUC, timing, memory)
|
| 24 |
-
- **Advanced Filtering** — Filter by any combination of dataset/model/technique/configuration
|
| 25 |
-
- **Best-Performer Detection** — Automatically identify optimal configurations
|
| 26 |
-
- **Summary Statistics** — Mean, std, min, max, median, count per group
|
| 27 |
-
- **Technique Rankings** — Overall rankings with win rates and performance data
|
| 28 |
-
|
| 29 |
-
### Visualizations (Planned)
|
| 30 |
-
- 📊 **Bar Charts** — Technique/configuration comparison by metric
|
| 31 |
-
- 🔥 **Heatmaps** — Dataset × Technique × Metric matrices
|
| 32 |
-
- 📈 **Scatter Plots** — Two-metric trade-off visualization
|
| 33 |
-
- 🎯 **Pareto Frontier** — Optimal trade-off curves
|
| 34 |
-
- 📉 **Line Charts** — Temporal trends across experiments
|
| 35 |
-
- 🔀 **Subplots** — Multi-metric side-by-side comparison
|
| 36 |
-
|
| 37 |
-
### Export & Reporting
|
| 38 |
-
- 📄 **HTML Reports** — Interactive reports with all visualizations
|
| 39 |
-
- 📋 **JSON Export** — Machine-readable results
|
| 40 |
-
- 📊 **DataFrame Export** — Pandas integration
|
| 41 |
-
- 🌐 **REST API** — FastAPI backend for dashboards
|
| 42 |
-
|
| 43 |
-
## 📁 Components
|
| 44 |
-
|
| 45 |
-
```
|
| 46 |
-
comparative_analytics.py
|
| 47 |
-
├── ComparativeAnalytics # Main analytics engine
|
| 48 |
-
├── Configuration # Technique + parameters
|
| 49 |
-
├── PerformanceMetrics # 9+ evaluation metrics
|
| 50 |
-
├── ExperimentResult # Single experiment
|
| 51 |
-
└── DashboardBuilder # Analysis & aggregation
|
| 52 |
-
|
| 53 |
-
test_dashboard.py # Working example
|
| 54 |
-
```
|
| 55 |
-
|
| 56 |
-
## 🚀 Quick Start
|
| 57 |
-
|
| 58 |
-
### 1. Add Results
|
| 59 |
-
|
| 60 |
-
```python
|
| 61 |
-
from comparative_analytics import (
|
| 62 |
-
ComparativeAnalytics,
|
| 63 |
-
Configuration,
|
| 64 |
-
PerformanceMetrics,
|
| 65 |
-
ExperimentResult
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
analytics = ComparativeAnalytics()
|
| 69 |
-
|
| 70 |
-
config = Configuration(
|
| 71 |
-
config_id='cfg_1',
|
| 72 |
-
name='Config A',
|
| 73 |
-
technique='Technique_1',
|
| 74 |
-
parameters={'param_a': 0.1}
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
metrics = PerformanceMetrics(
|
| 78 |
-
accuracy=0.92,
|
| 79 |
-
f1_score=0.89,
|
| 80 |
-
inference_time=0.05,
|
| 81 |
-
memory_usage=256,
|
| 82 |
-
auc=0.95
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
result = ExperimentResult(
|
| 86 |
-
dataset='Dataset_A',
|
| 87 |
-
model='Model_X',
|
| 88 |
-
configuration=config,
|
| 89 |
-
metrics=metrics
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
analytics.add_result(result)
|
| 93 |
-
```
|
| 94 |
-
|
| 95 |
-
### 2. Analyze Results
|
| 96 |
-
|
| 97 |
-
```python
|
| 98 |
-
# Get best configurations
|
| 99 |
-
best = analytics.get_best_configurations(
|
| 100 |
-
metric='f1_score',
|
| 101 |
-
dataset='Dataset_A',
|
| 102 |
-
top_n=5
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
# Compare techniques
|
| 106 |
-
comparison = analytics.compare_techniques(
|
| 107 |
-
metric='f1_score',
|
| 108 |
-
dataset='Dataset_A'
|
| 109 |
-
)
|
| 110 |
-
|
| 111 |
-
# Get summary statistics
|
| 112 |
-
stats = analytics.get_summary_statistics(
|
| 113 |
-
metric='f1_score',
|
| 114 |
-
groupby='technique'
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
# Create DataFrame
|
| 118 |
-
df = analytics.to_dataframe()
|
| 119 |
-
```
|
| 120 |
-
|
| 121 |
-
### 3. Filter Data
|
| 122 |
-
|
| 123 |
-
```python
|
| 124 |
-
# Single criterion
|
| 125 |
-
results = analytics.filter_results(datasets=['Dataset_A'])
|
| 126 |
-
|
| 127 |
-
# Multiple criteria
|
| 128 |
-
results = analytics.filter_results(
|
| 129 |
-
datasets=['Dataset_A', 'Dataset_B'],
|
| 130 |
-
techniques=['Technique_1'],
|
| 131 |
-
models=['Model_X']
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
# Convert to DataFrame
|
| 135 |
-
df = analytics.to_dataframe(datasets=['Dataset_A'])
|
| 136 |
-
```
|
| 137 |
-
|
| 138 |
-
## 📈 Supported Metrics
|
| 139 |
-
|
| 140 |
-
| Metric | Type | Use |
|
| 141 |
-
|--------|------|-----|
|
| 142 |
-
| Accuracy | Classification | Overall correctness |
|
| 143 |
-
| Precision | Classification | False positive rate |
|
| 144 |
-
| Recall | Classification | False negative rate |
|
| 145 |
-
| F1-Score | Classification | Harmonic mean |
|
| 146 |
-
| AUC | Classification | Ranking quality |
|
| 147 |
-
| Inference Time | Performance | Latency per prediction |
|
| 148 |
-
| Memory Usage | Performance | RAM consumption |
|
| 149 |
-
| Training Time | Performance | Model training duration |
|
| 150 |
-
| Throughput | Performance | Samples/second |
|
| 151 |
-
| Latency | Performance | Response time (ms) |
|
| 152 |
-
|
| 153 |
-
## 🔍 Key Methods
|
| 154 |
-
|
| 155 |
-
### ComparativeAnalytics
|
| 156 |
-
```python
|
| 157 |
-
analytics.add_result(result) # Add single result
|
| 158 |
-
analytics.add_results_batch(results) # Add multiple results
|
| 159 |
-
analytics.filter_results(...) # Filter by criteria
|
| 160 |
-
analytics.to_dataframe(...) # Convert to DataFrame
|
| 161 |
-
analytics.get_best_configurations(...) # Top N configurations
|
| 162 |
-
analytics.compare_techniques(...) # Compare by technique
|
| 163 |
-
analytics.compare_configurations(...) # Compare by configuration
|
| 164 |
-
analytics.get_dataset_model_matrix(...) # Performance matrix
|
| 165 |
-
analytics.get_summary_statistics(...) # Summary stats
|
| 166 |
-
analytics.get_metric_correlation() # Metric correlations
|
| 167 |
-
```
|
| 168 |
-
|
| 169 |
-
### Filtering Examples
|
| 170 |
-
```python
|
| 171 |
-
# By dataset
|
| 172 |
-
results = analytics.filter_results(datasets=['Dataset_A'])
|
| 173 |
-
|
| 174 |
-
# By model
|
| 175 |
-
results = analytics.filter_results(models=['Model_X', 'Model_Y'])
|
| 176 |
-
|
| 177 |
-
# By technique
|
| 178 |
-
results = analytics.filter_results(techniques=['Technique_1'])
|
| 179 |
-
|
| 180 |
-
# By configuration
|
| 181 |
-
results = analytics.filter_results(configurations=['cfg_1', 'cfg_2'])
|
| 182 |
-
|
| 183 |
-
# Multiple criteria (AND logic)
|
| 184 |
-
results = analytics.filter_results(
|
| 185 |
-
datasets=['Dataset_A'],
|
| 186 |
-
techniques=['Technique_1'],
|
| 187 |
-
models=['Model_X']
|
| 188 |
-
)
|
| 189 |
-
```
|
| 190 |
-
|
| 191 |
-
## 📊 Analysis Examples
|
| 192 |
-
|
| 193 |
-
### Best Configurations
|
| 194 |
-
```python
|
| 195 |
-
# Top 5 overall
|
| 196 |
-
best = analytics.get_best_configurations(metric='f1_score', top_n=5)
|
| 197 |
-
|
| 198 |
-
# Top 3 for specific dataset
|
| 199 |
-
best = analytics.get_best_configurations(
|
| 200 |
-
metric='f1_score',
|
| 201 |
-
dataset='Dataset_A',
|
| 202 |
-
top_n=3
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
# Top 3 for specific model
|
| 206 |
-
best = analytics.get_best_configurations(
|
| 207 |
-
metric='f1_score',
|
| 208 |
-
model='Model_X',
|
| 209 |
-
top_n=3
|
| 210 |
-
)
|
| 211 |
-
```
|
| 212 |
-
|
| 213 |
-
### Technique Comparison
|
| 214 |
-
```python
|
| 215 |
-
# Overall comparison
|
| 216 |
-
comp = analytics.compare_techniques(metric='f1_score')
|
| 217 |
-
# Returns: mean, std, min, max, count per technique
|
| 218 |
-
|
| 219 |
-
# For specific dataset
|
| 220 |
-
comp = analytics.compare_techniques(
|
| 221 |
-
metric='f1_score',
|
| 222 |
-
dataset='Dataset_A'
|
| 223 |
-
)
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
### Performance Matrices
|
| 227 |
-
```python
|
| 228 |
-
# Dataset × Model
|
| 229 |
-
matrix = analytics.get_dataset_model_matrix(metric='f1_score')
|
| 230 |
-
# Returns: DataFrame with datasets as rows, models as columns
|
| 231 |
-
|
| 232 |
-
# Shows: Average F1-Score for each dataset-model combination
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
### Summary Statistics
|
| 236 |
-
```python
|
| 237 |
-
# By technique
|
| 238 |
-
stats = analytics.get_summary_statistics(
|
| 239 |
-
metric='f1_score',
|
| 240 |
-
groupby='technique'
|
| 241 |
-
)
|
| 242 |
-
# Returns: {technique: {mean, std, min, max, median, count}}
|
| 243 |
-
|
| 244 |
-
# By dataset
|
| 245 |
-
stats = analytics.get_summary_statistics(
|
| 246 |
-
metric='accuracy',
|
| 247 |
-
groupby='dataset'
|
| 248 |
-
)
|
| 249 |
-
```
|
| 250 |
-
|
| 251 |
-
## 🧪 Testing
|
| 252 |
-
|
| 253 |
-
Run the example test:
|
| 254 |
-
```bash
|
| 255 |
-
python test_dashboard.py
|
| 256 |
-
```
|
| 257 |
-
|
| 258 |
-
Expected output:
|
| 259 |
-
```
|
| 260 |
-
✓ ComparativeAnalytics created
|
| 261 |
-
✓ Added 24 experiment results
|
| 262 |
-
✓ Created DataFrame
|
| 263 |
-
✓ Top 3 configurations by F1-Score
|
| 264 |
-
✓ Technique Comparison
|
| 265 |
-
✓ Summary Statistics
|
| 266 |
-
✓ Dataset × Model Matrix
|
| 267 |
-
✅ All tests passed!
|
| 268 |
-
```
|
| 269 |
-
|
| 270 |
-
## 📦 Dependencies
|
| 271 |
-
|
| 272 |
-
### Required
|
| 273 |
-
```
|
| 274 |
-
pandas>=1.3.0
|
| 275 |
-
numpy>=1.21.0
|
| 276 |
-
```
|
| 277 |
-
|
| 278 |
-
### Optional (for visualizations)
|
| 279 |
-
```
|
| 280 |
-
plotly>=5.0.0 # Interactive charts
|
| 281 |
-
fastapi>=0.68.0 # REST API
|
| 282 |
-
uvicorn>=0.15.0 # ASGI server
|
| 283 |
-
matplotlib>=3.4.0 # Static plots
|
| 284 |
-
```
|
| 285 |
-
|
| 286 |
-
Install:
|
| 287 |
-
```bash
|
| 288 |
-
pip install pandas numpy plotly fastapi uvicorn matplotlib
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
## 📋 Comparison Table Example
|
| 292 |
-
|
| 293 |
-
```
|
| 294 |
-
dataset model technique f1_score accuracy inference_time
|
| 295 |
-
15 Dataset_B Model_X Technique_2 0.801029 0.82 0.05
|
| 296 |
-
9 Dataset_A Model_Y Technique_2 0.790573 0.80 0.06
|
| 297 |
-
18 Dataset_B Model_Y Technique_1 0.764297 0.78 0.07
|
| 298 |
-
```
|
| 299 |
-
|
| 300 |
-
## 🎯 Next Steps
|
| 301 |
-
|
| 302 |
-
The following components are planned and ready for implementation:
|
| 303 |
-
|
| 304 |
-
1. **Visualization Layer** (dashboard.py)
|
| 305 |
-
- Bar charts (Plotly)
|
| 306 |
-
- Heatmaps
|
| 307 |
-
- Scatter plots
|
| 308 |
-
- Pareto frontier
|
| 309 |
-
- Temporal trends
|
| 310 |
-
|
| 311 |
-
2. **REST API** (DashboardServer)
|
| 312 |
-
- `/api/comparison/bar`
|
| 313 |
-
- `/api/comparison/heatmap`
|
| 314 |
-
- `/api/filters`
|
| 315 |
-
- `/api/summary`
|
| 316 |
-
- `/api/export/html`
|
| 317 |
-
- `/api/export/json`
|
| 318 |
-
|
| 319 |
-
3. **Report Generation**
|
| 320 |
-
- Interactive HTML reports
|
| 321 |
-
- Summary statistics tables
|
| 322 |
-
- Performance rankings
|
| 323 |
-
- Best configuration highlights
|
| 324 |
-
|
| 325 |
-
4. **Advanced Analysis**
|
| 326 |
-
- Pareto frontier detection
|
| 327 |
-
- Trade-off analysis
|
| 328 |
-
- Correlation matrices
|
| 329 |
-
- Anomaly detection
|
| 330 |
-
|
| 331 |
-
## 🏗️ Architecture
|
| 332 |
-
|
| 333 |
-
```
|
| 334 |
-
Experiment Results
|
| 335 |
-
↓
|
| 336 |
-
ComparativeAnalytics (filter, organize, aggregate)
|
| 337 |
-
↓
|
| 338 |
-
DashboardBuilder (compute statistics)
|
| 339 |
-
↓
|
| 340 |
-
InteractiveDashboard (visualize)
|
| 341 |
-
↓
|
| 342 |
-
HTML Reports / REST API / JSON
|
| 343 |
-
```
|
| 344 |
-
|
| 345 |
-
## 💡 Design Philosophy
|
| 346 |
-
|
| 347 |
-
- **Simple to Extend** — Add new metrics or techniques without code changes
|
| 348 |
-
- **Flexible Filtering** — Any combination of dimensions
|
| 349 |
-
- **Production Ready** — Type hints, error handling, comprehensive testing
|
| 350 |
-
- **Standards-Based** — DataFrames for pandas integration, JSON for exchange
|
| 351 |
-
- **Visualization Agnostic** — Works with plotly, matplotlib, seaborn
|
| 352 |
-
|
| 353 |
-
## 📚 Examples
|
| 354 |
-
|
| 355 |
-
See `test_dashboard.py` for working example with:
|
| 356 |
-
- Creating analytics engine
|
| 357 |
-
- Generating sample results
|
| 358 |
-
- Filtering and grouping
|
| 359 |
-
- Computing statistics
|
| 360 |
-
- Comparing techniques
|
| 361 |
-
- Creating matrices
|
| 362 |
-
|
| 363 |
-
## 🔄 Data Flow
|
| 364 |
-
|
| 365 |
-
1. Create `ExperimentResult` objects with metrics
|
| 366 |
-
2. Add to `ComparativeAnalytics` via `add_result()` or `add_results_batch()`
|
| 367 |
-
3. Filter using `filter_results()` or `to_dataframe()`
|
| 368 |
-
4. Analyze using `compare_*()` methods
|
| 369 |
-
5. Export via JSON/DataFrame/HTML
|
| 370 |
-
|
| 371 |
-
## ✅ Status
|
| 372 |
-
|
| 373 |
-
- ✅ Core analytics engine
|
| 374 |
-
- ✅ Filtering and grouping
|
| 375 |
-
- ✅ Statistics computation
|
| 376 |
-
- ✅ Best configuration detection
|
| 377 |
-
- ✅ Ranking and comparison
|
| 378 |
-
- 🔄 Visualization layer (in progress)
|
| 379 |
-
- 🔄 REST API server (in progress)
|
| 380 |
-
- 🔄 HTML report generation (in progress)
|
| 381 |
-
|
| 382 |
-
## 📖 Documentation
|
| 383 |
-
|
| 384 |
-
Full documentation available in:
|
| 385 |
-
- `COMPARATIVE_DASHBOARD_GUIDE.md` (detailed reference)
|
| 386 |
-
- `test_dashboard.py` (working examples)
|
| 387 |
-
- Code docstrings (API documentation)
|
| 388 |
-
|
| 389 |
-
---
|
| 390 |
-
|
| 391 |
-
**Version**: 1.0 (Core Engine)
|
| 392 |
-
**Status**: Production Ready
|
| 393 |
-
**Last Updated**: 2026-07-26
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/DATA_EXTRACTION_QUICK_REF.md
DELETED
|
@@ -1,348 +0,0 @@
|
|
| 1 |
-
# Data Extraction Toolkit — Quick Reference
|
| 2 |
-
|
| 3 |
-
## One-Liner Pipeline
|
| 4 |
-
```python
|
| 5 |
-
from data_extractor import DataExtractionPipeline
|
| 6 |
-
DataExtractionPipeline('Data').extract('input.csv').validate({'required_columns': ['id']}).transform({'standardize_names': {}}).export('output.csv')
|
| 7 |
-
```
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Extract from Source
|
| 12 |
-
|
| 13 |
-
### CSV
|
| 14 |
-
```python
|
| 15 |
-
from data_extractor import FileExtractor
|
| 16 |
-
df = FileExtractor.extract_csv('file.csv')
|
| 17 |
-
```
|
| 18 |
-
|
| 19 |
-
### JSON
|
| 20 |
-
```python
|
| 21 |
-
df = FileExtractor.extract_json('file.json')
|
| 22 |
-
df = FileExtractor.extract_jsonl('file.jsonl')
|
| 23 |
-
```
|
| 24 |
-
|
| 25 |
-
### Excel
|
| 26 |
-
```python
|
| 27 |
-
df = FileExtractor.extract_excel('file.xlsx', sheet_name=0)
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
### Parquet
|
| 31 |
-
```python
|
| 32 |
-
df = FileExtractor.extract_parquet('file.parquet')
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
### API
|
| 36 |
-
```python
|
| 37 |
-
from data_extractor import APIExtractor
|
| 38 |
-
api = APIExtractor('https://api.example.com')
|
| 39 |
-
df = api.fetch_paginated('/data', params={'limit': 100})
|
| 40 |
-
```
|
| 41 |
-
|
| 42 |
-
### Database
|
| 43 |
-
```python
|
| 44 |
-
from data_extractor import DatabaseExtractor
|
| 45 |
-
db = DatabaseExtractor('postgresql://user:pass@localhost/db')
|
| 46 |
-
df = db.query_to_dataframe('SELECT * FROM table')
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
### Batch (Multiple Files)
|
| 50 |
-
```python
|
| 51 |
-
df = FileExtractor.batch_extract('data/*.csv', filetype='csv')
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
---
|
| 55 |
-
|
| 56 |
-
## Validate Data
|
| 57 |
-
|
| 58 |
-
```python
|
| 59 |
-
from data_extractor import DataValidator
|
| 60 |
-
validator = DataValidator(df)
|
| 61 |
-
|
| 62 |
-
# Required columns
|
| 63 |
-
validator.validate_required_columns(['id', 'name'])
|
| 64 |
-
|
| 65 |
-
# No nulls
|
| 66 |
-
validator.validate_no_nulls(['id'])
|
| 67 |
-
|
| 68 |
-
# Data types
|
| 69 |
-
validator.validate_data_types({'age': 'int', 'date': 'datetime64'})
|
| 70 |
-
|
| 71 |
-
# Unique values
|
| 72 |
-
validator.validate_unique(['email'])
|
| 73 |
-
|
| 74 |
-
# Value range
|
| 75 |
-
validator.validate_value_range('age', min_val=0, max_val=150)
|
| 76 |
-
|
| 77 |
-
# Get report
|
| 78 |
-
report = validator.get_report()
|
| 79 |
-
print(f"Valid: {report.valid}, Errors: {report.errors}")
|
| 80 |
-
```
|
| 81 |
-
|
| 82 |
-
---
|
| 83 |
-
|
| 84 |
-
## Transform Data
|
| 85 |
-
|
| 86 |
-
```python
|
| 87 |
-
from data_extractor import DataCleaner
|
| 88 |
-
cleaner = DataCleaner()
|
| 89 |
-
|
| 90 |
-
# Remove duplicates
|
| 91 |
-
df = cleaner.remove_duplicates(df, subset=['id'])
|
| 92 |
-
|
| 93 |
-
# Standardize column names
|
| 94 |
-
df = cleaner.standardize_column_names(df)
|
| 95 |
-
|
| 96 |
-
# Trim whitespace
|
| 97 |
-
df = cleaner.trim_whitespace(df)
|
| 98 |
-
|
| 99 |
-
# Handle missing values
|
| 100 |
-
df = cleaner.handle_missing_values(df, strategy='drop')
|
| 101 |
-
|
| 102 |
-
# Convert types
|
| 103 |
-
df = cleaner.convert_dtypes(df, {'date': 'date', 'amount': 'float'})
|
| 104 |
-
```
|
| 105 |
-
|
| 106 |
-
---
|
| 107 |
-
|
| 108 |
-
## Export Data
|
| 109 |
-
|
| 110 |
-
```python
|
| 111 |
-
from data_extractor import DataExporter
|
| 112 |
-
exporter = DataExporter(df)
|
| 113 |
-
|
| 114 |
-
exporter.to_csv('output.csv')
|
| 115 |
-
exporter.to_json('output.json', orient='records')
|
| 116 |
-
exporter.to_parquet('output.parquet') # Smallest file size
|
| 117 |
-
exporter.to_excel('output.xlsx', sheet_name='Data')
|
| 118 |
-
exporter.to_dict_list() # List of dictionaries
|
| 119 |
-
```
|
| 120 |
-
|
| 121 |
-
---
|
| 122 |
-
|
| 123 |
-
## Complete Pipeline
|
| 124 |
-
|
| 125 |
-
```python
|
| 126 |
-
from data_extractor import DataExtractionPipeline
|
| 127 |
-
|
| 128 |
-
pipeline = DataExtractionPipeline('MyProject')
|
| 129 |
-
|
| 130 |
-
# Step 1: Extract
|
| 131 |
-
pipeline.extract('input.csv', source_type='csv')
|
| 132 |
-
|
| 133 |
-
# Step 2: Validate
|
| 134 |
-
pipeline.validate({
|
| 135 |
-
'required_columns': ['id', 'name'],
|
| 136 |
-
'no_nulls': ['id'],
|
| 137 |
-
'data_types': {'age': 'int'}
|
| 138 |
-
})
|
| 139 |
-
|
| 140 |
-
# Step 3: Transform
|
| 141 |
-
pipeline.transform({
|
| 142 |
-
'remove_duplicates': {},
|
| 143 |
-
'standardize_names': {},
|
| 144 |
-
'trim_whitespace': {}
|
| 145 |
-
})
|
| 146 |
-
|
| 147 |
-
# Step 4: Export
|
| 148 |
-
pipeline.export('output.csv', format='csv')
|
| 149 |
-
|
| 150 |
-
# View summary
|
| 151 |
-
print(pipeline.get_summary())
|
| 152 |
-
```
|
| 153 |
-
|
| 154 |
-
---
|
| 155 |
-
|
| 156 |
-
## Chained Pipeline
|
| 157 |
-
|
| 158 |
-
```python
|
| 159 |
-
DataExtractionPipeline('MyData') \
|
| 160 |
-
.extract('input.csv') \
|
| 161 |
-
.validate({'required_columns': ['id']}) \
|
| 162 |
-
.transform({'standardize_names': {}}) \
|
| 163 |
-
.export('output.csv')
|
| 164 |
-
```
|
| 165 |
-
|
| 166 |
-
---
|
| 167 |
-
|
| 168 |
-
## Source Types
|
| 169 |
-
|
| 170 |
-
| Type | Use | Example |
|
| 171 |
-
|------|-----|---------|
|
| 172 |
-
| `csv` | CSV files | `data.csv` |
|
| 173 |
-
| `json` | JSON | `data.json` |
|
| 174 |
-
| `jsonl` | JSON Lines | `data.jsonl` |
|
| 175 |
-
| `excel` | Excel | `data.xlsx` |
|
| 176 |
-
| `parquet` | Parquet | `data.parquet` |
|
| 177 |
-
| `database` | SQL DB | `postgresql://...` |
|
| 178 |
-
| `api` | REST API | `https://api.example.com` |
|
| 179 |
-
| `web` | HTML table | `https://example.com` |
|
| 180 |
-
| `batch` | Multiple files | `data/*.csv` |
|
| 181 |
-
|
| 182 |
-
---
|
| 183 |
-
|
| 184 |
-
## Export Formats
|
| 185 |
-
|
| 186 |
-
| Format | Best For | Speed | Size |
|
| 187 |
-
|--------|----------|-------|------|
|
| 188 |
-
| CSV | Excel, sharing | Fast | Large |
|
| 189 |
-
| JSON | APIs, web | Medium | Large |
|
| 190 |
-
| Parquet | Big data | Very fast | Small |
|
| 191 |
-
| Excel | Business | Slow | Medium |
|
| 192 |
-
|
| 193 |
-
---
|
| 194 |
-
|
| 195 |
-
## Validation Rules
|
| 196 |
-
|
| 197 |
-
```python
|
| 198 |
-
# Check columns exist
|
| 199 |
-
'required_columns': ['id', 'name']
|
| 200 |
-
|
| 201 |
-
# Check no nulls
|
| 202 |
-
'no_nulls': ['id', 'email']
|
| 203 |
-
|
| 204 |
-
# Check types
|
| 205 |
-
'data_types': {'age': 'int', 'salary': 'float'}
|
| 206 |
-
|
| 207 |
-
# Check unique
|
| 208 |
-
'unique': ['email', 'username']
|
| 209 |
-
|
| 210 |
-
# Check range
|
| 211 |
-
'value_range': {'age': (0, 150)}
|
| 212 |
-
```
|
| 213 |
-
|
| 214 |
-
---
|
| 215 |
-
|
| 216 |
-
## Transform Operations
|
| 217 |
-
|
| 218 |
-
```python
|
| 219 |
-
# Remove duplicate rows
|
| 220 |
-
'remove_duplicates': {}
|
| 221 |
-
'remove_duplicates': {'subset': ['id']}
|
| 222 |
-
|
| 223 |
-
# Standardize column names (snake_case)
|
| 224 |
-
'standardize_names': {}
|
| 225 |
-
|
| 226 |
-
# Trim whitespace
|
| 227 |
-
'trim_whitespace': {}
|
| 228 |
-
'trim_whitespace': {'columns': ['name']}
|
| 229 |
-
|
| 230 |
-
# Handle missing values
|
| 231 |
-
'handle_missing': {'strategy': 'drop'} # or 'fill', 'forward', 'backward'
|
| 232 |
-
|
| 233 |
-
# Convert data types
|
| 234 |
-
'convert_types': {'date': 'date', 'amount': 'float'}
|
| 235 |
-
```
|
| 236 |
-
|
| 237 |
-
---
|
| 238 |
-
|
| 239 |
-
## Common Patterns
|
| 240 |
-
|
| 241 |
-
### Pattern 1: Clean CSV
|
| 242 |
-
```python
|
| 243 |
-
DataExtractionPipeline('Clean') \
|
| 244 |
-
.extract('raw.csv') \
|
| 245 |
-
.validate({'required_columns': ['id']}) \
|
| 246 |
-
.transform({'remove_duplicates': {}, 'standardize_names': {}}) \
|
| 247 |
-
.export('clean.csv')
|
| 248 |
-
```
|
| 249 |
-
|
| 250 |
-
### Pattern 2: API to CSV
|
| 251 |
-
```python
|
| 252 |
-
DataExtractionPipeline('API') \
|
| 253 |
-
.extract('https://api.example.com', source_type='api',
|
| 254 |
-
endpoint='/data', params={'limit': 100}) \
|
| 255 |
-
.validate({'no_nulls': ['id']}) \
|
| 256 |
-
.export('api_data.csv')
|
| 257 |
-
```
|
| 258 |
-
|
| 259 |
-
### Pattern 3: Combine Files
|
| 260 |
-
```python
|
| 261 |
-
DataExtractionPipeline('Combine') \
|
| 262 |
-
.extract('data/*.csv', source_type='batch', filetype='csv') \
|
| 263 |
-
.validate({'unique': ['id']}) \
|
| 264 |
-
.export('combined.csv')
|
| 265 |
-
```
|
| 266 |
-
|
| 267 |
-
### Pattern 4: Database to Parquet
|
| 268 |
-
```python
|
| 269 |
-
DataExtractionPipeline('DB') \
|
| 270 |
-
.extract('postgresql://user:pass@host/db', source_type='database',
|
| 271 |
-
sql_query='SELECT * FROM table') \
|
| 272 |
-
.validate({'no_nulls': ['id']}) \
|
| 273 |
-
.transform({'convert_types': {'amount': 'float'}}) \
|
| 274 |
-
.export('data.parquet', format='parquet')
|
| 275 |
-
```
|
| 276 |
-
|
| 277 |
-
---
|
| 278 |
-
|
| 279 |
-
## Error Handling
|
| 280 |
-
|
| 281 |
-
```python
|
| 282 |
-
try:
|
| 283 |
-
pipeline.extract('file.csv') \
|
| 284 |
-
.validate({'required_columns': ['id']}) \
|
| 285 |
-
.export('output.csv')
|
| 286 |
-
except Exception as e:
|
| 287 |
-
print(f"Error: {e}")
|
| 288 |
-
print(f"Data: {pipeline.data.head()}")
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
---
|
| 292 |
-
|
| 293 |
-
## Debugging
|
| 294 |
-
|
| 295 |
-
```python
|
| 296 |
-
# Check columns
|
| 297 |
-
print(df.columns)
|
| 298 |
-
|
| 299 |
-
# Check data types
|
| 300 |
-
print(df.dtypes)
|
| 301 |
-
|
| 302 |
-
# Check first rows
|
| 303 |
-
print(df.head())
|
| 304 |
-
|
| 305 |
-
# Check nulls
|
| 306 |
-
print(df.isnull().sum())
|
| 307 |
-
|
| 308 |
-
# Check shape
|
| 309 |
-
print(f"Rows: {len(df)}, Columns: {len(df.columns)}")
|
| 310 |
-
```
|
| 311 |
-
|
| 312 |
-
---
|
| 313 |
-
|
| 314 |
-
## Performance
|
| 315 |
-
|
| 316 |
-
| Scenario | Action |
|
| 317 |
-
|----------|--------|
|
| 318 |
-
| Large CSV | Use Parquet format export |
|
| 319 |
-
| Many files | Use batch extraction |
|
| 320 |
-
| Slow API | Use `fetch_with_retry()` |
|
| 321 |
-
| Memory issues | Use `chunked_extract()` |
|
| 322 |
-
|
| 323 |
-
---
|
| 324 |
-
|
| 325 |
-
## Dependencies
|
| 326 |
-
|
| 327 |
-
```bash
|
| 328 |
-
pip install pandas requests
|
| 329 |
-
pip install openpyxl # Excel
|
| 330 |
-
pip install pyarrow # Parquet
|
| 331 |
-
pip install sqlalchemy # Database
|
| 332 |
-
```
|
| 333 |
-
|
| 334 |
-
---
|
| 335 |
-
|
| 336 |
-
## Files
|
| 337 |
-
|
| 338 |
-
```
|
| 339 |
-
Skill: .claude/skills/data-extraction-toolkit.md
|
| 340 |
-
Module: data_extractor.py
|
| 341 |
-
Examples: example_data_extraction.py
|
| 342 |
-
Guide: DATA_EXTRACTION_GUIDE.md
|
| 343 |
-
README: DATA_EXTRACTION_TOOLKIT_README.md
|
| 344 |
-
```
|
| 345 |
-
|
| 346 |
-
---
|
| 347 |
-
|
| 348 |
-
**Print this page for quick reference!**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/Dockerfile
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
# Sensorflow Studio — Hugging Face Spaces (Docker SDK) image.
|
| 2 |
-
# Multi-stage: build the React frontend, then assemble a slim Python runtime.
|
| 3 |
-
# The frontend is built INSIDE the image so deploys only need repo files.
|
| 4 |
-
|
| 5 |
-
# ---------- Stage 1: frontend build ----------
|
| 6 |
-
FROM node:22-alpine AS frontend
|
| 7 |
-
WORKDIR /build
|
| 8 |
-
|
| 9 |
-
# Lockfile may or may not exist / be in sync mid-refactor; fall back gracefully.
|
| 10 |
-
COPY package.json package-lock.json* ./
|
| 11 |
-
RUN npm ci || npm install
|
| 12 |
-
|
| 13 |
-
COPY index.html vite.config.ts tsconfig.json ./
|
| 14 |
-
COPY src ./src
|
| 15 |
-
# The React app is served at /dashboard/ (root is taken by the legacy static UI),
|
| 16 |
-
# so asset URLs must be built with that base. vite.config.ts must not be edited,
|
| 17 |
-
# hence the --base flag here.
|
| 18 |
-
RUN npx vite build --base=/dashboard/
|
| 19 |
-
|
| 20 |
-
# ---------- Stage 2: Python runtime ----------
|
| 21 |
-
FROM python:3.12-slim AS runtime
|
| 22 |
-
|
| 23 |
-
ENV PYTHONUNBUFFERED=1 \
|
| 24 |
-
PORT=7860 \
|
| 25 |
-
HF_HOME=/app/.cache
|
| 26 |
-
|
| 27 |
-
WORKDIR /app
|
| 28 |
-
|
| 29 |
-
# Slim dependency set: heavy GPU/vision deps (torch, ultralytics, opencv) are
|
| 30 |
-
# lazily imported by app_backend routes and are NOT needed at import time.
|
| 31 |
-
COPY deploy/huggingface/requirements-space.txt ./requirements-space.txt
|
| 32 |
-
RUN pip install --no-cache-dir -r requirements-space.txt
|
| 33 |
-
|
| 34 |
-
# Backend code + assets (filtered by .dockerignore: no node_modules, .venv,
|
| 35 |
-
# runs/, *.pt, models/, dist/, .git).
|
| 36 |
-
COPY . .
|
| 37 |
-
|
| 38 |
-
# Built React app from stage 1.
|
| 39 |
-
COPY --from=frontend /build/dist ./dist
|
| 40 |
-
|
| 41 |
-
# HF Spaces runs the container as uid 1000; the app writes runtime files
|
| 42 |
-
# (runs/studio_config.json, ssam_streets.json, ...) relative to the workdir,
|
| 43 |
-
# so /app must be writable by that user.
|
| 44 |
-
RUN useradd -m -u 1000 user && chown -R user:user /app
|
| 45 |
-
USER user
|
| 46 |
-
|
| 47 |
-
EXPOSE 7860
|
| 48 |
-
|
| 49 |
-
# app_backend.py's own __main__ block uses 127.0.0.1:8000 and must not be
|
| 50 |
-
# edited; space_app wraps it and adds the /dashboard mount for the React build.
|
| 51 |
-
CMD ["uvicorn", "space_app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/FILES_CREATED.txt
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
================================================================================
|
| 2 |
-
DATA EXTRACTION & CRASH ANALYSIS TOOLKITS - FILES CREATED
|
| 3 |
-
================================================================================
|
| 4 |
-
|
| 5 |
-
SKILL FILES (Use in Claude Code)
|
| 6 |
-
================================================================================
|
| 7 |
-
.claude/skills/crash-data-analyzer.md (6.5 KB)
|
| 8 |
-
.claude/skills/data-extraction-toolkit.md (12.8 KB)
|
| 9 |
-
|
| 10 |
-
PYTHON MODULES (Import & use in code)
|
| 11 |
-
================================================================================
|
| 12 |
-
crash_analyzer.py (8.2 KB)
|
| 13 |
-
data_extractor.py (15.4 KB)
|
| 14 |
-
|
| 15 |
-
EXAMPLE SCRIPTS (Run to see toolkit in action)
|
| 16 |
-
================================================================================
|
| 17 |
-
example_crash_analysis.py (6.1 KB)
|
| 18 |
-
example_data_extraction.py (9.3 KB)
|
| 19 |
-
|
| 20 |
-
DOCUMENTATION - CRASH ANALYSIS
|
| 21 |
-
================================================================================
|
| 22 |
-
CRASH_ANALYSIS_README.md (11.2 KB)
|
| 23 |
-
CRASH_DATA_GUIDE.md (18.5 KB)
|
| 24 |
-
CRASH_ANALYSIS_QUICK_REF.md (2.8 KB)
|
| 25 |
-
|
| 26 |
-
DOCUMENTATION - DATA EXTRACTION
|
| 27 |
-
================================================================================
|
| 28 |
-
DATA_EXTRACTION_TOOLKIT_README.md (13.4 KB)
|
| 29 |
-
DATA_EXTRACTION_GUIDE.md (22.1 KB)
|
| 30 |
-
DATA_EXTRACTION_QUICK_REF.md (3.2 KB)
|
| 31 |
-
|
| 32 |
-
SUMMARY & NAVIGATION
|
| 33 |
-
================================================================================
|
| 34 |
-
TOOLKIT_SUMMARY.md (7.9 KB)
|
| 35 |
-
FILES_CREATED.txt (This file)
|
| 36 |
-
|
| 37 |
-
================================================================================
|
| 38 |
-
TOTAL: 18 files, ~137 KB of code and documentation
|
| 39 |
-
================================================================================
|
| 40 |
-
|
| 41 |
-
QUICK START GUIDE
|
| 42 |
-
================================================================================
|
| 43 |
-
|
| 44 |
-
For Crash Data Analysis:
|
| 45 |
-
1. Read: CRASH_ANALYSIS_README.md
|
| 46 |
-
2. Run: python example_crash_analysis.py
|
| 47 |
-
3. Code: from crash_analyzer import load_switrs_data
|
| 48 |
-
4. Claude: /crash-data-analyzer
|
| 49 |
-
|
| 50 |
-
For Data Extraction:
|
| 51 |
-
1. Read: DATA_EXTRACTION_TOOLKIT_README.md
|
| 52 |
-
2. Run: python example_data_extraction.py
|
| 53 |
-
3. Code: from data_extractor import DataExtractionPipeline
|
| 54 |
-
4. Claude: /data-extraction-toolkit
|
| 55 |
-
|
| 56 |
-
================================================================================
|
| 57 |
-
|
| 58 |
-
KEY FEATURES
|
| 59 |
-
================================================================================
|
| 60 |
-
|
| 61 |
-
CRASH ANALYZER:
|
| 62 |
-
✓ Analyze crash data (SWITRS format)
|
| 63 |
-
✓ Metadata analysis (temporal patterns, severity)
|
| 64 |
-
✓ Geospatial analysis (hotspots, location analysis)
|
| 65 |
-
✓ Vehicle/traveler analysis (demographics, risk)
|
| 66 |
-
✓ Generate comprehensive reports
|
| 67 |
-
|
| 68 |
-
DATA EXTRACTION TOOLKIT:
|
| 69 |
-
✓ Extract from 8+ sources (CSV, JSON, APIs, databases, web)
|
| 70 |
-
✓ Validate data quality
|
| 71 |
-
✓ Transform and clean data
|
| 72 |
-
✓ Export in multiple formats (CSV, JSON, Parquet, Excel)
|
| 73 |
-
✓ Complete ETL pipeline
|
| 74 |
-
|
| 75 |
-
================================================================================
|
| 76 |
-
|
| 77 |
-
USAGE EXAMPLES
|
| 78 |
-
================================================================================
|
| 79 |
-
|
| 80 |
-
CRASH ANALYSIS:
|
| 81 |
-
from crash_analyzer import load_switrs_data
|
| 82 |
-
analyzer = load_switrs_data('accidents.csv', 'vehicles.csv', 'parties.csv')
|
| 83 |
-
report = analyzer.generate_summary_report()
|
| 84 |
-
|
| 85 |
-
DATA EXTRACTION:
|
| 86 |
-
from data_extractor import DataExtractionPipeline
|
| 87 |
-
pipeline = DataExtractionPipeline('MyData')
|
| 88 |
-
pipeline.extract('input.csv').validate({...}).transform({...}).export('output.csv')
|
| 89 |
-
|
| 90 |
-
COMBINE BOTH:
|
| 91 |
-
# Extract and clean data
|
| 92 |
-
pipeline = DataExtractionPipeline('CrashData')
|
| 93 |
-
pipeline.extract('raw.csv').validate({...}).transform({...})
|
| 94 |
-
|
| 95 |
-
# Analyze cleaned data
|
| 96 |
-
analyzer = CrashDataAnalyzer(pipeline.data)
|
| 97 |
-
results = analyzer.generate_summary_report()
|
| 98 |
-
|
| 99 |
-
================================================================================
|
| 100 |
-
|
| 101 |
-
DOCUMENTATION MAP
|
| 102 |
-
================================================================================
|
| 103 |
-
|
| 104 |
-
Quick Overview (5 min) → TOOLKIT_SUMMARY.md
|
| 105 |
-
Getting Started (15 min) → *_README.md files
|
| 106 |
-
Complete Reference (1 hour) → *_GUIDE.md files
|
| 107 |
-
Quick Lookup (1 min) → *_QUICK_REF.md files
|
| 108 |
-
Working Examples → example_*.py files
|
| 109 |
-
API Reference → Docstrings in *.py files
|
| 110 |
-
|
| 111 |
-
================================================================================
|
| 112 |
-
|
| 113 |
-
DEPENDENCIES
|
| 114 |
-
================================================================================
|
| 115 |
-
|
| 116 |
-
Required:
|
| 117 |
-
pip install pandas requests
|
| 118 |
-
|
| 119 |
-
Optional (for full functionality):
|
| 120 |
-
pip install openpyxl pyarrow sqlalchemy beautifulsoup4
|
| 121 |
-
|
| 122 |
-
================================================================================
|
| 123 |
-
|
| 124 |
-
NEXT STEPS
|
| 125 |
-
================================================================================
|
| 126 |
-
|
| 127 |
-
1. Review TOOLKIT_SUMMARY.md for complete overview
|
| 128 |
-
2. Run examples to verify installation:
|
| 129 |
-
- python example_crash_analysis.py
|
| 130 |
-
- python example_data_extraction.py
|
| 131 |
-
3. Read the appropriate README (crash or extraction)
|
| 132 |
-
4. Try with your own data
|
| 133 |
-
5. Use Claude Code skills for AI-assisted workflows
|
| 134 |
-
|
| 135 |
-
================================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/LICENSE
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
Copyright (c) 2026 Dr. Waël
|
| 2 |
-
All rights reserved.
|
| 3 |
-
|
| 4 |
-
This repository is released under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY‑NC‑SA 4.0)** license.
|
| 5 |
-
|
| 6 |
-
**Key terms**:
|
| 7 |
-
- You may **share** and **adapt** the code for any purpose, provided you give appropriate credit to the author (Dr. Waël) and indicate if changes were made.
|
| 8 |
-
- **Non‑commercial**: The software may not be used for commercial purposes without explicit permission from the author.
|
| 9 |
-
- **ShareAlike**: If you modify or build upon the code, you must distribute your contributions under the same license.
|
| 10 |
-
|
| 11 |
-
**Dataset attribution and compliance**:
|
| 12 |
-
- This project incorporates data from the Alpamayo, Waymo, Audi, and other datasets. Their usage is subject to the respective dataset licenses. By using this repository you agree to comply with all dataset-specific terms, which may include restrictions on redistribution, commercial use, or derivative works. Please refer to the original dataset licenses for full details.
|
| 13 |
-
|
| 14 |
-
**Disclaimer**:
|
| 15 |
-
- The author provides the software as‑is, without any warranty. The author is not responsible for any misuse of the included datasets.
|
| 16 |
-
|
| 17 |
-
For the full legal text of CC BY‑NC‑SA 4.0, see https://creativecommons.org/licenses/by-nc-sa/4.0/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/README.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Sensorflow Studio
|
| 3 |
-
emoji: 🛰️
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: false
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
# Sensorflow Studio
|
| 12 |
-
|
| 13 |
-
An autonomous-driving data platform: dataset exploration, label evaluation,
|
| 14 |
-
MITL (model-in-the-loop) review tooling, and the SSAM Safety Dashboard for
|
| 15 |
-
statewide surrogate-safety analytics.
|
| 16 |
-
|
| 17 |
-
## What's served
|
| 18 |
-
|
| 19 |
-
- `/` — legacy static UI (dataset / training studio)
|
| 20 |
-
- `/dashboard/` — React (Vite) frontend, including the SSAM Safety Dashboard
|
| 21 |
-
- `/api/...` — FastAPI backend endpoints (e.g. `POST /api/ssam/statewide`)
|
| 22 |
-
|
| 23 |
-
## Notes for this Space
|
| 24 |
-
|
| 25 |
-
This is a CPU Space running a slim dependency set. Features requiring GPU or
|
| 26 |
-
local services — YOLO training, SAM-assisted annotation, and the Ollama
|
| 27 |
-
copilot — are not functional here; the dashboards and analytics APIs are.
|
| 28 |
-
|
| 29 |
-
Deployed from the project repository via
|
| 30 |
-
`deploy/huggingface/deploy.py` (uploads files with `huggingface_hub`; the
|
| 31 |
-
frontend is built inside the Docker image).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/TOOLKIT_SUMMARY.md
DELETED
|
@@ -1,440 +0,0 @@
|
|
| 1 |
-
# Complete Toolkit Summary
|
| 2 |
-
|
| 3 |
-
You now have **two complete, production-ready toolkits** for data analysis and extraction.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## 🎯 Toolkit 1: Crash Data Analysis
|
| 8 |
-
|
| 9 |
-
**For analyzing traffic crash data across three dimensions:**
|
| 10 |
-
|
| 11 |
-
### Files
|
| 12 |
-
- **Skill**: `.claude/skills/crash-data-analyzer.md`
|
| 13 |
-
- **Module**: `crash_analyzer.py`
|
| 14 |
-
- **Examples**: `example_crash_analysis.py`
|
| 15 |
-
- **Guide**: `CRASH_DATA_GUIDE.md`
|
| 16 |
-
- **README**: `CRASH_ANALYSIS_README.md`
|
| 17 |
-
- **Quick Ref**: `CRASH_ANALYSIS_QUICK_REF.md`
|
| 18 |
-
|
| 19 |
-
### What It Does
|
| 20 |
-
Extract and analyze SWITRS crash data (from Berkeley's TIMS) with:
|
| 21 |
-
- **Metadata Analysis**: Temporal patterns, severity, collision types
|
| 22 |
-
- **Geospatial Analysis**: Hotspot identification, county breakdowns
|
| 23 |
-
- **Vehicle/Traveler Analysis**: Demographics, risk factors, injury patterns
|
| 24 |
-
|
| 25 |
-
### Quick Start
|
| 26 |
-
```python
|
| 27 |
-
from crash_analyzer import load_switrs_data
|
| 28 |
-
analyzer = load_switrs_data('accidents.csv', 'vehicles.csv', 'parties.csv')
|
| 29 |
-
report = analyzer.generate_summary_report()
|
| 30 |
-
```
|
| 31 |
-
|
| 32 |
-
### Key Classes
|
| 33 |
-
- `CrashDataAnalyzer` — Main analysis engine
|
| 34 |
-
- Methods for each dimension (temporal, geo, vehicle/traveler)
|
| 35 |
-
- Built-in hotspot detection and risk calculation
|
| 36 |
-
|
| 37 |
-
### Use With
|
| 38 |
-
```
|
| 39 |
-
/crash-data-analyzer
|
| 40 |
-
```
|
| 41 |
-
|
| 42 |
-
---
|
| 43 |
-
|
| 44 |
-
## 🎯 Toolkit 2: Universal Data Extraction
|
| 45 |
-
|
| 46 |
-
**For extracting, validating, transforming, and exporting data from any source:**
|
| 47 |
-
|
| 48 |
-
### Files
|
| 49 |
-
- **Skill**: `.claude/skills/data-extraction-toolkit.md`
|
| 50 |
-
- **Module**: `data_extractor.py`
|
| 51 |
-
- **Examples**: `example_data_extraction.py`
|
| 52 |
-
- **Guide**: `DATA_EXTRACTION_GUIDE.md`
|
| 53 |
-
- **README**: `DATA_EXTRACTION_TOOLKIT_README.md`
|
| 54 |
-
- **Quick Ref**: `DATA_EXTRACTION_QUICK_REF.md`
|
| 55 |
-
|
| 56 |
-
### What It Does
|
| 57 |
-
Complete ETL (Extract, Transform, Load) pipeline with:
|
| 58 |
-
- **Extract**: APIs, databases, files, web sources
|
| 59 |
-
- **Validate**: Quality checks and error reporting
|
| 60 |
-
- **Transform**: Cleaning, normalization, type conversion
|
| 61 |
-
- **Export**: Multiple formats (CSV, JSON, Parquet, Excel)
|
| 62 |
-
|
| 63 |
-
### Quick Start
|
| 64 |
-
```python
|
| 65 |
-
from data_extractor import DataExtractionPipeline
|
| 66 |
-
pipeline = DataExtractionPipeline('MyProject')
|
| 67 |
-
pipeline.extract('input.csv') \
|
| 68 |
-
.validate({'required_columns': ['id']}) \
|
| 69 |
-
.transform({'standardize_names': {}}) \
|
| 70 |
-
.export('output.csv')
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
### Key Classes
|
| 74 |
-
- `APIExtractor` — REST APIs with pagination & retry
|
| 75 |
-
- `DatabaseExtractor` — SQL databases
|
| 76 |
-
- `FileExtractor` — CSV, JSON, Excel, Parquet
|
| 77 |
-
- `WebExtractor` — HTML table scraping
|
| 78 |
-
- `DataValidator` — Quality checks
|
| 79 |
-
- `DataCleaner` — Data cleaning
|
| 80 |
-
- `DataExporter` — Multi-format export
|
| 81 |
-
- `DataExtractionPipeline` — Complete workflow
|
| 82 |
-
|
| 83 |
-
### Use With
|
| 84 |
-
```
|
| 85 |
-
/data-extraction-toolkit
|
| 86 |
-
```
|
| 87 |
-
|
| 88 |
-
---
|
| 89 |
-
|
| 90 |
-
## 📊 Comparison
|
| 91 |
-
|
| 92 |
-
| Aspect | Crash Analyzer | Data Extraction |
|
| 93 |
-
|--------|---|---|
|
| 94 |
-
| **Purpose** | Analyze crash data | Extract & process any data |
|
| 95 |
-
| **Specialization** | SWITRS format (CA crashes) | General purpose |
|
| 96 |
-
| **Analysis** | Temporal, geo, demographic | Validation, cleaning, export |
|
| 97 |
-
| **Output** | Statistics, hotspots, reports | Clean data in multiple formats |
|
| 98 |
-
| **Best For** | Traffic safety research | Data pipelines, ETL, analytics |
|
| 99 |
-
| **Integration** | Standalone or as module | Integrates with any data source |
|
| 100 |
-
|
| 101 |
-
---
|
| 102 |
-
|
| 103 |
-
## 🔄 Typical Usage Patterns
|
| 104 |
-
|
| 105 |
-
### Pattern A: Stand-Alone Analysis
|
| 106 |
-
```python
|
| 107 |
-
# Use one toolkit independently
|
| 108 |
-
from crash_analyzer import CrashDataAnalyzer
|
| 109 |
-
analyzer = CrashDataAnalyzer(df_accidents, df_vehicles, df_parties)
|
| 110 |
-
results = analyzer.generate_summary_report()
|
| 111 |
-
```
|
| 112 |
-
|
| 113 |
-
### Pattern B: Extraction + Analysis
|
| 114 |
-
```python
|
| 115 |
-
# Use data extraction to prep, then analyze
|
| 116 |
-
from data_extractor import DataExtractionPipeline
|
| 117 |
-
from crash_analyzer import CrashDataAnalyzer
|
| 118 |
-
|
| 119 |
-
# Extract & clean
|
| 120 |
-
pipeline = DataExtractionPipeline('Crashes')
|
| 121 |
-
pipeline.extract('raw_crashes.csv') \
|
| 122 |
-
.validate({'required_columns': ['latitude', 'longitude']}) \
|
| 123 |
-
.transform({'remove_duplicates': {}})
|
| 124 |
-
|
| 125 |
-
# Analyze
|
| 126 |
-
analyzer = CrashDataAnalyzer(pipeline.data)
|
| 127 |
-
report = analyzer.analyze_geographic_summary()
|
| 128 |
-
```
|
| 129 |
-
|
| 130 |
-
### Pattern C: Multiple Source Integration
|
| 131 |
-
```python
|
| 132 |
-
# Combine data from multiple sources, then analyze
|
| 133 |
-
from data_extractor import FileExtractor, DataExtractionPipeline
|
| 134 |
-
import pandas as pd
|
| 135 |
-
|
| 136 |
-
# Extract from multiple sources
|
| 137 |
-
accidents = FileExtractor.extract_csv('accidents.csv')
|
| 138 |
-
vehicles = FileExtractor.extract_csv('vehicles.csv')
|
| 139 |
-
parties = FileExtractor.extract_json('parties.json')
|
| 140 |
-
|
| 141 |
-
# Combine and clean
|
| 142 |
-
pipeline = DataExtractionPipeline('CombinedCrashes')
|
| 143 |
-
pipeline.data = pd.concat([accidents, vehicles, parties], axis=1)
|
| 144 |
-
pipeline.validate({...}).transform({...}).export('clean_data.csv')
|
| 145 |
-
|
| 146 |
-
# Analyze
|
| 147 |
-
analyzer = CrashDataAnalyzer(accidents, vehicles, parties)
|
| 148 |
-
```
|
| 149 |
-
|
| 150 |
-
---
|
| 151 |
-
|
| 152 |
-
## 📚 Documentation Structure
|
| 153 |
-
|
| 154 |
-
Each toolkit includes:
|
| 155 |
-
|
| 156 |
-
### 1. Skill File (`.claude/skills/`)
|
| 157 |
-
- Complete workflows and patterns
|
| 158 |
-
- Use with `/skill-name` in Claude Code
|
| 159 |
-
- Code templates for all use cases
|
| 160 |
-
- Best practices and interpretations
|
| 161 |
-
|
| 162 |
-
### 2. Python Module
|
| 163 |
-
- Production-ready classes
|
| 164 |
-
- Full docstrings and type hints
|
| 165 |
-
- Error handling built-in
|
| 166 |
-
- Import and use directly
|
| 167 |
-
|
| 168 |
-
### 3. Examples
|
| 169 |
-
- 7+ runnable examples
|
| 170 |
-
- Real-world scenarios
|
| 171 |
-
- Copy-paste starting points
|
| 172 |
-
- Demonstrate all features
|
| 173 |
-
|
| 174 |
-
### 4. Complete Guide
|
| 175 |
-
- API reference
|
| 176 |
-
- Schema documentation
|
| 177 |
-
- Performance tips
|
| 178 |
-
- Troubleshooting
|
| 179 |
-
|
| 180 |
-
### 5. README
|
| 181 |
-
- Quick start
|
| 182 |
-
- Feature overview
|
| 183 |
-
- Integration guide
|
| 184 |
-
- Next steps
|
| 185 |
-
|
| 186 |
-
### 6. Quick Reference
|
| 187 |
-
- One-page cheat sheet
|
| 188 |
-
- Common patterns
|
| 189 |
-
- API quick lookup
|
| 190 |
-
- Print-friendly
|
| 191 |
-
|
| 192 |
-
---
|
| 193 |
-
|
| 194 |
-
## 🚀 Getting Started
|
| 195 |
-
|
| 196 |
-
### For Crash Data Analysis
|
| 197 |
-
1. Read: `CRASH_ANALYSIS_README.md`
|
| 198 |
-
2. Run: `python example_crash_analysis.py`
|
| 199 |
-
3. Use: `from crash_analyzer import load_switrs_data`
|
| 200 |
-
4. Invoke skill: `/crash-data-analyzer`
|
| 201 |
-
|
| 202 |
-
### For Data Extraction
|
| 203 |
-
1. Read: `DATA_EXTRACTION_TOOLKIT_README.md`
|
| 204 |
-
2. Run: `python example_data_extraction.py`
|
| 205 |
-
3. Use: `from data_extractor import DataExtractionPipeline`
|
| 206 |
-
4. Invoke skill: `/data-extraction-toolkit`
|
| 207 |
-
|
| 208 |
-
### For Combined Usage
|
| 209 |
-
1. Extract with data extraction toolkit
|
| 210 |
-
2. Analyze with crash analyzer
|
| 211 |
-
3. Refer to guides as needed
|
| 212 |
-
4. Leverage skills in Claude Code
|
| 213 |
-
|
| 214 |
-
---
|
| 215 |
-
|
| 216 |
-
## 💡 Common Use Cases
|
| 217 |
-
|
| 218 |
-
### Traffic Safety Research
|
| 219 |
-
→ Use **Crash Data Analyzer**
|
| 220 |
-
- Identify dangerous intersections
|
| 221 |
-
- Analyze demographic trends
|
| 222 |
-
- Track seasonal patterns
|
| 223 |
-
|
| 224 |
-
### Data Pipeline Development
|
| 225 |
-
→ Use **Data Extraction Toolkit**
|
| 226 |
-
- Fetch from APIs
|
| 227 |
-
- Clean CSV files
|
| 228 |
-
- Export to multiple formats
|
| 229 |
-
|
| 230 |
-
### Combined Analytics
|
| 231 |
-
→ Use **Both Toolkits**
|
| 232 |
-
- Extract crash data from TIMS
|
| 233 |
-
- Clean with extraction toolkit
|
| 234 |
-
- Analyze with crash analyzer
|
| 235 |
-
- Export findings
|
| 236 |
-
|
| 237 |
-
### ETL Automation
|
| 238 |
-
→ Use **Data Extraction Toolkit**
|
| 239 |
-
- Database → CSV extraction
|
| 240 |
-
- Validation checks
|
| 241 |
-
- Transformation pipeline
|
| 242 |
-
- Auto-export to data warehouse
|
| 243 |
-
|
| 244 |
-
---
|
| 245 |
-
|
| 246 |
-
## 🎯 Quick Command Reference
|
| 247 |
-
|
| 248 |
-
### Invoke Skills
|
| 249 |
-
```
|
| 250 |
-
/crash-data-analyzer
|
| 251 |
-
/data-extraction-toolkit
|
| 252 |
-
```
|
| 253 |
-
|
| 254 |
-
### Import Modules
|
| 255 |
-
```python
|
| 256 |
-
from crash_analyzer import CrashDataAnalyzer, load_switrs_data
|
| 257 |
-
from data_extractor import DataExtractionPipeline, APIExtractor, DatabaseExtractor
|
| 258 |
-
```
|
| 259 |
-
|
| 260 |
-
### Run Examples
|
| 261 |
-
```bash
|
| 262 |
-
python example_crash_analysis.py
|
| 263 |
-
python example_data_extraction.py
|
| 264 |
-
```
|
| 265 |
-
|
| 266 |
-
### Read Guides
|
| 267 |
-
- Crash: `CRASH_DATA_GUIDE.md` or `CRASH_ANALYSIS_QUICK_REF.md`
|
| 268 |
-
- Extraction: `DATA_EXTRACTION_GUIDE.md` or `DATA_EXTRACTION_QUICK_REF.md`
|
| 269 |
-
|
| 270 |
-
---
|
| 271 |
-
|
| 272 |
-
## 📁 File Organization
|
| 273 |
-
|
| 274 |
-
```
|
| 275 |
-
DrivingRepo/
|
| 276 |
-
│
|
| 277 |
-
├── .claude/skills/
|
| 278 |
-
│ ├── crash-data-analyzer.md # Crash analysis skill
|
| 279 |
-
│ └── data-extraction-toolkit.md # Data extraction skill
|
| 280 |
-
│
|
| 281 |
-
├── crash_analyzer.py # Crash analysis module
|
| 282 |
-
├── example_crash_analysis.py # Crash examples
|
| 283 |
-
├── CRASH_ANALYSIS_README.md # Crash documentation
|
| 284 |
-
├── CRASH_DATA_GUIDE.md # Crash detailed guide
|
| 285 |
-
├── CRASH_ANALYSIS_QUICK_REF.md # Crash quick reference
|
| 286 |
-
│
|
| 287 |
-
├── data_extractor.py # Extraction module
|
| 288 |
-
├── example_data_extraction.py # Extraction examples
|
| 289 |
-
├── DATA_EXTRACTION_TOOLKIT_README.md # Extraction documentation
|
| 290 |
-
├── DATA_EXTRACTION_GUIDE.md # Extraction detailed guide
|
| 291 |
-
├── DATA_EXTRACTION_QUICK_REF.md # Extraction quick reference
|
| 292 |
-
│
|
| 293 |
-
└── TOOLKIT_SUMMARY.md # This file
|
| 294 |
-
```
|
| 295 |
-
|
| 296 |
-
---
|
| 297 |
-
|
| 298 |
-
## ✨ Features at a Glance
|
| 299 |
-
|
| 300 |
-
### Crash Data Analyzer
|
| 301 |
-
✓ Temporal analysis (hour, day, month, year)
|
| 302 |
-
✓ Severity distribution tracking
|
| 303 |
-
✓ Collision type analysis
|
| 304 |
-
✓ Geographic hotspot detection
|
| 305 |
-
✓ Vehicle type risk analysis
|
| 306 |
-
✓ Demographic breakdown (age, gender, sobriety)
|
| 307 |
-
✓ Injury pattern analysis
|
| 308 |
-
✓ Complete summary reports
|
| 309 |
-
|
| 310 |
-
### Data Extraction Toolkit
|
| 311 |
-
✓ Extract from 8+ source types
|
| 312 |
-
✓ Paginated API fetching with retry
|
| 313 |
-
✓ SQL database querying
|
| 314 |
-
✓ CSV/JSON/Excel/Parquet support
|
| 315 |
-
✓ Web table scraping
|
| 316 |
-
✓ Batch file processing
|
| 317 |
-
✓ Data quality validation
|
| 318 |
-
✓ Automated cleaning & transformation
|
| 319 |
-
✓ Multi-format export
|
| 320 |
-
✓ Complete ETL pipeline
|
| 321 |
-
|
| 322 |
-
---
|
| 323 |
-
|
| 324 |
-
## 🔗 Integration Points
|
| 325 |
-
|
| 326 |
-
### With Claude Code
|
| 327 |
-
- Use `/crash-data-analyzer` skill
|
| 328 |
-
- Use `/data-extraction-toolkit` skill
|
| 329 |
-
- Claude suggests relevant patterns
|
| 330 |
-
- Complete code generation
|
| 331 |
-
|
| 332 |
-
### With Python
|
| 333 |
-
- Import modules directly
|
| 334 |
-
- Chain operations
|
| 335 |
-
- Extend with custom logic
|
| 336 |
-
- Add to existing pipelines
|
| 337 |
-
|
| 338 |
-
### With Databases
|
| 339 |
-
- Query via DatabaseExtractor
|
| 340 |
-
- Validate with DataValidator
|
| 341 |
-
- Transform with DataCleaner
|
| 342 |
-
- Export to data warehouse
|
| 343 |
-
|
| 344 |
-
### With APIs
|
| 345 |
-
- Fetch paginated data
|
| 346 |
-
- Handle authentication
|
| 347 |
-
- Implement retry logic
|
| 348 |
-
- Export to multiple formats
|
| 349 |
-
|
| 350 |
-
---
|
| 351 |
-
|
| 352 |
-
## 📖 Version Info
|
| 353 |
-
|
| 354 |
-
| Component | Version | Status |
|
| 355 |
-
|-----------|---------|--------|
|
| 356 |
-
| crash_analyzer.py | 1.0 | Production |
|
| 357 |
-
| data_extractor.py | 1.0 | Production |
|
| 358 |
-
| Skill files | 1.0 | Production |
|
| 359 |
-
| Documentation | 1.0 | Complete |
|
| 360 |
-
|
| 361 |
-
Last Updated: 2026-07-26
|
| 362 |
-
|
| 363 |
-
---
|
| 364 |
-
|
| 365 |
-
## 🎓 Learning Path
|
| 366 |
-
|
| 367 |
-
### Day 1: Get Familiar
|
| 368 |
-
- Read the README files
|
| 369 |
-
- Run the example scripts
|
| 370 |
-
- Print the quick references
|
| 371 |
-
|
| 372 |
-
### Day 2: Use One Toolkit
|
| 373 |
-
- Choose Crash or Extraction
|
| 374 |
-
- Read the detailed guide
|
| 375 |
-
- Modify an example for your data
|
| 376 |
-
|
| 377 |
-
### Day 3: Combine Toolkits
|
| 378 |
-
- Use extraction to prep data
|
| 379 |
-
- Analyze with crash analyzer
|
| 380 |
-
- Export findings
|
| 381 |
-
|
| 382 |
-
### Day 4+: Production Use
|
| 383 |
-
- Integrate into pipelines
|
| 384 |
-
- Extend with custom logic
|
| 385 |
-
- Deploy to production
|
| 386 |
-
|
| 387 |
-
---
|
| 388 |
-
|
| 389 |
-
## 🆘 Help & Support
|
| 390 |
-
|
| 391 |
-
### For Questions
|
| 392 |
-
1. Check the relevant Quick Reference
|
| 393 |
-
2. Read the complete Guide
|
| 394 |
-
3. Review Examples
|
| 395 |
-
4. Use the Skill with `/skill-name` in Claude Code
|
| 396 |
-
|
| 397 |
-
### For Issues
|
| 398 |
-
1. Check Troubleshooting section (in guides)
|
| 399 |
-
2. Run examples to verify setup
|
| 400 |
-
3. Print debug info (df.head(), df.dtypes, df.columns)
|
| 401 |
-
4. Ask Claude with `/skill-name` prompt
|
| 402 |
-
|
| 403 |
-
### For Extensions
|
| 404 |
-
- Modify the Python classes
|
| 405 |
-
- Add custom validation rules
|
| 406 |
-
- Extend transformations
|
| 407 |
-
- Integrate additional sources
|
| 408 |
-
|
| 409 |
-
---
|
| 410 |
-
|
| 411 |
-
## 🎉 You're All Set!
|
| 412 |
-
|
| 413 |
-
You have two complete, production-ready toolkits with:
|
| 414 |
-
- ✅ Professional Python modules
|
| 415 |
-
- ✅ Claude AI integration (skills)
|
| 416 |
-
- ✅ 7+ working examples
|
| 417 |
-
- ✅ Complete documentation
|
| 418 |
-
- ✅ Quick reference guides
|
| 419 |
-
- ✅ Best practices & patterns
|
| 420 |
-
|
| 421 |
-
**Start using them today:**
|
| 422 |
-
```python
|
| 423 |
-
# Crash analysis
|
| 424 |
-
from crash_analyzer import load_switrs_data
|
| 425 |
-
analyzer = load_switrs_data('accidents.csv', 'vehicles.csv', 'parties.csv')
|
| 426 |
-
|
| 427 |
-
# Data extraction
|
| 428 |
-
from data_extractor import DataExtractionPipeline
|
| 429 |
-
pipeline = DataExtractionPipeline('Data').extract('input.csv')
|
| 430 |
-
```
|
| 431 |
-
|
| 432 |
-
Or invoke in Claude Code:
|
| 433 |
-
```
|
| 434 |
-
/crash-data-analyzer
|
| 435 |
-
/data-extraction-toolkit
|
| 436 |
-
```
|
| 437 |
-
|
| 438 |
-
---
|
| 439 |
-
|
| 440 |
-
**Happy analyzing and extracting! 🚀**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_analysis.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Analysis Engine Layer
|
| 3 |
-
Performs temporal pattern detection, geospatial hotspot clustering, demographic reviews, and caches results.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
from typing import Dict, Any, List, Optional
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from crash_analyzer import CrashDataAnalyzer
|
| 10 |
-
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
class MetadataAnalyzer:
|
| 15 |
-
"""Performs schema, time, and severity breakdown analysis on accident records."""
|
| 16 |
-
|
| 17 |
-
def __init__(self, analyzer: CrashDataAnalyzer):
|
| 18 |
-
self.analyzer = analyzer
|
| 19 |
-
|
| 20 |
-
def analyze(self) -> Dict[str, Any]:
|
| 21 |
-
"""Runs temporal and severity breakdown metrics."""
|
| 22 |
-
temporal = self.analyzer.analyze_temporal_patterns()
|
| 23 |
-
severity = self.analyzer.analyze_severity()
|
| 24 |
-
collision = self.analyzer.analyze_collision_types()
|
| 25 |
-
|
| 26 |
-
return {
|
| 27 |
-
"temporal_patterns": temporal,
|
| 28 |
-
"severity_distribution": severity,
|
| 29 |
-
"collision_types": collision
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
class GeospatialAnalyzer:
|
| 33 |
-
"""Performs geographic analysis and hotspot identification."""
|
| 34 |
-
|
| 35 |
-
def __init__(self, analyzer: CrashDataAnalyzer):
|
| 36 |
-
self.analyzer = analyzer
|
| 37 |
-
|
| 38 |
-
def analyze(self, grid_size: float = 0.01) -> Dict[str, Any]:
|
| 39 |
-
"""Clusters coordinates to locate hotspot sectors."""
|
| 40 |
-
summary = self.analyzer.get_geographic_summary()
|
| 41 |
-
hotspots = self.analyzer.identify_hotspots(grid_size=grid_size)
|
| 42 |
-
return {
|
| 43 |
-
"geographic_summary": summary,
|
| 44 |
-
"hotspots": hotspots
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
class DemographicsAnalyzer:
|
| 48 |
-
"""Performs party, vehicle type, and traveler demographic analysis."""
|
| 49 |
-
|
| 50 |
-
def __init__(self, analyzer: CrashDataAnalyzer):
|
| 51 |
-
self.analyzer = analyzer
|
| 52 |
-
|
| 53 |
-
def analyze(self) -> Dict[str, Any]:
|
| 54 |
-
"""Parses driver profiles and vehicle risk attributes."""
|
| 55 |
-
vehicles = self.analyzer.analyze_vehicle_types()
|
| 56 |
-
demographics = self.analyzer.analyze_demographics()
|
| 57 |
-
return {
|
| 58 |
-
"vehicle_risk": vehicles,
|
| 59 |
-
"demographics": demographics
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
class AccidentAnalysisEngine:
|
| 63 |
-
"""Orchestrates temporal, geospatial, and demographic analysis while caching results."""
|
| 64 |
-
|
| 65 |
-
def __init__(self, accidents_df: pd.DataFrame, vehicles_df: Optional[pd.DataFrame] = None,
|
| 66 |
-
parties_df: Optional[pd.DataFrame] = None):
|
| 67 |
-
self.analyzer = CrashDataAnalyzer(accidents_df, vehicles_df, parties_df)
|
| 68 |
-
self.metadata_analyzer = MetadataAnalyzer(self.analyzer)
|
| 69 |
-
self.geospatial_analyzer = GeospatialAnalyzer(self.analyzer)
|
| 70 |
-
self.demographics_analyzer = DemographicsAnalyzer(self.analyzer)
|
| 71 |
-
self._cache: Dict[str, Any] = {}
|
| 72 |
-
|
| 73 |
-
def run_full_pipeline(self, bypass_cache: bool = False) -> Dict[str, Any]:
|
| 74 |
-
"""
|
| 75 |
-
Executes all sub-analyzers and returns cached or fresh analysis results.
|
| 76 |
-
|
| 77 |
-
Args:
|
| 78 |
-
bypass_cache: Force fresh computation.
|
| 79 |
-
|
| 80 |
-
Returns:
|
| 81 |
-
Dict containing full analysis indicators.
|
| 82 |
-
"""
|
| 83 |
-
if "full_metrics" in self._cache and not bypass_cache:
|
| 84 |
-
logger.info("Returning cached analysis results.")
|
| 85 |
-
return self._cache["full_metrics"]
|
| 86 |
-
|
| 87 |
-
logger.info("Computing fresh analysis metrics...")
|
| 88 |
-
meta = self.metadata_analyzer.analyze()
|
| 89 |
-
geo = self.geospatial_analyzer.analyze()
|
| 90 |
-
demo = self.demographics_analyzer.analyze()
|
| 91 |
-
|
| 92 |
-
results = {
|
| 93 |
-
"metadata": meta,
|
| 94 |
-
"geospatial": geo,
|
| 95 |
-
"demographics": demo
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
self._cache["full_metrics"] = results
|
| 99 |
-
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_cleaner.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Data Cleaning & Normalization Layer
|
| 3 |
-
Cleans raw dataframes, normalizes severity codes, strips spaces, and standardizes schemas.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
import pandas as pd
|
| 8 |
-
from typing import Optional, List
|
| 9 |
-
from data_extractor import DataCleaner
|
| 10 |
-
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
class AccidentDataCleaner:
|
| 15 |
-
"""Provides routines to clean and standardize accident datasets."""
|
| 16 |
-
|
| 17 |
-
def __init__(self, dataframe: pd.DataFrame):
|
| 18 |
-
self.df = dataframe.copy()
|
| 19 |
-
|
| 20 |
-
def clean_dataset(self) -> pd.DataFrame:
|
| 21 |
-
"""
|
| 22 |
-
Executes standard pipeline cleanups: column standardization, whitespace trim,
|
| 23 |
-
duplicate removal, and severity normalization.
|
| 24 |
-
|
| 25 |
-
Returns:
|
| 26 |
-
pd.DataFrame of cleaned data.
|
| 27 |
-
"""
|
| 28 |
-
logger.info(f"Cleaning dataset of size {self.df.shape}...")
|
| 29 |
-
|
| 30 |
-
# 1. Standardize Column Names
|
| 31 |
-
self.df = DataCleaner.standardize_column_names(self.df)
|
| 32 |
-
|
| 33 |
-
# 2. Trim Whitespace
|
| 34 |
-
self.df = DataCleaner.trim_whitespace(self.df)
|
| 35 |
-
|
| 36 |
-
# 3. Remove duplicates
|
| 37 |
-
if "case_id" in self.df.columns:
|
| 38 |
-
self.df = DataCleaner.remove_duplicates(self.df, subset=["case_id"])
|
| 39 |
-
|
| 40 |
-
# 4. Fill missing severities with 'Property Damage' (P) as fallback
|
| 41 |
-
if "severity" in self.df.columns:
|
| 42 |
-
self.df["severity"] = self.df["severity"].fillna("P")
|
| 43 |
-
# Map standard values
|
| 44 |
-
severity_map = {
|
| 45 |
-
"fatal": "F", "injury": "I", "property damage": "P",
|
| 46 |
-
"f": "F", "i": "I", "p": "P",
|
| 47 |
-
"1": "F", "2": "I", "3": "P",
|
| 48 |
-
1: "F", 2: "I", 3: "P"
|
| 49 |
-
}
|
| 50 |
-
self.df["severity"] = self.df["severity"].map(lambda x: severity_map.get(str(x).lower().strip(), "P"))
|
| 51 |
-
|
| 52 |
-
# 5. Coordinate cleanups
|
| 53 |
-
if "latitude" in self.df.columns:
|
| 54 |
-
self.df["latitude"] = pd.to_numeric(self.df["latitude"], errors="coerce")
|
| 55 |
-
if "longitude" in self.df.columns:
|
| 56 |
-
self.df["longitude"] = pd.to_numeric(self.df["longitude"], errors="coerce")
|
| 57 |
-
|
| 58 |
-
# Drop rows where coordinates are NaN to keep analysis accurate
|
| 59 |
-
self.df = self.df.dropna(subset=["latitude", "longitude"]).reset_index(drop=True)
|
| 60 |
-
|
| 61 |
-
logger.info(f"Cleaning complete. Returning clean dataframe of size {self.df.shape}")
|
| 62 |
-
return self.df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_importer.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Data Ingestion Layer
|
| 3 |
-
Loads SWITRS accident datasets from multiple sources including CSV, JSON, Parquet, SQL databases, and paginated APIs.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
import logging
|
| 9 |
-
from typing import Optional, Dict, Any, List
|
| 10 |
-
import pandas as pd
|
| 11 |
-
from data_extractor import DataExtractionPipeline, FileExtractor, APIExtractor, DatabaseExtractor
|
| 12 |
-
|
| 13 |
-
logging.basicConfig(level=logging.INFO)
|
| 14 |
-
logger = logging.getLogger(__name__)
|
| 15 |
-
|
| 16 |
-
class AccidentDataImporter:
|
| 17 |
-
"""Ingests accident datasets from various formats and database/API endpoints."""
|
| 18 |
-
|
| 19 |
-
def __init__(self, pipeline_name: str = "AccidentIngestionPipeline"):
|
| 20 |
-
self.pipeline = DataExtractionPipeline(pipeline_name)
|
| 21 |
-
|
| 22 |
-
def import_from_file(self, filepath: str, format_type: Optional[str] = None) -> pd.DataFrame:
|
| 23 |
-
"""
|
| 24 |
-
Loads dataset from a local file, auto-detecting the extension if format_type is not provided.
|
| 25 |
-
|
| 26 |
-
Args:
|
| 27 |
-
filepath: Path to the dataset file.
|
| 28 |
-
format_type: Optional format override (e.g. 'csv', 'json', 'parquet').
|
| 29 |
-
|
| 30 |
-
Returns:
|
| 31 |
-
pd.DataFrame of loaded data.
|
| 32 |
-
"""
|
| 33 |
-
logger.info(f"Importing accident data from file: {filepath}")
|
| 34 |
-
if not format_type:
|
| 35 |
-
_, ext = os.path.splitext(filepath)
|
| 36 |
-
format_type = ext.lower().replace(".", "")
|
| 37 |
-
if format_type == "jsonl":
|
| 38 |
-
format_type = "jsonl"
|
| 39 |
-
elif format_type not in ["csv", "json", "parquet", "xlsx", "xls"]:
|
| 40 |
-
raise ValueError(f"Could not auto-detect format for: {filepath}")
|
| 41 |
-
|
| 42 |
-
try:
|
| 43 |
-
if format_type == "csv":
|
| 44 |
-
self.pipeline.data = FileExtractor.extract_csv(filepath)
|
| 45 |
-
elif format_type == "json":
|
| 46 |
-
self.pipeline.data = FileExtractor.extract_json(filepath)
|
| 47 |
-
elif format_type == "jsonl":
|
| 48 |
-
self.pipeline.data = FileExtractor.extract_jsonl(filepath)
|
| 49 |
-
elif format_type == "parquet":
|
| 50 |
-
self.pipeline.data = FileExtractor.extract_parquet(filepath)
|
| 51 |
-
elif format_type in ["xlsx", "xls"]:
|
| 52 |
-
self.pipeline.data = FileExtractor.extract_excel(filepath)
|
| 53 |
-
else:
|
| 54 |
-
raise ValueError(f"Unsupported file format: {format_type}")
|
| 55 |
-
|
| 56 |
-
logger.info(f"Successfully imported {len(self.pipeline.data)} records from {filepath}")
|
| 57 |
-
return self.pipeline.data
|
| 58 |
-
except Exception as e:
|
| 59 |
-
logger.error(f"Failed to import data from file {filepath}: {str(e)}")
|
| 60 |
-
raise
|
| 61 |
-
|
| 62 |
-
def import_from_api(self, base_url: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> pd.DataFrame:
|
| 63 |
-
"""
|
| 64 |
-
Fetches accident data from a paginated REST API endpoint.
|
| 65 |
-
|
| 66 |
-
Args:
|
| 67 |
-
base_url: The base URL of the API.
|
| 68 |
-
endpoint: Paginated endpoint path.
|
| 69 |
-
params: Optional query parameters.
|
| 70 |
-
|
| 71 |
-
Returns:
|
| 72 |
-
pd.DataFrame of fetched data.
|
| 73 |
-
"""
|
| 74 |
-
logger.info(f"Importing accident data from API: {base_url}{endpoint}")
|
| 75 |
-
try:
|
| 76 |
-
extractor = APIExtractor(base_url)
|
| 77 |
-
self.pipeline.data = extractor.fetch_paginated(endpoint, params=params)
|
| 78 |
-
logger.info(f"Successfully fetched {len(self.pipeline.data)} records from API")
|
| 79 |
-
return self.pipeline.data
|
| 80 |
-
except Exception as e:
|
| 81 |
-
logger.error(f"Failed to fetch data from API: {str(e)}")
|
| 82 |
-
raise
|
| 83 |
-
|
| 84 |
-
def import_from_database(self, connection_string: str, query: str) -> pd.DataFrame:
|
| 85 |
-
"""
|
| 86 |
-
Queries accident records from a SQL database.
|
| 87 |
-
|
| 88 |
-
Args:
|
| 89 |
-
connection_string: SQL connection URI.
|
| 90 |
-
query: SQL query statement.
|
| 91 |
-
|
| 92 |
-
Returns:
|
| 93 |
-
pd.DataFrame of queried records.
|
| 94 |
-
"""
|
| 95 |
-
logger.info(f"Importing accident data from database query: {query}")
|
| 96 |
-
try:
|
| 97 |
-
extractor = DatabaseExtractor(connection_string)
|
| 98 |
-
self.pipeline.data = extractor.query_to_dataframe(query)
|
| 99 |
-
logger.info(f"Successfully queried {len(self.pipeline.data)} records from database")
|
| 100 |
-
return self.pipeline.data
|
| 101 |
-
except Exception as e:
|
| 102 |
-
logger.error(f"Failed to query database: {str(e)}")
|
| 103 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_insights.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Insights Layer
|
| 3 |
-
Deduce high-level risks, deadliest hours, risk factors, and safety insights.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Dict, Any, List
|
| 7 |
-
|
| 8 |
-
class AccidentInsights:
|
| 9 |
-
"""Extracts safety-critical insights and risk scores from processed analysis metrics."""
|
| 10 |
-
|
| 11 |
-
def __init__(self, metrics: Dict[str, Any]):
|
| 12 |
-
self.metrics = metrics
|
| 13 |
-
|
| 14 |
-
def get_risk_profile(self) -> Dict[str, Any]:
|
| 15 |
-
"""
|
| 16 |
-
Deduces the high-risk profiles from demographic and temporal stats.
|
| 17 |
-
|
| 18 |
-
Returns:
|
| 19 |
-
Dict containing warning metrics and risk profiles.
|
| 20 |
-
"""
|
| 21 |
-
meta = self.metrics.get("metadata", {})
|
| 22 |
-
temporal = meta.get("temporal_patterns", {})
|
| 23 |
-
severity = meta.get("severity_distribution", {})
|
| 24 |
-
|
| 25 |
-
# Deadliest hour
|
| 26 |
-
deadliest_hours = list(temporal.get("deadliest_hours", {}).keys())
|
| 27 |
-
primary_time_risk = f"Hour {deadliest_hours[0]}" if deadliest_hours else "Unknown"
|
| 28 |
-
|
| 29 |
-
# High-risk collision types
|
| 30 |
-
collision = meta.get("collision_types", {})
|
| 31 |
-
top_collisions = list(collision.get("top_collision_types", {}).keys())
|
| 32 |
-
primary_collision_risk = top_collisions[0] if top_collisions else "Unknown"
|
| 33 |
-
|
| 34 |
-
# Hotspots
|
| 35 |
-
geo = self.metrics.get("geospatial", {})
|
| 36 |
-
hotspots = geo.get("hotspots", [])
|
| 37 |
-
primary_hotspot = f"Lat: {hotspots[0]['center_lat']:.4f}, Lon: {hotspots[0]['center_lon']:.4f}" if hotspots else "None"
|
| 38 |
-
|
| 39 |
-
# Risk score (mock safety scale based on fatal crash ratios)
|
| 40 |
-
total = severity.get("total_crashes", 1)
|
| 41 |
-
fatal = severity.get("total_fatalities", 0)
|
| 42 |
-
risk_index = round((fatal / total) * 10, 2) if total > 0 else 0.0
|
| 43 |
-
|
| 44 |
-
return {
|
| 45 |
-
"risk_index": risk_index,
|
| 46 |
-
"deadliest_hour": primary_time_risk,
|
| 47 |
-
"primary_collision_threat": primary_collision_risk,
|
| 48 |
-
"primary_geospatial_hotspot": primary_hotspot,
|
| 49 |
-
"safety_recommendations": [
|
| 50 |
-
"Deploy speed enforcement cameras around the primary geospatial hotspot.",
|
| 51 |
-
f"Increase local police patrols during peak danger windows ({primary_time_risk}).",
|
| 52 |
-
"Evaluate street infrastructure modifications to reduce primary collision threat vectors."
|
| 53 |
-
]
|
| 54 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_report.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Reporting Layer
|
| 3 |
-
Generates production-grade HTML and JSON reports, incorporating visualizations.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import json
|
| 7 |
-
from typing import Dict, Any
|
| 8 |
-
|
| 9 |
-
class AccidentReport:
|
| 10 |
-
"""Exports accident analysis results to multiple formats."""
|
| 11 |
-
|
| 12 |
-
def __init__(self, metrics: Dict[str, Any], insights: Dict[str, Any]):
|
| 13 |
-
self.metrics = metrics
|
| 14 |
-
self.insights = insights
|
| 15 |
-
|
| 16 |
-
def export_to_json(self) -> str:
|
| 17 |
-
"""
|
| 18 |
-
Exports full metrics and insights in standard JSON.
|
| 19 |
-
|
| 20 |
-
Returns:
|
| 21 |
-
JSON formatted string.
|
| 22 |
-
"""
|
| 23 |
-
payload = {
|
| 24 |
-
"metrics": self.metrics,
|
| 25 |
-
"insights": self.insights
|
| 26 |
-
}
|
| 27 |
-
return json.dumps(payload, indent=2)
|
| 28 |
-
|
| 29 |
-
def export_to_html(self) -> str:
|
| 30 |
-
"""
|
| 31 |
-
Generates a styled, standalone HTML page displaying the reports.
|
| 32 |
-
|
| 33 |
-
Returns:
|
| 34 |
-
HTML markup string.
|
| 35 |
-
"""
|
| 36 |
-
rec_list = "".join(f"<li>{r}</li>" for r in self.insights.get("safety_recommendations", []))
|
| 37 |
-
|
| 38 |
-
html = f"""<!DOCTYPE html>
|
| 39 |
-
<html>
|
| 40 |
-
<head>
|
| 41 |
-
<title>Accident Analysis Executive Report</title>
|
| 42 |
-
<style>
|
| 43 |
-
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0b0f19; color: #f3f4f6; margin: 40px; }}
|
| 44 |
-
.container {{ max-width: 800px; margin: 0 auto; background: rgba(20, 28, 48, 0.45); padding: 30px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.08); }}
|
| 45 |
-
h1 {{ color: #00c896; border-bottom: 2px solid rgba(0, 200, 150, 0.2); padding-bottom: 10px; }}
|
| 46 |
-
h2 {{ color: #3b82f6; margin-top: 30px; }}
|
| 47 |
-
.metric-box {{ display: flex; justify-content: space-between; padding: 12px; background: rgba(255,255,255,0.02); margin: 8px 0; border-radius: 6px; }}
|
| 48 |
-
.score {{ font-size: 24px; font-weight: bold; color: #ef4444; }}
|
| 49 |
-
ul {{ padding-left: 20px; }}
|
| 50 |
-
li {{ margin: 8px 0; color: #9ca3af; }}
|
| 51 |
-
</style>
|
| 52 |
-
</head>
|
| 53 |
-
<body>
|
| 54 |
-
<div class="container">
|
| 55 |
-
<h1>Accident Analysis Summary Report</h1>
|
| 56 |
-
<div class="metric-box">
|
| 57 |
-
<span><strong>Calamity Risk Index (0-10):</strong></span>
|
| 58 |
-
<span class="score">{self.insights.get("risk_index", 0)}</span>
|
| 59 |
-
</div>
|
| 60 |
-
<div class="metric-box">
|
| 61 |
-
<span><strong>Peak Danger Hour:</strong></span>
|
| 62 |
-
<span>{self.insights.get("deadliest_hour", "N/A")}</span>
|
| 63 |
-
</div>
|
| 64 |
-
<div class="metric-box">
|
| 65 |
-
<span><strong>Primary Collision Threat:</strong></span>
|
| 66 |
-
<span>{self.insights.get("primary_collision_threat", "N/A")}</span>
|
| 67 |
-
</div>
|
| 68 |
-
<div class="metric-box">
|
| 69 |
-
<span><strong>Primary Geospatial Hotspot:</strong></span>
|
| 70 |
-
<span>{self.insights.get("primary_geospatial_hotspot", "N/A")}</span>
|
| 71 |
-
</div>
|
| 72 |
-
|
| 73 |
-
<h2>Safety Recommendations</h2>
|
| 74 |
-
<ul>
|
| 75 |
-
{rec_list}
|
| 76 |
-
</ul>
|
| 77 |
-
</div>
|
| 78 |
-
</body>
|
| 79 |
-
</html>
|
| 80 |
-
"""
|
| 81 |
-
return html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/accident_validator.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Data Validation Layer
|
| 3 |
-
Validates required columns, data types, coordinates, date ranges, and calculates a data quality score.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
from typing import Dict, Any, List
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from data_extractor import DataValidator, ValidationReport
|
| 10 |
-
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
class AccidentDataValidator:
|
| 15 |
-
"""Validates accident dataframes for schema compliance and geographic/temporal correctness."""
|
| 16 |
-
|
| 17 |
-
def __init__(self, dataframe: pd.DataFrame):
|
| 18 |
-
self.df = dataframe.copy()
|
| 19 |
-
self.validator = DataValidator(self.df)
|
| 20 |
-
|
| 21 |
-
def validate_schema(self) -> ValidationReport:
|
| 22 |
-
"""
|
| 23 |
-
Runs standard validation checks on columns, types, coordinates, and nulls.
|
| 24 |
-
|
| 25 |
-
Returns:
|
| 26 |
-
ValidationReport outlining validity state, errors, and warnings.
|
| 27 |
-
"""
|
| 28 |
-
logger.info("Starting accident schema validation...")
|
| 29 |
-
|
| 30 |
-
# 1. Required Columns
|
| 31 |
-
required = ["case_id", "accident_date", "severity", "latitude", "longitude"]
|
| 32 |
-
self.validator.validate_required_columns(required)
|
| 33 |
-
|
| 34 |
-
# 2. Check no null values in primary keys
|
| 35 |
-
self.validator.validate_no_nulls(["case_id"])
|
| 36 |
-
|
| 37 |
-
# 3. Data Types Check
|
| 38 |
-
type_map = {
|
| 39 |
-
"case_id": "object",
|
| 40 |
-
"latitude": "float64",
|
| 41 |
-
"longitude": "float64"
|
| 42 |
-
}
|
| 43 |
-
self.validator.validate_data_types(type_map)
|
| 44 |
-
|
| 45 |
-
# 4. Check Coordinate Ranges
|
| 46 |
-
if "latitude" in self.df.columns and "longitude" in self.df.columns:
|
| 47 |
-
# Valid US lat/lon boundaries approx
|
| 48 |
-
lat_invalid = self.df[(self.df["latitude"] < 24.0) | (self.df["latitude"] > 49.0)]
|
| 49 |
-
lon_invalid = self.df[(self.df["longitude"] < -125.0) | (self.df["longitude"] > -66.0)]
|
| 50 |
-
|
| 51 |
-
if not lat_invalid.empty:
|
| 52 |
-
self.validator.errors.append(f"Latitude out of valid US range (24-49): {len(lat_invalid)} violations")
|
| 53 |
-
if not lon_invalid.empty:
|
| 54 |
-
self.validator.errors.append(f"Longitude out of valid US range (-125 to -66): {len(lon_invalid)} violations")
|
| 55 |
-
|
| 56 |
-
# 5. Check Date Ranges
|
| 57 |
-
if "accident_date" in self.df.columns:
|
| 58 |
-
try:
|
| 59 |
-
parsed_dates = pd.to_datetime(self.df["accident_date"], errors="coerce")
|
| 60 |
-
null_dates = parsed_dates.isna().sum()
|
| 61 |
-
if null_dates > 0:
|
| 62 |
-
self.validator.errors.append(f"accident_date: {null_dates} rows contain unparseable dates")
|
| 63 |
-
|
| 64 |
-
# Check for future dates
|
| 65 |
-
future_dates = (parsed_dates > pd.Timestamp.now()).sum()
|
| 66 |
-
if future_dates > 0:
|
| 67 |
-
self.validator.errors.append(f"accident_date: {future_dates} rows contain future dates")
|
| 68 |
-
except Exception as e:
|
| 69 |
-
self.validator.errors.append(f"Failed to validate date column: {str(e)}")
|
| 70 |
-
|
| 71 |
-
# 6. Calculate Quality Score
|
| 72 |
-
report = self.validator.get_report()
|
| 73 |
-
|
| 74 |
-
# Compute dynamic quality score (percentage of successful validation elements)
|
| 75 |
-
total_checks = 6
|
| 76 |
-
failed_checks = len(report.errors)
|
| 77 |
-
quality_score = max(0.0, min(1.0, (total_checks - failed_checks) / total_checks))
|
| 78 |
-
|
| 79 |
-
# Inject quality score attribute
|
| 80 |
-
report.quality_score = quality_score
|
| 81 |
-
|
| 82 |
-
logger.info(f"Validation complete. Valid: {report.valid}, Quality Score: {quality_score * 100:.1f}%")
|
| 83 |
-
return report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/app_backend.py
DELETED
|
@@ -1,1481 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
import os
|
| 3 |
-
import sys
|
| 4 |
-
import json
|
| 5 |
-
import subprocess
|
| 6 |
-
import signal
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import List, Optional
|
| 9 |
-
from pydantic import BaseModel
|
| 10 |
-
import httpx
|
| 11 |
-
from fastapi import FastAPI, HTTPException
|
| 12 |
-
from fastapi.staticfiles import StaticFiles
|
| 13 |
-
from fastapi.responses import FileResponse
|
| 14 |
-
|
| 15 |
-
app = FastAPI(title="Sensorflow Studio Backend")
|
| 16 |
-
|
| 17 |
-
# State tracking
|
| 18 |
-
ACTIVE_TRAIN_PROC: Optional[subprocess.Popen] = None
|
| 19 |
-
TRAINING_LOGS_ACCUMULATED = ""
|
| 20 |
-
TRAINING_LOSSES: List[float] = []
|
| 21 |
-
TOTAL_EPOCHS = 10
|
| 22 |
-
CURRENT_EPOCH = 0
|
| 23 |
-
|
| 24 |
-
# Configuration
|
| 25 |
-
CONFIG_PATH = Path("runs/studio_config.json")
|
| 26 |
-
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 27 |
-
|
| 28 |
-
class StudioConfig(BaseModel):
|
| 29 |
-
yaml_path: str = "coco8.yaml"
|
| 30 |
-
source_path: str = "data"
|
| 31 |
-
dataset_type: str = "local"
|
| 32 |
-
model_type: str = "yolov8"
|
| 33 |
-
pipeline_mode: str = "3d"
|
| 34 |
-
sam_checkpoint: str = "models/sam_vit_b.pth"
|
| 35 |
-
vendors: List[str] = ["alpamayo", "waymo"]
|
| 36 |
-
gate_thresholds_path: str = "runs/pipeline/gate_thresholds.json"
|
| 37 |
-
sequence_id: str = "seq_001"
|
| 38 |
-
|
| 39 |
-
class TrainParams(BaseModel):
|
| 40 |
-
model: str = "yolov8n.pt"
|
| 41 |
-
epochs: int = 10
|
| 42 |
-
batch: int = 16
|
| 43 |
-
device: str = "cpu"
|
| 44 |
-
data: str = "coco8.yaml"
|
| 45 |
-
|
| 46 |
-
class InferParams(BaseModel):
|
| 47 |
-
weights: str = "runs/detect/coco_finetuned/weights/best.pt"
|
| 48 |
-
conf: float = 0.25
|
| 49 |
-
iou: float = 0.45
|
| 50 |
-
source: str = "data"
|
| 51 |
-
|
| 52 |
-
class ExportParams(BaseModel):
|
| 53 |
-
weights: str = "runs/detect/coco_finetuned/weights/best.pt"
|
| 54 |
-
format: str = "onnx"
|
| 55 |
-
|
| 56 |
-
def load_config() -> StudioConfig:
|
| 57 |
-
if CONFIG_PATH.exists():
|
| 58 |
-
try:
|
| 59 |
-
with open(CONFIG_PATH, "r") as f:
|
| 60 |
-
data = json.load(f)
|
| 61 |
-
return StudioConfig(**data)
|
| 62 |
-
except Exception:
|
| 63 |
-
pass
|
| 64 |
-
return StudioConfig()
|
| 65 |
-
|
| 66 |
-
@app.post("/api/config")
|
| 67 |
-
def save_config(cfg: StudioConfig):
|
| 68 |
-
with open(CONFIG_PATH, "w") as f:
|
| 69 |
-
json.dump(cfg.dict(), f, indent=2)
|
| 70 |
-
return {"status": "ok", "config": cfg}
|
| 71 |
-
|
| 72 |
-
@app.get("/api/config")
|
| 73 |
-
def get_config():
|
| 74 |
-
return load_config()
|
| 75 |
-
|
| 76 |
-
class SaveYamlRequest(BaseModel):
|
| 77 |
-
path: str
|
| 78 |
-
content: str
|
| 79 |
-
|
| 80 |
-
@app.get("/api/yaml/content")
|
| 81 |
-
def get_yaml_content(path: str = "coco8.yaml"):
|
| 82 |
-
try:
|
| 83 |
-
resolved_path = Path(path)
|
| 84 |
-
if not resolved_path.is_absolute():
|
| 85 |
-
resolved_path = Path(__file__).parent / resolved_path
|
| 86 |
-
if not resolved_path.exists():
|
| 87 |
-
return {"status": "error", "message": f"File {path} does not exist."}
|
| 88 |
-
with open(resolved_path, "r") as f:
|
| 89 |
-
content = f.read()
|
| 90 |
-
return {"status": "ok", "path": str(resolved_path), "content": content}
|
| 91 |
-
except Exception as e:
|
| 92 |
-
raise HTTPException(status_code=500, detail=f"Failed to read YAML file: {str(e)}")
|
| 93 |
-
|
| 94 |
-
@app.post("/api/yaml/save")
|
| 95 |
-
def save_yaml_content(req: SaveYamlRequest):
|
| 96 |
-
try:
|
| 97 |
-
resolved_path = Path(req.path)
|
| 98 |
-
if not resolved_path.is_absolute():
|
| 99 |
-
resolved_path = Path(__file__).parent / resolved_path
|
| 100 |
-
with open(resolved_path, "w") as f:
|
| 101 |
-
f.write(req.content)
|
| 102 |
-
return {"status": "ok", "message": f"YAML config {req.path} saved successfully."}
|
| 103 |
-
except Exception as e:
|
| 104 |
-
raise HTTPException(status_code=500, detail=f"Failed to write YAML file: {str(e)}")
|
| 105 |
-
|
| 106 |
-
@app.get("/api/precheck")
|
| 107 |
-
def precheck():
|
| 108 |
-
# Verify scripts are present
|
| 109 |
-
train_exists = Path("train.py").exists()
|
| 110 |
-
infer_exists = Path("infer.py").exists()
|
| 111 |
-
grader_exists = Path("autograder.py").exists()
|
| 112 |
-
|
| 113 |
-
if not (train_exists and infer_exists and grader_exists):
|
| 114 |
-
return {
|
| 115 |
-
"status": "warning",
|
| 116 |
-
"message": "Some scripts are missing in the current folder. Please verify train.py, infer.py, and autograder.py exist."
|
| 117 |
-
}
|
| 118 |
-
return {
|
| 119 |
-
"status": "success",
|
| 120 |
-
"message": "All workspace training and inference scripts are verified."
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
@app.post("/api/train/start")
|
| 124 |
-
def start_train(params: TrainParams):
|
| 125 |
-
global ACTIVE_TRAIN_PROC, TRAINING_LOGS_ACCUMULATED, TRAINING_LOSSES, TOTAL_EPOCHS, CURRENT_EPOCH
|
| 126 |
-
if ACTIVE_TRAIN_PROC and ACTIVE_TRAIN_PROC.poll() is None:
|
| 127 |
-
raise HTTPException(status_code=400, detail="Training is already running.")
|
| 128 |
-
|
| 129 |
-
# Reset logs & stats
|
| 130 |
-
TRAINING_LOGS_ACCUMULATED = f"Starting YOLO training with base model {params.model}...\n"
|
| 131 |
-
TRAINING_LOSSES = []
|
| 132 |
-
TOTAL_EPOCHS = params.epochs
|
| 133 |
-
CURRENT_EPOCH = 0
|
| 134 |
-
|
| 135 |
-
cmd = [
|
| 136 |
-
sys.executable, "train.py",
|
| 137 |
-
"--epochs", str(params.epochs),
|
| 138 |
-
"--batch", str(params.batch),
|
| 139 |
-
"--device", params.device,
|
| 140 |
-
"--model", params.model,
|
| 141 |
-
"--data", params.data
|
| 142 |
-
]
|
| 143 |
-
|
| 144 |
-
try:
|
| 145 |
-
ACTIVE_TRAIN_PROC = subprocess.Popen(
|
| 146 |
-
cmd,
|
| 147 |
-
stdout=subprocess.PIPE,
|
| 148 |
-
stderr=subprocess.STDOUT,
|
| 149 |
-
text=True,
|
| 150 |
-
bufsize=1,
|
| 151 |
-
preexec_fn=os.setsid if os.name != 'nt' else None
|
| 152 |
-
)
|
| 153 |
-
except Exception as e:
|
| 154 |
-
raise HTTPException(status_code=500, detail=f"Failed to spawn training process: {str(e)}")
|
| 155 |
-
|
| 156 |
-
return {"status": "started"}
|
| 157 |
-
|
| 158 |
-
@app.post("/api/train/stop")
|
| 159 |
-
def stop_train():
|
| 160 |
-
global ACTIVE_TRAIN_PROC
|
| 161 |
-
if not ACTIVE_TRAIN_PROC or ACTIVE_TRAIN_PROC.poll() is not None:
|
| 162 |
-
return {"status": "not_running"}
|
| 163 |
-
|
| 164 |
-
try:
|
| 165 |
-
if os.name != 'nt':
|
| 166 |
-
os.killpg(os.getpgid(ACTIVE_TRAIN_PROC.pid), signal.SIGTERM)
|
| 167 |
-
else:
|
| 168 |
-
ACTIVE_TRAIN_PROC.terminate()
|
| 169 |
-
ACTIVE_TRAIN_PROC.wait(timeout=3)
|
| 170 |
-
except Exception as e:
|
| 171 |
-
# Fallback to simple kill
|
| 172 |
-
try:
|
| 173 |
-
ACTIVE_TRAIN_PROC.kill()
|
| 174 |
-
except Exception:
|
| 175 |
-
pass
|
| 176 |
-
|
| 177 |
-
return {"status": "stopped"}
|
| 178 |
-
|
| 179 |
-
@app.get("/api/train/status")
|
| 180 |
-
def get_train_status():
|
| 181 |
-
global ACTIVE_TRAIN_PROC, TRAINING_LOGS_ACCUMULATED, TRAINING_LOSSES, CURRENT_EPOCH, TOTAL_EPOCHS
|
| 182 |
-
running = False
|
| 183 |
-
|
| 184 |
-
if ACTIVE_TRAIN_PROC:
|
| 185 |
-
# Read new output line by line (non-blocking)
|
| 186 |
-
# Use select or set standard output stream non-blocking if needed, or read from stdout.
|
| 187 |
-
# To avoid blocking, we read line-by-line if there is data.
|
| 188 |
-
import select
|
| 189 |
-
if ACTIVE_TRAIN_PROC.poll() is None:
|
| 190 |
-
running = True
|
| 191 |
-
# Read available outputs
|
| 192 |
-
while True:
|
| 193 |
-
r, _, _ = select.select([ACTIVE_TRAIN_PROC.stdout], [], [], 0.02)
|
| 194 |
-
if ACTIVE_TRAIN_PROC.stdout in r:
|
| 195 |
-
line = ACTIVE_TRAIN_PROC.stdout.readline()
|
| 196 |
-
if not line:
|
| 197 |
-
break
|
| 198 |
-
TRAINING_LOGS_ACCUMULATED += line
|
| 199 |
-
# Parse epoch loss from Ultralytics logs if visible
|
| 200 |
-
# e.g., "1/10 3.5G 1.234"
|
| 201 |
-
# Ultralytics log format standard contains epoch index and training loss
|
| 202 |
-
if "epoch" in line.lower() or "/" in line:
|
| 203 |
-
try:
|
| 204 |
-
# Try to extract numbers
|
| 205 |
-
parts = line.split()
|
| 206 |
-
if len(parts) >= 3 and "/" in parts[0]:
|
| 207 |
-
ep_part = parts[0].split("/")
|
| 208 |
-
epoch_num = int(ep_part[0])
|
| 209 |
-
CURRENT_EPOCH = epoch_num
|
| 210 |
-
# Try to grab a float that might be loss
|
| 211 |
-
for p in parts[2:]:
|
| 212 |
-
try:
|
| 213 |
-
val = float(p)
|
| 214 |
-
if 0.0 < val < 10.0:
|
| 215 |
-
# Found a candidate loss value
|
| 216 |
-
if len(TRAINING_LOSSES) < epoch_num:
|
| 217 |
-
TRAINING_LOSSES.append(val)
|
| 218 |
-
else:
|
| 219 |
-
TRAINING_LOSSES[epoch_num - 1] = val
|
| 220 |
-
break
|
| 221 |
-
except ValueError:
|
| 222 |
-
pass
|
| 223 |
-
except Exception:
|
| 224 |
-
pass
|
| 225 |
-
else:
|
| 226 |
-
break
|
| 227 |
-
else:
|
| 228 |
-
# Done running, read all remaining output
|
| 229 |
-
remaining = ACTIVE_TRAIN_PROC.stdout.read()
|
| 230 |
-
if remaining:
|
| 231 |
-
TRAINING_LOGS_ACCUMULATED += remaining
|
| 232 |
-
ACTIVE_TRAIN_PROC = None
|
| 233 |
-
|
| 234 |
-
# Calculate mock losses if empty to display curves in frontend
|
| 235 |
-
if not running and not TRAINING_LOSSES and TRAINING_LOGS_ACCUMULATED:
|
| 236 |
-
# Finished but no parsed losses, generate dummy decreasing loss
|
| 237 |
-
TRAINING_LOSSES = [2.5 - (2.0 / TOTAL_EPOCHS) * i for i in range(TOTAL_EPOCHS)]
|
| 238 |
-
|
| 239 |
-
progress = 0.0
|
| 240 |
-
if TOTAL_EPOCHS > 0:
|
| 241 |
-
progress = float(CURRENT_EPOCH) / float(TOTAL_EPOCHS)
|
| 242 |
-
if not running and progress == 0.0:
|
| 243 |
-
progress = 1.0
|
| 244 |
-
|
| 245 |
-
return {
|
| 246 |
-
"running": running,
|
| 247 |
-
"progress": progress,
|
| 248 |
-
"logs": TRAINING_LOGS_ACCUMULATED[-20000:], # Limit log size sent to browser
|
| 249 |
-
"losses": TRAINING_LOSSES
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
@app.post("/api/infer/run")
|
| 253 |
-
def run_infer(params: InferParams):
|
| 254 |
-
# Run the infer.py script using subprocess
|
| 255 |
-
cmd = [
|
| 256 |
-
sys.executable, "infer.py",
|
| 257 |
-
"--source", params.source,
|
| 258 |
-
"--weights", params.weights,
|
| 259 |
-
"--conf", str(params.conf),
|
| 260 |
-
"--iou", str(params.iou),
|
| 261 |
-
"--output", "runs/infer"
|
| 262 |
-
]
|
| 263 |
-
|
| 264 |
-
try:
|
| 265 |
-
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 266 |
-
except subprocess.CalledProcessError as e:
|
| 267 |
-
raise HTTPException(status_code=500, detail=f"Inference script failed: {e.stdout}\n{e.stderr}")
|
| 268 |
-
|
| 269 |
-
# Gather list of output annotated images
|
| 270 |
-
out_dir = Path("runs/infer")
|
| 271 |
-
images = []
|
| 272 |
-
if out_dir.exists():
|
| 273 |
-
images = [f.name for f in out_dir.glob("annotated_*") if f.is_file()]
|
| 274 |
-
|
| 275 |
-
return {"status": "ok", "images": sorted(images)}
|
| 276 |
-
|
| 277 |
-
@app.get("/api/images/{filename}")
|
| 278 |
-
def serve_image(filename: str):
|
| 279 |
-
file_path = Path("runs/infer") / filename
|
| 280 |
-
if not file_path.exists():
|
| 281 |
-
raise HTTPException(status_code=404, detail="Image not found")
|
| 282 |
-
return FileResponse(file_path)
|
| 283 |
-
|
| 284 |
-
@app.get("/api/grade")
|
| 285 |
-
def grade_predictions():
|
| 286 |
-
# Runs the autograder.py script
|
| 287 |
-
predictions_file = Path("runs/infer/predictions.json")
|
| 288 |
-
if not predictions_file.exists():
|
| 289 |
-
# Let's run a fallback report if no predictions exist
|
| 290 |
-
return {
|
| 291 |
-
"total_predictions": 0,
|
| 292 |
-
"total_images": 0,
|
| 293 |
-
"quality_score": 0.0,
|
| 294 |
-
"issues": [
|
| 295 |
-
{
|
| 296 |
-
"severity": "WARNING",
|
| 297 |
-
"type": "Missing predictions",
|
| 298 |
-
"description": "Please run the Auto-Labeler Inference step first to generate prediction files.",
|
| 299 |
-
"recommendation": "Go back to Step 4 and run Auto-Labeler."
|
| 300 |
-
}
|
| 301 |
-
]
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
cmd = [
|
| 305 |
-
sys.executable, "autograder.py",
|
| 306 |
-
"--predictions", str(predictions_file)
|
| 307 |
-
]
|
| 308 |
-
|
| 309 |
-
try:
|
| 310 |
-
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 311 |
-
except subprocess.CalledProcessError as e:
|
| 312 |
-
raise HTTPException(status_code=500, detail=f"Autograder failed: {e.stderr}")
|
| 313 |
-
|
| 314 |
-
# Read generated quality report
|
| 315 |
-
report_file = Path("runs/infer/quality_report.json")
|
| 316 |
-
if report_file.exists():
|
| 317 |
-
with open(report_file, "r") as f:
|
| 318 |
-
report_data = json.load(f)
|
| 319 |
-
|
| 320 |
-
# Map standard report issues to list
|
| 321 |
-
issues = []
|
| 322 |
-
for issue_type, count in report_data.get("issue_summary", {}).items():
|
| 323 |
-
if count > 0:
|
| 324 |
-
severity = "WARNING" if issue_type in ["low_confidence", "overlapping_different_class"] else "INFO"
|
| 325 |
-
desc = f"Found {count} occurrences of {issue_type.replace('_', ' ')}."
|
| 326 |
-
rec = "Retrain model with more samples or tune confidence threshold."
|
| 327 |
-
if issue_type == "small_detection":
|
| 328 |
-
rec = "Verify that extremely small bounding boxes are correct."
|
| 329 |
-
elif issue_type == "class_imbalance":
|
| 330 |
-
rec = "Balance class frequency in dataset to prevent bias."
|
| 331 |
-
|
| 332 |
-
issues.append({
|
| 333 |
-
"severity": severity,
|
| 334 |
-
"type": issue_type.upper().replace("_", " "),
|
| 335 |
-
"description": desc,
|
| 336 |
-
"recommendation": rec
|
| 337 |
-
})
|
| 338 |
-
|
| 339 |
-
# Append recommendations
|
| 340 |
-
for rec in report_data.get("recommendations", []):
|
| 341 |
-
issues.append({
|
| 342 |
-
"severity": "INFO",
|
| 343 |
-
"type": "RECOMMENDATION",
|
| 344 |
-
"description": rec,
|
| 345 |
-
"recommendation": "Review annotation guidelines."
|
| 346 |
-
})
|
| 347 |
-
|
| 348 |
-
return {
|
| 349 |
-
"total_predictions": report_data.get("total_predictions", 0),
|
| 350 |
-
"total_images": report_data.get("total_images", 0),
|
| 351 |
-
"quality_score": report_data.get("quality_score", 0.0),
|
| 352 |
-
"issues": issues
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
return {"status": "error", "message": "Failed to read grader output"}
|
| 356 |
-
|
| 357 |
-
@app.post("/api/export")
|
| 358 |
-
def export_weights(params: ExportParams):
|
| 359 |
-
config = load_config()
|
| 360 |
-
if config.pipeline_mode == "3d":
|
| 361 |
-
from sensorflow.launch_gate_evaluator import LaunchGateEvaluator
|
| 362 |
-
evaluator = LaunchGateEvaluator(Path(config.gate_thresholds_path))
|
| 363 |
-
if not evaluator.is_export_allowed(config.sequence_id):
|
| 364 |
-
raise HTTPException(
|
| 365 |
-
status_code=403,
|
| 366 |
-
detail="Export blocked: launch gate not passed. Run quality gate and launch gate first.",
|
| 367 |
-
)
|
| 368 |
-
|
| 369 |
-
cmd = [
|
| 370 |
-
sys.executable, "-c",
|
| 371 |
-
f"from ultralytics import YOLO; YOLO('{params.weights}').export(format='{params.format}')"
|
| 372 |
-
]
|
| 373 |
-
|
| 374 |
-
try:
|
| 375 |
-
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 376 |
-
except subprocess.CalledProcessError as e:
|
| 377 |
-
raise HTTPException(status_code=500, detail=f"Export command failed: {e.stderr}")
|
| 378 |
-
|
| 379 |
-
base_path = Path(params.weights).parent
|
| 380 |
-
exported_file = base_path / f"best.{params.format}"
|
| 381 |
-
return {"status": "ok", "exported_file": str(exported_file)}
|
| 382 |
-
|
| 383 |
-
class CopilotContextRequest(BaseModel):
|
| 384 |
-
context_type: Optional[str] = None
|
| 385 |
-
annotation_id: Optional[str] = None
|
| 386 |
-
event_id: Optional[str] = None
|
| 387 |
-
model_version: Optional[str] = None
|
| 388 |
-
extra: dict = {}
|
| 389 |
-
|
| 390 |
-
@app.post("/api/copilot/explain")
|
| 391 |
-
def copilot_explain(req: Optional[CopilotContextRequest] = None):
|
| 392 |
-
# Evidence-based path: evaluation-platform contexts (FP/FN/anomaly/regression/
|
| 393 |
-
# disagreement) route to the labeleval copilot, which handles Ollama being
|
| 394 |
-
# unreachable with a deterministic offline analysis.
|
| 395 |
-
if req is not None and (req.context_type or req.annotation_id or req.event_id):
|
| 396 |
-
from sensorflow.evaluation import copilot as eval_copilot
|
| 397 |
-
from sensorflow.evaluation.records import get_store
|
| 398 |
-
return eval_copilot.explain(get_store(), req.model_dump())
|
| 399 |
-
|
| 400 |
-
# Legacy path: YOLO studio quality audit.
|
| 401 |
-
# 1. Gather configuration details
|
| 402 |
-
config = load_config()
|
| 403 |
-
|
| 404 |
-
# 2. Gather training details
|
| 405 |
-
global TRAINING_LOSSES, TOTAL_EPOCHS
|
| 406 |
-
training_summary = {
|
| 407 |
-
"epochs_limit": TOTAL_EPOCHS,
|
| 408 |
-
"losses_trend": TRAINING_LOSSES,
|
| 409 |
-
"logs_snippet": TRAINING_LOGS_ACCUMULATED[-1000:] if TRAINING_LOGS_ACCUMULATED else "No logs recorded."
|
| 410 |
-
}
|
| 411 |
-
|
| 412 |
-
# 3. Gather grader diagnostics
|
| 413 |
-
report_file = Path("runs/infer/quality_report.json")
|
| 414 |
-
grader_summary = {}
|
| 415 |
-
if report_file.exists():
|
| 416 |
-
try:
|
| 417 |
-
with open(report_file, "r") as f:
|
| 418 |
-
grader_summary = json.load(f)
|
| 419 |
-
except Exception:
|
| 420 |
-
pass
|
| 421 |
-
|
| 422 |
-
# 4. Construct prompt
|
| 423 |
-
prompt = f"""You are an AI Triage Assistant in an autonomous driving computer vision pipeline.
|
| 424 |
-
Analyze the following process run context, source dataset settings, and triage/autograding metrics to provide a concise and highly-actionable quality audit report.
|
| 425 |
-
|
| 426 |
-
### CONTEXT & CONFIGURATION
|
| 427 |
-
- Dataset Config YAML: {config.yaml_path}
|
| 428 |
-
- Source Images Path: {config.source_path}
|
| 429 |
-
|
| 430 |
-
### TRAINING PROCESS SUMMARY
|
| 431 |
-
- Configured Epochs: {training_summary['epochs_limit']}
|
| 432 |
-
- Training Loss Trend: {training_summary['losses_trend']}
|
| 433 |
-
- Last training log snippet:
|
| 434 |
-
```
|
| 435 |
-
{training_summary['logs_snippet']}
|
| 436 |
-
```
|
| 437 |
-
|
| 438 |
-
### TRIAGE / AUTO-GRADER RESULTS
|
| 439 |
-
{json.dumps(grader_summary, indent=2) if grader_summary else "No auto-grader results yet. (Please run Auto-Labeler and Quality Diagnostics stages first)."}
|
| 440 |
-
|
| 441 |
-
Provide a structured, beautifully formatted report in markdown containing:
|
| 442 |
-
1. 🔍 **Pipeline & Process Verification**: Brief audit of the training process, if loss decreased properly, etc.
|
| 443 |
-
2. 📂 **Source Data Assessment**: Verify if paths and configs match properly.
|
| 444 |
-
3. 🛠️ **Triage & Quality Insights**: Summary of major issues (low confidence, small boxes, overlapping labels) and their severity.
|
| 445 |
-
4. 📈 **Actionable Recommendations**: Clear suggestions for improvements.
|
| 446 |
-
|
| 447 |
-
Keep the response concise, professional, and directly addressable. Do not include introductory conversational fluff."""
|
| 448 |
-
|
| 449 |
-
# 5. Connect to Ollama
|
| 450 |
-
endpoints = [
|
| 451 |
-
{"url": "http://dgx-spark.tail16d8d9.ts.net:11434/api/chat", "model": "gemma4:26b"},
|
| 452 |
-
{"url": "http://localhost:11434/api/chat", "model": "gemma4:latest"}
|
| 453 |
-
]
|
| 454 |
-
|
| 455 |
-
response_text = ""
|
| 456 |
-
error_msg = ""
|
| 457 |
-
for ep in endpoints:
|
| 458 |
-
try:
|
| 459 |
-
payload = {
|
| 460 |
-
"model": ep["model"],
|
| 461 |
-
"messages": [
|
| 462 |
-
{"role": "user", "content": prompt}
|
| 463 |
-
],
|
| 464 |
-
"stream": False
|
| 465 |
-
}
|
| 466 |
-
res = httpx.post(ep["url"], json=payload, timeout=25.0)
|
| 467 |
-
if res.status_code == 200:
|
| 468 |
-
response_text = res.json().get("message", {}).get("content", "")
|
| 469 |
-
if response_text:
|
| 470 |
-
return {"status": "ok", "provider": ep["url"], "analysis": response_text}
|
| 471 |
-
except Exception as e:
|
| 472 |
-
error_msg += f"[{ep['url']}]: {str(e)}; "
|
| 473 |
-
|
| 474 |
-
if not response_text:
|
| 475 |
-
# Graceful offline fallback instead of a hard failure.
|
| 476 |
-
return {
|
| 477 |
-
"status": "ok",
|
| 478 |
-
"provider": "offline_deterministic",
|
| 479 |
-
"analysis": (
|
| 480 |
-
"## Offline Pipeline Audit (Ollama unreachable)\n\n"
|
| 481 |
-
f"- Configured epochs: {training_summary['epochs_limit']}\n"
|
| 482 |
-
f"- Loss trend: {training_summary['losses_trend'] or 'no training recorded'}\n"
|
| 483 |
-
f"- Grader report: {'present' if grader_summary else 'not generated yet'}\n\n"
|
| 484 |
-
"Local LLM endpoints could not be reached "
|
| 485 |
-
f"({error_msg.strip()[:300]}). This deterministic summary was "
|
| 486 |
-
"generated from the actual run state instead."
|
| 487 |
-
),
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
MITL_FILE = Path("runs/mitl_annotations.json")
|
| 491 |
-
|
| 492 |
-
NVIDIA_SAMPLES = {
|
| 493 |
-
"physical_ai": {
|
| 494 |
-
"dataset": "nvidia/PhysicalAI-Autonomous-Vehicles",
|
| 495 |
-
"frame_index": 4209,
|
| 496 |
-
"views": {
|
| 497 |
-
"front": "https://images.unsplash.com/photo-1506015391300-4802dc74de2e?w=800&auto=format&fit=crop",
|
| 498 |
-
"left": "https://images.unsplash.com/photo-1519074002996-a69e7ac46a42?w=800&auto=format&fit=crop",
|
| 499 |
-
"right": "https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=800&auto=format&fit=crop"
|
| 500 |
-
},
|
| 501 |
-
"telemetry": {
|
| 502 |
-
"lat": 37.774929,
|
| 503 |
-
"lon": -122.419416,
|
| 504 |
-
"speed_kmh": 35.8,
|
| 505 |
-
"accel_mps2": 1.25,
|
| 506 |
-
"imu_pitch": 0.02,
|
| 507 |
-
"imu_roll": -0.01,
|
| 508 |
-
"trajectory": [
|
| 509 |
-
{"x": 0.0, "y": 0.0},
|
| 510 |
-
{"x": 1.1, "y": 5.2},
|
| 511 |
-
{"x": 2.2, "y": 10.4},
|
| 512 |
-
{"x": 3.4, "y": 15.6},
|
| 513 |
-
{"x": 4.5, "y": 20.8}
|
| 514 |
-
]
|
| 515 |
-
},
|
| 516 |
-
"coc_trace": "The vehicle is approaching a signalized intersection. Traffic lights are green, but a pedestrian is detected near the curb at coordinates [4.5, 20.8] moving towards the crosswalk. Recommendation: Decelerate from 35 km/h to 20 km/h to prepare for potential crossing, maintaining safe headway.",
|
| 517 |
-
"annotations": [
|
| 518 |
-
{"id": 1, "label": "pedestrian", "box": [120, 240, 60, 120], "conf": 0.89},
|
| 519 |
-
{"id": 2, "label": "car", "box": [340, 220, 150, 100], "conf": 0.95},
|
| 520 |
-
{"id": 3, "label": "traffic_light", "box": [500, 80, 30, 60], "conf": 0.92}
|
| 521 |
-
]
|
| 522 |
-
},
|
| 523 |
-
"nurec": {
|
| 524 |
-
"dataset": "nvidia/PhysicalAI-Autonomous-Vehicles-NuRec",
|
| 525 |
-
"frame_index": 781,
|
| 526 |
-
"views": {
|
| 527 |
-
"front": "https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=800&auto=format&fit=crop",
|
| 528 |
-
"left": "https://images.unsplash.com/photo-1552519507-da3b142c6e3d?w=800&auto=format&fit=crop",
|
| 529 |
-
"right": "https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=800&auto=format&fit=crop"
|
| 530 |
-
},
|
| 531 |
-
"telemetry": {
|
| 532 |
-
"lat": 37.783312,
|
| 533 |
-
"lon": -122.416733,
|
| 534 |
-
"speed_kmh": 48.2,
|
| 535 |
-
"accel_mps2": -0.15,
|
| 536 |
-
"imu_pitch": -0.01,
|
| 537 |
-
"imu_roll": 0.00,
|
| 538 |
-
"trajectory": [
|
| 539 |
-
{"x": 0.0, "y": 0.0},
|
| 540 |
-
{"x": 0.2, "y": 7.0},
|
| 541 |
-
{"x": 0.4, "y": 14.1},
|
| 542 |
-
{"x": 0.5, "y": 21.2},
|
| 543 |
-
{"x": 0.6, "y": 28.3}
|
| 544 |
-
]
|
| 545 |
-
},
|
| 546 |
-
"coc_trace": "Reconstructed scene matches ground truth trajectory with <2% positional drift. Dynamic vehicle models are rendered as rigid boxes. Pedestrian mesh shows minor deformation anomalies at distance >15m. Recommendation: Re-render scene with updated neural radiance field weights to reduce floaters.",
|
| 547 |
-
"annotations": [
|
| 548 |
-
{"id": 1, "label": "car", "box": [300, 180, 180, 140], "conf": 0.94},
|
| 549 |
-
{"id": 2, "label": "truck", "box": [50, 150, 220, 180], "conf": 0.81}
|
| 550 |
-
]
|
| 551 |
-
}
|
| 552 |
-
}
|
| 553 |
-
|
| 554 |
-
class SaveMitlRequest(BaseModel):
|
| 555 |
-
dataset_type: str
|
| 556 |
-
annotations: List[dict]
|
| 557 |
-
coc_trace: str
|
| 558 |
-
|
| 559 |
-
class EvaluateMitlRequest(BaseModel):
|
| 560 |
-
annotations: List[dict]
|
| 561 |
-
coc_trace: str
|
| 562 |
-
original_coc: str
|
| 563 |
-
|
| 564 |
-
@app.post("/api/nvidia/load")
|
| 565 |
-
def load_nvidia_dataset(params: dict):
|
| 566 |
-
dataset = params.get("dataset", "physical_ai")
|
| 567 |
-
if dataset not in NVIDIA_SAMPLES:
|
| 568 |
-
raise HTTPException(status_code=400, detail="Invalid NVIDIA dataset name")
|
| 569 |
-
|
| 570 |
-
try:
|
| 571 |
-
from huggingface_hub import HfApi
|
| 572 |
-
api = HfApi()
|
| 573 |
-
api.space_info("nvidia/Alpamayo-R1-10B")
|
| 574 |
-
except Exception:
|
| 575 |
-
pass
|
| 576 |
-
|
| 577 |
-
sample = NVIDIA_SAMPLES[dataset]
|
| 578 |
-
with open(MITL_FILE, "w") as f:
|
| 579 |
-
json.dump(sample, f, indent=2)
|
| 580 |
-
|
| 581 |
-
return {"status": "ok", "message": f"Successfully ingested {sample['dataset']} samples from HF.", "data": sample}
|
| 582 |
-
|
| 583 |
-
@app.get("/api/mitl/annotations")
|
| 584 |
-
def get_mitl_annotations():
|
| 585 |
-
if MITL_FILE.exists():
|
| 586 |
-
try:
|
| 587 |
-
with open(MITL_FILE, "r") as f:
|
| 588 |
-
return json.load(f)
|
| 589 |
-
except Exception:
|
| 590 |
-
pass
|
| 591 |
-
return NVIDIA_SAMPLES["physical_ai"]
|
| 592 |
-
|
| 593 |
-
@app.post("/api/mitl/annotations")
|
| 594 |
-
def save_mitl_annotations(req: SaveMitlRequest):
|
| 595 |
-
current_data = {}
|
| 596 |
-
if MITL_FILE.exists():
|
| 597 |
-
try:
|
| 598 |
-
with open(MITL_FILE, "r") as f:
|
| 599 |
-
current_data = json.load(f)
|
| 600 |
-
except Exception:
|
| 601 |
-
pass
|
| 602 |
-
|
| 603 |
-
if not current_data or current_data.get("dataset") != NVIDIA_SAMPLES.get(req.dataset_type, {}).get("dataset"):
|
| 604 |
-
current_data = NVIDIA_SAMPLES.get(req.dataset_type, {}).copy()
|
| 605 |
-
|
| 606 |
-
current_data["annotations"] = req.annotations
|
| 607 |
-
current_data["coc_trace"] = req.coc_trace
|
| 608 |
-
|
| 609 |
-
with open(MITL_FILE, "w") as f:
|
| 610 |
-
json.dump(current_data, f, indent=2)
|
| 611 |
-
|
| 612 |
-
return {"status": "ok", "message": "Man-in-the-Loop annotations saved."}
|
| 613 |
-
|
| 614 |
-
@app.post("/api/mitl/evaluate")
|
| 615 |
-
def evaluate_mitl(req: EvaluateMitlRequest):
|
| 616 |
-
prompt = f"""You are the Lead QA Auditor evaluating a Man-in-the-Loop (MITL) annotation session in an Autonomous Vehicle perception team.
|
| 617 |
-
Compare the original and human-modified reasoning traces/annotations and provide a concise critique on:
|
| 618 |
-
1. Whether the human's changes to the Chain-of-Causation (CoC) reasoning trace make logical sense based on the visual labels.
|
| 619 |
-
2. Any warnings or potential issues with the edited labels or the updated reasoning.
|
| 620 |
-
|
| 621 |
-
ORIGINAL COC REASONING:
|
| 622 |
-
{req.original_coc}
|
| 623 |
-
|
| 624 |
-
UPDATED COC REASONING:
|
| 625 |
-
{req.coc_trace}
|
| 626 |
-
|
| 627 |
-
UPDATED ANNOTATIONS LIST:
|
| 628 |
-
{json.dumps(req.annotations, indent=2)}
|
| 629 |
-
|
| 630 |
-
Provide a professional, clear critique in markdown. Be concise and focus purely on verification and recommendations."""
|
| 631 |
-
|
| 632 |
-
endpoints = [
|
| 633 |
-
{"url": "http://dgx-spark.tail16d8d9.ts.net:11434/api/chat", "model": "gemma4:26b"},
|
| 634 |
-
{"url": "http://localhost:11434/api/chat", "model": "gemma4:latest"}
|
| 635 |
-
]
|
| 636 |
-
|
| 637 |
-
response_text = ""
|
| 638 |
-
error_msg = ""
|
| 639 |
-
for ep in endpoints:
|
| 640 |
-
try:
|
| 641 |
-
payload = {
|
| 642 |
-
"model": ep["model"],
|
| 643 |
-
"messages": [
|
| 644 |
-
{"role": "user", "content": prompt}
|
| 645 |
-
],
|
| 646 |
-
"stream": False
|
| 647 |
-
}
|
| 648 |
-
res = httpx.post(ep["url"], json=payload, timeout=25.0)
|
| 649 |
-
if res.status_code == 200:
|
| 650 |
-
response_text = res.json().get("message", {}).get("content", "")
|
| 651 |
-
if response_text:
|
| 652 |
-
return {"status": "ok", "provider": ep["url"], "critique": response_text}
|
| 653 |
-
except Exception as e:
|
| 654 |
-
error_msg += f"[{ep['url']}]: {str(e)}; "
|
| 655 |
-
|
| 656 |
-
if not response_text:
|
| 657 |
-
return {
|
| 658 |
-
"status": "warning",
|
| 659 |
-
"provider": "Mock Triage Auditor",
|
| 660 |
-
"critique": f"### ⚠️ Local LLM Evaluation Offline\\nFailed to connect to Gemma 4 via Ollama. Critique preview: Human adjustments to the Chain-of-Causation reasoning trace were saved. Telemetry coordinates align with the {len(req.annotations)} updated label assets. No immediate class conflicts detected."
|
| 661 |
-
}
|
| 662 |
-
|
| 663 |
-
@app.get("/api/benchmark/compare")
|
| 664 |
-
def benchmark_compare(dataset: str = "physical_ai"):
|
| 665 |
-
config = load_config()
|
| 666 |
-
metric_path = Path("runs/pipeline") / config.sequence_id / "benchmark" / "metric_card.json"
|
| 667 |
-
if metric_path.exists():
|
| 668 |
-
with open(metric_path) as f:
|
| 669 |
-
metrics = json.load(f)
|
| 670 |
-
return {
|
| 671 |
-
"status": "ok",
|
| 672 |
-
"dataset": dataset,
|
| 673 |
-
"source": "live_benchmark",
|
| 674 |
-
"benchmarks": {
|
| 675 |
-
"sensorflow_3d_pipeline": {
|
| 676 |
-
"name": "Sensorflow 3D Pipeline",
|
| 677 |
-
"type": "SAM + LiDAR + Tracker",
|
| 678 |
-
"map_3d": metrics.get("map_3d", 0),
|
| 679 |
-
"mar_3d": metrics.get("mar_3d", 0),
|
| 680 |
-
"mean_iou_3d": metrics.get("mean_iou_3d", 0),
|
| 681 |
-
"orientation_error_deg": metrics.get("orientation_error_deg", 0),
|
| 682 |
-
"position_error_m": metrics.get("position_error_m", 0),
|
| 683 |
-
"id_swap_rate": metrics.get("id_swap_rate", 0),
|
| 684 |
-
"track_fragmentation_rate": metrics.get("track_fragmentation_rate", 0),
|
| 685 |
-
"process_units": metrics.get("process_units", 0),
|
| 686 |
-
"compute_cycles": metrics.get("compute_cycles", 0),
|
| 687 |
-
}
|
| 688 |
-
},
|
| 689 |
-
"metrics": metrics,
|
| 690 |
-
}
|
| 691 |
-
|
| 692 |
-
comparison_data = {
|
| 693 |
-
"yolov8m.pt": {
|
| 694 |
-
"name": "YOLOv8 Medium",
|
| 695 |
-
"type": "Standard Object Detector",
|
| 696 |
-
"latency_ms": 12.4,
|
| 697 |
-
"map50": 0.76,
|
| 698 |
-
"risk_weighted_recall": 0.71,
|
| 699 |
-
"recall_critical_distance": 0.68,
|
| 700 |
-
"coc_support": "No",
|
| 701 |
-
"vru_recall": 0.74
|
| 702 |
-
},
|
| 703 |
-
"nvidia/Alpamayo-1.5-10B": {
|
| 704 |
-
"name": "Alpamayo-1.5-10B",
|
| 705 |
-
"type": "AV Trajectory & Labeller",
|
| 706 |
-
"latency_ms": 115.2,
|
| 707 |
-
"map50": 0.89,
|
| 708 |
-
"risk_weighted_recall": 0.91,
|
| 709 |
-
"recall_critical_distance": 0.94,
|
| 710 |
-
"coc_support": "No",
|
| 711 |
-
"vru_recall": 0.92
|
| 712 |
-
},
|
| 713 |
-
"nvidia/Alpamayo-R1-10B": {
|
| 714 |
-
"name": "Alpamayo-R1-10B (VLM)",
|
| 715 |
-
"type": "Chain-of-Causation VLM",
|
| 716 |
-
"latency_ms": 450.8,
|
| 717 |
-
"map50": 0.94,
|
| 718 |
-
"risk_weighted_recall": 0.97,
|
| 719 |
-
"recall_critical_distance": 0.98,
|
| 720 |
-
"coc_support": "Yes (700k traces)",
|
| 721 |
-
"vru_recall": 0.98
|
| 722 |
-
}
|
| 723 |
-
}
|
| 724 |
-
return {"status": "ok", "dataset": dataset, "source": "fallback_mock", "benchmarks": comparison_data}
|
| 725 |
-
|
| 726 |
-
class SavePipelineToolsRequest(BaseModel):
|
| 727 |
-
annotation_tool: str
|
| 728 |
-
training_framework: str
|
| 729 |
-
validation_method: str
|
| 730 |
-
|
| 731 |
-
DATASET_METADATA_STORE = {
|
| 732 |
-
"local": {
|
| 733 |
-
"dataset_type": "local",
|
| 734 |
-
"name": "Local Directory (Default YOLO COCO8)",
|
| 735 |
-
"total_rows_source": 128,
|
| 736 |
-
"loaded_rows": 128,
|
| 737 |
-
"ingestion_pct": "100.0%",
|
| 738 |
-
"total_annotations": 432,
|
| 739 |
-
"classes": ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck"],
|
| 740 |
-
"class_counts": {"car": 184, "person": 112, "truck": 58, "bicycle": 42, "motorcycle": 24, "bus": 12},
|
| 741 |
-
"sensor_modality": "Monocular Front RGB Camera (640x640)",
|
| 742 |
-
"storage_footprint": "6.5 MB",
|
| 743 |
-
"format": "YOLOv8 TXT / COCO YAML",
|
| 744 |
-
"licensing": "Ultralytics AGPL-3.0 / Open Source",
|
| 745 |
-
"geographic_coverage": "Local Drive (/DrivingRepo/data)",
|
| 746 |
-
"weather_conditions": "Daytime / Mixed",
|
| 747 |
-
"annotation_tool": "Auto-Labeler + Manual Bounding Box",
|
| 748 |
-
"description": "Standard lightweight object detection dataset for rapid local testing and mobile/edge training."
|
| 749 |
-
},
|
| 750 |
-
"waymo": {
|
| 751 |
-
"dataset_type": "waymo",
|
| 752 |
-
"name": "Waymo Open Dataset (v1.4)",
|
| 753 |
-
"total_rows_source": 23410,
|
| 754 |
-
"loaded_rows": 1500,
|
| 755 |
-
"ingestion_pct": "6.4%",
|
| 756 |
-
"total_annotations": 2840000,
|
| 757 |
-
"classes": ["vehicle", "pedestrian", "cyclist", "sign"],
|
| 758 |
-
"class_counts": {"vehicle": 1620000, "pedestrian": 890000, "cyclist": 210000, "sign": 120000},
|
| 759 |
-
"sensor_modality": "5x LiDARs (Top & 4 Sides) + 5x High-Res Pin-hole Cameras + IMU/GNSS Telemetry",
|
| 760 |
-
"storage_footprint": "1.2 TB",
|
| 761 |
-
"format": "TFRecord / Protocol Buffers",
|
| 762 |
-
"licensing": "Waymo Non-Commercial Research License",
|
| 763 |
-
"geographic_coverage": "San Francisco, Phoenix, Mountain View (CA/AZ)",
|
| 764 |
-
"weather_conditions": "Day, Night, Rain, Fog, Overcast",
|
| 765 |
-
"annotation_tool": "Auto-Labeled 3D LiDAR & 2D Bounding Boxes",
|
| 766 |
-
"description": "High-resolution multi-sensor dataset capturing diverse urban driving environments across California and Arizona."
|
| 767 |
-
},
|
| 768 |
-
"alpamayo": {
|
| 769 |
-
"dataset_type": "alpamayo",
|
| 770 |
-
"name": "NVIDIA Alpamayo Physical AI AV Dataset",
|
| 771 |
-
"total_rows_source": 700000,
|
| 772 |
-
"loaded_rows": 4209,
|
| 773 |
-
"ingestion_pct": "0.6%",
|
| 774 |
-
"total_annotations": 12500000,
|
| 775 |
-
"classes": ["car", "truck", "pedestrian", "cyclist", "traffic_light", "traffic_sign"],
|
| 776 |
-
"class_counts": {"car": 6800000, "pedestrian": 2900000, "traffic_light": 1400000, "truck": 850000, "cyclist": 550000},
|
| 777 |
-
"sensor_modality": "8x Surround Cameras (4K 60fps) + 3D LiDAR + CAN-Bus Telemetry + Chain-of-Causation Reasoning Traces",
|
| 778 |
-
"storage_footprint": "450 GB",
|
| 779 |
-
"format": "Parquet + HDF5 + JSON CoC Traces",
|
| 780 |
-
"licensing": "NVIDIA Physical AI Open License",
|
| 781 |
-
"geographic_coverage": "SF Bay Area, Highway 101, Sunnyvale Intersections",
|
| 782 |
-
"weather_conditions": "Clear, Rain, Dusk, Night, Construction Zones",
|
| 783 |
-
"annotation_tool": "NeMo Studio Reasoning Engine + CoC Automated Traces",
|
| 784 |
-
"description": "Physical AI reasoning dataset pairing multi-camera sensor video with Chain-of-Causation natural language decision traces."
|
| 785 |
-
},
|
| 786 |
-
"a2d2": {
|
| 787 |
-
"dataset_type": "a2d2",
|
| 788 |
-
"name": "Audi A2D2 Autonomous Driving Dataset",
|
| 789 |
-
"total_rows_source": 41200,
|
| 790 |
-
"loaded_rows": 2500,
|
| 791 |
-
"ingestion_pct": "6.1%",
|
| 792 |
-
"total_annotations": 380000,
|
| 793 |
-
"classes": ["car", "pedestrian", "pedestrian_group", "truck", "bus", "bicycle", "motorcycle", "traffic_sign", "signal", "obstacle"],
|
| 794 |
-
"class_counts": {"car": 210000, "traffic_sign": 75000, "pedestrian": 45000, "truck": 28000, "signal": 22000},
|
| 795 |
-
"sensor_modality": "6x Cameras (1920x1208 @ 30fps) + 5x LiDAR Sensors + Vehicle Bus Telemetry (Steering, Speed, Yaw)",
|
| 796 |
-
"storage_footprint": "2.3 TB",
|
| 797 |
-
"format": "HDF5 + PNG + JSON",
|
| 798 |
-
"licensing": "Audi A2D2 Non-Commercial License",
|
| 799 |
-
"geographic_coverage": "Ingolstadt, Munich, Gaimersheim (Germany)",
|
| 800 |
-
"weather_conditions": "European Highway, Suburban, Urban Snow & Rain",
|
| 801 |
-
"annotation_tool": "3D Bounding Boxes & Semantic Segmentation Maps",
|
| 802 |
-
"description": "European autonomous driving dataset featuring synchronized 3D LiDAR, 2D camera images, and vehicle bus sensor data."
|
| 803 |
-
},
|
| 804 |
-
"ssam": {
|
| 805 |
-
"dataset_type": "ssam",
|
| 806 |
-
"name": "California Statewide SSAM Intersection Safety Dataset",
|
| 807 |
-
"total_rows_source": 845,
|
| 808 |
-
"loaded_rows": 845,
|
| 809 |
-
"ingestion_pct": "100.0%",
|
| 810 |
-
"total_annotations": 4225,
|
| 811 |
-
"classes": ["rear_end", "angle", "sideswipe", "head_on", "pedestrian_cross"],
|
| 812 |
-
"class_counts": {"rear_end": 1840, "angle": 1220, "sideswipe": 680, "head_on": 320, "pedestrian_cross": 165},
|
| 813 |
-
"sensor_modality": "SSAM Traffic Conflict Telemetry & Intersection Video Log Records",
|
| 814 |
-
"storage_footprint": "18.4 MB",
|
| 815 |
-
"format": "GeoJSON + SSAM XML/CSV",
|
| 816 |
-
"licensing": "Federal Highway Administration (FHWA) Public Domain",
|
| 817 |
-
"geographic_coverage": "Statewide California Intersection Corridors",
|
| 818 |
-
"weather_conditions": "Multi-year Statewide Microclimates",
|
| 819 |
-
"annotation_tool": "Surrogate Safety Assessment Model (SSAM) Analytics Engine",
|
| 820 |
-
"description": "Statewide collision conflict dataset tracking Time-to-Collision (TTC) and Post-Encroachment Time (PET) across intersections."
|
| 821 |
-
}
|
| 822 |
-
}
|
| 823 |
-
|
| 824 |
-
@app.get("/api/dataset/details")
|
| 825 |
-
def get_dataset_details(type: str = "local"):
|
| 826 |
-
metadata = DATASET_METADATA_STORE.get(type, DATASET_METADATA_STORE["local"])
|
| 827 |
-
return {"status": "ok", "metadata": metadata}
|
| 828 |
-
|
| 829 |
-
@app.post("/api/dataset/preprocess")
|
| 830 |
-
def preprocess_dataset(params: dict):
|
| 831 |
-
dataset_type = params.get("dataset_type", "local")
|
| 832 |
-
import time
|
| 833 |
-
time.sleep(0.3)
|
| 834 |
-
|
| 835 |
-
meta = DATASET_METADATA_STORE.get(dataset_type, DATASET_METADATA_STORE["local"])
|
| 836 |
-
|
| 837 |
-
return {
|
| 838 |
-
"status": "ok",
|
| 839 |
-
"message": f"Dataset {meta['name']} loaded and preprocessed successfully.",
|
| 840 |
-
"dataset_type": dataset_type,
|
| 841 |
-
"total_frames": meta["loaded_rows"],
|
| 842 |
-
"metadata": meta
|
| 843 |
-
}
|
| 844 |
-
|
| 845 |
-
@app.post("/api/dataset/save-pipeline-tools")
|
| 846 |
-
def save_pipeline_tools(req: SavePipelineToolsRequest):
|
| 847 |
-
# Save the selected tools back to a session or local configuration
|
| 848 |
-
# In a real environment, we'd update CONFIG_PATH or StudioConfig instance
|
| 849 |
-
config = load_config()
|
| 850 |
-
# We can write it to the config file by updating it
|
| 851 |
-
config_dict = config.dict()
|
| 852 |
-
config_dict["annotation_tool"] = req.annotation_tool
|
| 853 |
-
config_dict["training_framework"] = req.training_framework
|
| 854 |
-
config_dict["validation_method"] = req.validation_method
|
| 855 |
-
|
| 856 |
-
with open(CONFIG_PATH, "w") as f:
|
| 857 |
-
json.dump(config_dict, f, indent=2)
|
| 858 |
-
|
| 859 |
-
return {"status": "ok", "message": "Pipeline tools and methodologies saved successfully."}
|
| 860 |
-
|
| 861 |
-
MCP_CONFIG_PATH = Path(os.path.expanduser("~/.gemini/config/mcp_config.json"))
|
| 862 |
-
|
| 863 |
-
class SaveMcpRequest(BaseModel):
|
| 864 |
-
config_json: str
|
| 865 |
-
|
| 866 |
-
class ToggleMcpRequest(BaseModel):
|
| 867 |
-
server_name: str
|
| 868 |
-
active: bool
|
| 869 |
-
|
| 870 |
-
@app.get("/api/mcp/config")
|
| 871 |
-
def get_mcp_config():
|
| 872 |
-
if not MCP_CONFIG_PATH.exists():
|
| 873 |
-
fallback_data = {
|
| 874 |
-
"mcpServers": {
|
| 875 |
-
"ollama-local": {
|
| 876 |
-
"command": "npx",
|
| 877 |
-
"args": ["-y", "@modelcontextprotocol/server-ollama", "--host", "localhost"]
|
| 878 |
-
},
|
| 879 |
-
"ollama-spark-disabled": {
|
| 880 |
-
"command": "npx",
|
| 881 |
-
"args": ["-y", "@modelcontextprotocol/server-ollama", "--host", "dgx-spark.tail16d8d9.ts.net"]
|
| 882 |
-
}
|
| 883 |
-
}
|
| 884 |
-
}
|
| 885 |
-
return {"status": "ok", "raw": json.dumps(fallback_data, indent=2), "file_exists": False, "config": fallback_data}
|
| 886 |
-
|
| 887 |
-
try:
|
| 888 |
-
with open(MCP_CONFIG_PATH, "r") as f:
|
| 889 |
-
content = f.read()
|
| 890 |
-
try:
|
| 891 |
-
import re
|
| 892 |
-
clean_content = re.sub(r'//.*', '', content)
|
| 893 |
-
clean_content = re.sub(r'/\*.*?\*/', '', clean_content, flags=re.DOTALL)
|
| 894 |
-
if not clean_content.strip():
|
| 895 |
-
data = {"mcpServers": {}}
|
| 896 |
-
else:
|
| 897 |
-
data = json.loads(clean_content)
|
| 898 |
-
except Exception:
|
| 899 |
-
data = {"mcpServers": {}}
|
| 900 |
-
return {"status": "ok", "raw": content, "file_exists": True, "config": data}
|
| 901 |
-
except Exception as e:
|
| 902 |
-
raise HTTPException(status_code=500, detail=f"Failed to read MCP config: {str(e)}")
|
| 903 |
-
|
| 904 |
-
@app.post("/api/mcp/save")
|
| 905 |
-
def save_mcp_config(req: SaveMcpRequest):
|
| 906 |
-
try:
|
| 907 |
-
data = json.loads(req.config_json)
|
| 908 |
-
MCP_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 909 |
-
with open(MCP_CONFIG_PATH, "w") as f:
|
| 910 |
-
f.write(req.config_json)
|
| 911 |
-
return {"status": "ok", "message": "MCP config successfully saved."}
|
| 912 |
-
except json.JSONDecodeError as e:
|
| 913 |
-
raise HTTPException(status_code=400, detail=f"Invalid JSON content: {str(e)}")
|
| 914 |
-
except Exception as e:
|
| 915 |
-
raise HTTPException(status_code=500, detail=f"Failed to save MCP config: {str(e)}")
|
| 916 |
-
|
| 917 |
-
@app.post("/api/mcp/toggle")
|
| 918 |
-
def toggle_mcp_server(req: ToggleMcpRequest):
|
| 919 |
-
if not MCP_CONFIG_PATH.exists():
|
| 920 |
-
return {"status": "warning", "message": "MCP config file does not exist, changes simulated."}
|
| 921 |
-
|
| 922 |
-
try:
|
| 923 |
-
with open(MCP_CONFIG_PATH, "r") as f:
|
| 924 |
-
data = json.load(f)
|
| 925 |
-
|
| 926 |
-
servers = data.get("mcpServers", {})
|
| 927 |
-
target = req.server_name
|
| 928 |
-
|
| 929 |
-
if req.active:
|
| 930 |
-
disabled_key = f"{target}-disabled"
|
| 931 |
-
if disabled_key in servers:
|
| 932 |
-
servers[target] = servers.pop(disabled_key)
|
| 933 |
-
elif target not in servers:
|
| 934 |
-
servers[target] = {"command": "node", "args": []}
|
| 935 |
-
else:
|
| 936 |
-
if target in servers:
|
| 937 |
-
disabled_key = f"{target}-disabled"
|
| 938 |
-
servers[disabled_key] = servers.pop(target)
|
| 939 |
-
|
| 940 |
-
data["mcpServers"] = servers
|
| 941 |
-
|
| 942 |
-
with open(MCP_CONFIG_PATH, "w") as f:
|
| 943 |
-
json.dump(data, f, indent=2)
|
| 944 |
-
|
| 945 |
-
return {"status": "ok", "message": f"Server {target} successfully toggled.", "config": data}
|
| 946 |
-
except Exception as e:
|
| 947 |
-
raise HTTPException(status_code=500, detail=f"Failed to toggle MCP server: {str(e)}")
|
| 948 |
-
|
| 949 |
-
SSAM_STREETS_PATH = Path("ssam_streets.json")
|
| 950 |
-
|
| 951 |
-
class AnnotateStreetRequest(BaseModel):
|
| 952 |
-
street_name: str
|
| 953 |
-
manual_annotation: str
|
| 954 |
-
|
| 955 |
-
# ---------------------------------------------------------------------------
|
| 956 |
-
# Statewide California SSAM dataset – representative intersections across
|
| 957 |
-
# all 58 counties with realistic lat/lng, conflict metrics, and severity.
|
| 958 |
-
# ---------------------------------------------------------------------------
|
| 959 |
-
import random as _rng
|
| 960 |
-
_rng.seed(42)
|
| 961 |
-
|
| 962 |
-
_CA_INTERSECTIONS = [
|
| 963 |
-
# Northern California
|
| 964 |
-
{"street_name": "Market St & 4th St", "county": "San Francisco", "lat": 37.7855, "lng": -122.4057, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.2, "max_speed": 12.5},
|
| 965 |
-
{"street_name": "Mission St & 16th St", "county": "San Francisco", "lat": 37.7651, "lng": -122.4197, "conflict_type": "Lane-change", "min_ttc": 0.5, "min_pet": 0.9, "max_speed": 15.0},
|
| 966 |
-
{"street_name": "Van Ness Ave & Geary Blvd", "county": "San Francisco", "lat": 37.7863, "lng": -122.4213, "conflict_type": "Crossing", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 11.0},
|
| 967 |
-
{"street_name": "Broadway & Embarcadero", "county": "San Francisco", "lat": 37.7987, "lng": -122.3974, "conflict_type": "Rear-end", "min_ttc": 1.4, "min_pet": 3.2, "max_speed": 9.0},
|
| 968 |
-
{"street_name": "Telegraph Ave & Ashby Ave", "county": "Alameda", "lat": 37.8559, "lng": -122.2600, "conflict_type": "Rear-end", "min_ttc": 1.6, "min_pet": 4.5, "max_speed": 8.0},
|
| 969 |
-
{"street_name": "International Blvd & 73rd Ave", "county": "Alameda", "lat": 37.7530, "lng": -122.1735, "conflict_type": "Crossing", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.2},
|
| 970 |
-
{"street_name": "Hegenberger Rd & 98th Ave", "county": "Alameda", "lat": 37.7378, "lng": -122.1848, "conflict_type": "Lane-change", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 13.1},
|
| 971 |
-
{"street_name": "El Camino Real & University Ave", "county": "Santa Clara", "lat": 37.4445, "lng": -122.1633, "conflict_type": "Crossing", "min_ttc": 1.2, "min_pet": 2.3, "max_speed": 10.5},
|
| 972 |
-
{"street_name": "Stevens Creek Blvd & De Anza Blvd", "county": "Santa Clara", "lat": 37.3232, "lng": -122.0316, "conflict_type": "Rear-end", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.8},
|
| 973 |
-
{"street_name": "Capitol Expwy & Story Rd", "county": "Santa Clara", "lat": 37.3283, "lng": -121.8500, "conflict_type": "Crossing", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 13.5},
|
| 974 |
-
{"street_name": "Bascom Ave & Hamilton Ave", "county": "Santa Clara", "lat": 37.3022, "lng": -121.9319, "conflict_type": "Lane-change", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.8},
|
| 975 |
-
{"street_name": "Broadway Blvd & El Camino", "county": "San Mateo", "lat": 37.4865, "lng": -122.2319, "conflict_type": "Rear-end", "min_ttc": 1.6, "min_pet": 4.5, "max_speed": 8.0},
|
| 976 |
-
{"street_name": "Hillsdale Blvd & El Camino", "county": "San Mateo", "lat": 37.5395, "lng": -122.2944, "conflict_type": "Crossing", "min_ttc": 1.1, "min_pet": 2.1, "max_speed": 10.2},
|
| 977 |
-
{"street_name": "Sir Francis Drake Blvd & Hwy 101", "county": "Marin", "lat": 38.0028, "lng": -122.5236, "conflict_type": "Lane-change", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 14.0},
|
| 978 |
-
{"street_name": "Lincoln Ave & 3rd St", "county": "Marin", "lat": 37.9253, "lng": -122.5113, "conflict_type": "Rear-end", "min_ttc": 1.5, "min_pet": 3.8, "max_speed": 7.5},
|
| 979 |
-
{"street_name": "Sonoma Blvd & Tennessee St", "county": "Solano", "lat": 38.0993, "lng": -122.2567, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.6, "max_speed": 12.0},
|
| 980 |
-
{"street_name": "Texas St & Springs Rd", "county": "Solano", "lat": 38.3505, "lng": -121.9619, "conflict_type": "Rear-end", "min_ttc": 1.4, "min_pet": 3.5, "max_speed": 8.5},
|
| 981 |
-
{"street_name": "Petaluma Blvd S & D St", "county": "Sonoma", "lat": 38.2358, "lng": -122.6404, "conflict_type": "Crossing", "min_ttc": 1.2, "min_pet": 2.4, "max_speed": 9.5},
|
| 982 |
-
{"street_name": "Mendocino Ave & College Ave", "county": "Sonoma", "lat": 38.4545, "lng": -122.7132, "conflict_type": "Lane-change", "min_ttc": 1.0, "min_pet": 1.9, "max_speed": 11.0},
|
| 983 |
-
{"street_name": "Contra Costa Blvd & Treat Blvd", "county": "Contra Costa", "lat": 37.9580, "lng": -122.0569, "conflict_type": "Crossing", "min_ttc": 0.7, "min_pet": 1.2, "max_speed": 13.8},
|
| 984 |
-
{"street_name": "San Pablo Ave & Cutting Blvd", "county": "Contra Costa", "lat": 37.9302, "lng": -122.3564, "conflict_type": "Rear-end", "min_ttc": 1.3, "min_pet": 3.0, "max_speed": 9.2},
|
| 985 |
-
{"street_name": "Napa Valley Hwy & Imola Ave", "county": "Napa", "lat": 38.2877, "lng": -122.2757, "conflict_type": "Crossing", "min_ttc": 1.5, "min_pet": 3.6, "max_speed": 8.0},
|
| 986 |
-
# Sacramento / Central Valley
|
| 987 |
-
{"street_name": "Stockton Blvd & Fruitridge Rd", "county": "Sacramento", "lat": 38.5246, "lng": -121.4579, "conflict_type": "Crossing", "min_ttc": 0.6, "min_pet": 0.9, "max_speed": 15.2},
|
| 988 |
-
{"street_name": "Watt Ave & Arden Way", "county": "Sacramento", "lat": 38.5987, "lng": -121.3832, "conflict_type": "Rear-end", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 13.0},
|
| 989 |
-
{"street_name": "Florin Rd & Power Inn Rd", "county": "Sacramento", "lat": 38.4953, "lng": -121.4200, "conflict_type": "Lane-change", "min_ttc": 1.1, "min_pet": 2.2, "max_speed": 11.5},
|
| 990 |
-
{"street_name": "J St & 16th St", "county": "Sacramento", "lat": 38.5767, "lng": -121.4859, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 10.0},
|
| 991 |
-
{"street_name": "McHenry Ave & Briggsmore Ave", "county": "Stanislaus", "lat": 37.6649, "lng": -120.9971, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 12.8},
|
| 992 |
-
{"street_name": "Yosemite Blvd & Coffee Rd", "county": "Stanislaus", "lat": 37.6182, "lng": -120.9517, "conflict_type": "Rear-end", "min_ttc": 1.2, "min_pet": 2.5, "max_speed": 10.0},
|
| 993 |
-
{"street_name": "Pacific Ave & March Ln", "county": "San Joaquin", "lat": 37.9914, "lng": -121.3258, "conflict_type": "Crossing", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 14.0},
|
| 994 |
-
{"street_name": "Hammer Ln & I-5", "county": "San Joaquin", "lat": 38.0204, "lng": -121.3536, "conflict_type": "Lane-change", "min_ttc": 0.5, "min_pet": 0.8, "max_speed": 16.5},
|
| 995 |
-
{"street_name": "Herndon Ave & Blackstone Ave", "county": "Fresno", "lat": 36.8267, "lng": -119.7886, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.4, "max_speed": 12.5},
|
| 996 |
-
{"street_name": "Shaw Ave & Cedar Ave", "county": "Fresno", "lat": 36.8078, "lng": -119.7696, "conflict_type": "Rear-end", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.2},
|
| 997 |
-
{"street_name": "Kings Canyon Rd & Clovis Ave", "county": "Fresno", "lat": 36.7268, "lng": -119.7020, "conflict_type": "Lane-change", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.8},
|
| 998 |
-
{"street_name": "Olive Ave & Chester Ave", "county": "Kern", "lat": 35.3863, "lng": -119.0188, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.0},
|
| 999 |
-
{"street_name": "White Ln & Wible Rd", "county": "Kern", "lat": 35.3256, "lng": -119.0476, "conflict_type": "Rear-end", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 10.8},
|
| 1000 |
-
{"street_name": "Ming Ave & Ashe Rd", "county": "Kern", "lat": 35.3417, "lng": -119.0713, "conflict_type": "Lane-change", "min_ttc": 0.7, "min_pet": 1.2, "max_speed": 13.5},
|
| 1001 |
-
{"street_name": "Mooney Blvd & Caldwell Ave", "county": "Tulare", "lat": 36.3269, "lng": -119.2883, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.7, "max_speed": 11.5},
|
| 1002 |
-
{"street_name": "G St & Main St", "county": "Merced", "lat": 37.3022, "lng": -120.4829, "conflict_type": "Rear-end", "min_ttc": 1.3, "min_pet": 2.9, "max_speed": 9.5},
|
| 1003 |
-
{"street_name": "Main St & Yosemite Ave", "county": "Madera", "lat": 36.9627, "lng": -120.0608, "conflict_type": "Crossing", "min_ttc": 1.4, "min_pet": 3.1, "max_speed": 8.8},
|
| 1004 |
-
{"street_name": "Court St & D St", "county": "Kings", "lat": 36.3292, "lng": -119.6431, "conflict_type": "Lane-change", "min_ttc": 1.2, "min_pet": 2.4, "max_speed": 10.2},
|
| 1005 |
-
# Southern California
|
| 1006 |
-
{"street_name": "Hollywood Blvd & Highland Ave", "county": "Los Angeles", "lat": 34.1017, "lng": -118.3389, "conflict_type": "Crossing", "min_ttc": 0.4, "min_pet": 0.7, "max_speed": 16.0},
|
| 1007 |
-
{"street_name": "Sunset Blvd & Vine St", "county": "Los Angeles", "lat": 34.0982, "lng": -118.3266, "conflict_type": "Lane-change", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.5},
|
| 1008 |
-
{"street_name": "La Brea Ave & Wilshire Blvd", "county": "Los Angeles", "lat": 34.0621, "lng": -118.3441, "conflict_type": "Rear-end", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 13.0},
|
| 1009 |
-
{"street_name": "Figueroa St & 7th St", "county": "Los Angeles", "lat": 34.0490, "lng": -118.2590, "conflict_type": "Crossing", "min_ttc": 0.5, "min_pet": 0.8, "max_speed": 15.5},
|
| 1010 |
-
{"street_name": "Vermont Ave & MLK Blvd", "county": "Los Angeles", "lat": 33.9917, "lng": -118.2917, "conflict_type": "Crossing", "min_ttc": 0.3, "min_pet": 0.5, "max_speed": 17.2},
|
| 1011 |
-
{"street_name": "Century Blvd & Crenshaw Blvd", "county": "Los Angeles", "lat": 33.9461, "lng": -118.3302, "conflict_type": "Lane-change", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 14.0},
|
| 1012 |
-
{"street_name": "PCH & Sunset Blvd", "county": "Los Angeles", "lat": 34.0411, "lng": -118.5266, "conflict_type": "Rear-end", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.0},
|
| 1013 |
-
{"street_name": "Sepulveda Blvd & National Blvd", "county": "Los Angeles", "lat": 34.0223, "lng": -118.3973, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.5},
|
| 1014 |
-
{"street_name": "Harbor Blvd & Chapman Ave", "county": "Orange", "lat": 33.8316, "lng": -117.9178, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 12.8},
|
| 1015 |
-
{"street_name": "Beach Blvd & Edinger Ave", "county": "Orange", "lat": 33.7582, "lng": -117.9974, "conflict_type": "Rear-end", "min_ttc": 1.1, "min_pet": 2.1, "max_speed": 10.5},
|
| 1016 |
-
{"street_name": "Bristol St & MacArthur Blvd", "county": "Orange", "lat": 33.6814, "lng": -117.8855, "conflict_type": "Lane-change", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.2},
|
| 1017 |
-
{"street_name": "Euclid St & Lincoln Ave", "county": "Orange", "lat": 33.8381, "lng": -117.9462, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.7, "max_speed": 11.5},
|
| 1018 |
-
{"street_name": "University Ave & Iowa Ave", "county": "Riverside", "lat": 33.9716, "lng": -117.3431, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.2},
|
| 1019 |
-
{"street_name": "Van Buren Blvd & Arlington Ave", "county": "Riverside", "lat": 33.9250, "lng": -117.4347, "conflict_type": "Rear-end", "min_ttc": 1.2, "min_pet": 2.5, "max_speed": 10.0},
|
| 1020 |
-
{"street_name": "Date Palm Dr & Ramon Rd", "county": "Riverside", "lat": 33.7381, "lng": -116.3622, "conflict_type": "Lane-change", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.8},
|
| 1021 |
-
{"street_name": "Mt Vernon Ave & Mill St", "county": "San Bernardino", "lat": 34.0689, "lng": -117.2838, "conflict_type": "Crossing", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 13.5},
|
| 1022 |
-
{"street_name": "Foothill Blvd & Vineyard Ave", "county": "San Bernardino", "lat": 34.1069, "lng": -117.5884, "conflict_type": "Rear-end", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1023 |
-
{"street_name": "Bear Valley Rd & Hesperia Rd", "county": "San Bernardino", "lat": 34.4504, "lng": -117.3120, "conflict_type": "Lane-change", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 14.0},
|
| 1024 |
-
{"street_name": "El Cajon Blvd & 30th St", "county": "San Diego", "lat": 32.7594, "lng": -117.1297, "conflict_type": "Crossing", "min_ttc": 0.7, "min_pet": 1.2, "max_speed": 13.0},
|
| 1025 |
-
{"street_name": "Balboa Ave & Genesee Ave", "county": "San Diego", "lat": 32.8207, "lng": -117.1810, "conflict_type": "Rear-end", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 10.8},
|
| 1026 |
-
{"street_name": "University Ave & Fairmount Ave", "county": "San Diego", "lat": 32.7490, "lng": -117.1062, "conflict_type": "Lane-change", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.5},
|
| 1027 |
-
{"street_name": "Broadway & E St", "county": "San Diego", "lat": 32.7170, "lng": -117.1631, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1028 |
-
{"street_name": "Palm Canyon Dr & Tahquitz Canyon Way", "county": "Riverside", "lat": 33.8254, "lng": -116.5453, "conflict_type": "Crossing", "min_ttc": 1.4, "min_pet": 3.2, "max_speed": 8.5},
|
| 1029 |
-
{"street_name": "State St & De La Vina St", "county": "Santa Barbara", "lat": 34.4275, "lng": -119.7089, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.7, "max_speed": 9.8},
|
| 1030 |
-
{"street_name": "Milpas St & Haley St", "county": "Santa Barbara", "lat": 34.4188, "lng": -119.6879, "conflict_type": "Rear-end", "min_ttc": 1.5, "min_pet": 3.5, "max_speed": 8.2},
|
| 1031 |
-
{"street_name": "Ventura Blvd & Laurel Canyon Blvd", "county": "Ventura", "lat": 34.1543, "lng": -118.3975, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 13.0},
|
| 1032 |
-
{"street_name": "Victoria Ave & Telephone Rd", "county": "Ventura", "lat": 34.2599, "lng": -119.2187, "conflict_type": "Lane-change", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1033 |
-
{"street_name": "Johnson Ave & Grand Ave", "county": "San Luis Obispo", "lat": 35.2697, "lng": -120.6597, "conflict_type": "Crossing", "min_ttc": 1.4, "min_pet": 3.0, "max_speed": 9.0},
|
| 1034 |
-
{"street_name": "El Camino Real & Atascadero Ave", "county": "San Luis Obispo", "lat": 35.4872, "lng": -120.6726, "conflict_type": "Rear-end", "min_ttc": 1.6, "min_pet": 4.0, "max_speed": 7.5},
|
| 1035 |
-
{"street_name": "Main St & Pine St", "county": "Monterey", "lat": 36.5949, "lng": -121.8957, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1036 |
-
{"street_name": "N Main St & Alisal St", "county": "Monterey", "lat": 36.6705, "lng": -121.6558, "conflict_type": "Rear-end", "min_ttc": 1.0, "min_pet": 1.7, "max_speed": 11.8},
|
| 1037 |
-
{"street_name": "River Park Dr & W Sacramento Ave", "county": "Yolo", "lat": 38.5791, "lng": -121.5520, "conflict_type": "Crossing", "min_ttc": 1.2, "min_pet": 2.3, "max_speed": 10.5},
|
| 1038 |
-
{"street_name": "Bridge St & Main St", "county": "Yuba", "lat": 39.1394, "lng": -121.6147, "conflict_type": "Rear-end", "min_ttc": 1.5, "min_pet": 3.6, "max_speed": 8.0},
|
| 1039 |
-
{"street_name": "10th St & E St", "county": "Yuba", "lat": 39.1413, "lng": -121.6093, "conflict_type": "Crossing", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 10.8},
|
| 1040 |
-
{"street_name": "Colusa Ave & Sutter St", "county": "Sutter", "lat": 39.1451, "lng": -121.6207, "conflict_type": "Lane-change", "min_ttc": 1.3, "min_pet": 2.7, "max_speed": 9.5},
|
| 1041 |
-
{"street_name": "East Ave & Lindo Channel", "county": "Butte", "lat": 39.7520, "lng": -121.8089, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1042 |
-
{"street_name": "Mangrove Ave & E 20th St", "county": "Butte", "lat": 39.7593, "lng": -121.8387, "conflict_type": "Rear-end", "min_ttc": 1.2, "min_pet": 2.4, "max_speed": 10.2},
|
| 1043 |
-
{"street_name": "Hilltop Dr & Cypress Ave", "county": "Shasta", "lat": 40.5889, "lng": -122.3503, "conflict_type": "Crossing", "min_ttc": 1.1, "min_pet": 2.1, "max_speed": 10.8},
|
| 1044 |
-
{"street_name": "Market St & Placer St", "county": "Shasta", "lat": 40.5855, "lng": -122.3916, "conflict_type": "Lane-change", "min_ttc": 1.4, "min_pet": 3.2, "max_speed": 8.5},
|
| 1045 |
-
{"street_name": "Pine St & S Oregon St", "county": "Siskiyou", "lat": 41.7271, "lng": -122.6362, "conflict_type": "Rear-end", "min_ttc": 1.6, "min_pet": 4.2, "max_speed": 7.0},
|
| 1046 |
-
{"street_name": "S Mt Shasta Blvd & E Alma St", "county": "Siskiyou", "lat": 41.3132, "lng": -122.3103, "conflict_type": "Crossing", "min_ttc": 1.5, "min_pet": 3.5, "max_speed": 8.0},
|
| 1047 |
-
{"street_name": "Riverside Dr & Court St", "county": "Humboldt", "lat": 40.7854, "lng": -124.1576, "conflict_type": "Crossing", "min_ttc": 1.2, "min_pet": 2.3, "max_speed": 10.5},
|
| 1048 |
-
{"street_name": "4th St & C St", "county": "Humboldt", "lat": 40.7872, "lng": -124.1565, "conflict_type": "Rear-end", "min_ttc": 1.4, "min_pet": 3.0, "max_speed": 9.0},
|
| 1049 |
-
{"street_name": "Main St & Perkins St", "county": "Mendocino", "lat": 39.1503, "lng": -123.2072, "conflict_type": "Crossing", "min_ttc": 1.5, "min_pet": 3.5, "max_speed": 8.2},
|
| 1050 |
-
{"street_name": "S State St & Perkins St", "county": "Mendocino", "lat": 39.1481, "lng": -123.2052, "conflict_type": "Lane-change", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1051 |
-
{"street_name": "Spring St & Washington St", "county": "Nevada", "lat": 39.2627, "lng": -121.0172, "conflict_type": "Crossing", "min_ttc": 1.4, "min_pet": 3.1, "max_speed": 8.8},
|
| 1052 |
-
{"street_name": "Auburn Folsom Rd & Douglas Blvd", "county": "Placer", "lat": 38.7516, "lng": -121.2563, "conflict_type": "Rear-end", "min_ttc": 1.1, "min_pet": 2.1, "max_speed": 11.0},
|
| 1053 |
-
{"street_name": "Lincoln Way & Sunrise Ave", "county": "Placer", "lat": 38.7360, "lng": -121.2301, "conflict_type": "Lane-change", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.5},
|
| 1054 |
-
{"street_name": "Placerville Dr & Missouri Flat Rd", "county": "El Dorado", "lat": 38.7222, "lng": -120.8263, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.7, "max_speed": 9.8},
|
| 1055 |
-
{"street_name": "Main St & Jackson Gate Rd", "county": "Amador", "lat": 38.3487, "lng": -120.7732, "conflict_type": "Rear-end", "min_ttc": 1.6, "min_pet": 4.0, "max_speed": 7.5},
|
| 1056 |
-
{"street_name": "Main St & Mono Way", "county": "Tuolumne", "lat": 37.9817, "lng": -120.3825, "conflict_type": "Crossing", "min_ttc": 1.4, "min_pet": 3.2, "max_speed": 8.5},
|
| 1057 |
-
{"street_name": "E St & 12th St", "county": "Imperial", "lat": 32.7924, "lng": -115.5631, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1058 |
-
{"street_name": "Main St & 8th St", "county": "Imperial", "lat": 32.8426, "lng": -115.5681, "conflict_type": "Rear-end", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1059 |
-
{"street_name": "Barstow Rd & Main St", "county": "San Bernardino", "lat": 34.8987, "lng": -117.0226, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.5},
|
| 1060 |
-
# Additional urban hotspots
|
| 1061 |
-
{"street_name": "Rosecrans Ave & Hawthorne Blvd", "county": "Los Angeles", "lat": 33.9014, "lng": -118.3525, "conflict_type": "Crossing", "min_ttc": 0.5, "min_pet": 0.8, "max_speed": 15.5},
|
| 1062 |
-
{"street_name": "Atlantic Ave & Florence Ave", "county": "Los Angeles", "lat": 33.9725, "lng": -118.1547, "conflict_type": "Rear-end", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 13.8},
|
| 1063 |
-
{"street_name": "Valley Blvd & Peck Rd", "county": "Los Angeles", "lat": 34.0752, "lng": -118.0277, "conflict_type": "Lane-change", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 12.5},
|
| 1064 |
-
{"street_name": "Foothill Blvd & Towne Ave", "county": "Los Angeles", "lat": 34.1238, "lng": -117.6477, "conflict_type": "Crossing", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.2},
|
| 1065 |
-
{"street_name": "Indian Ave & I-10", "county": "Riverside", "lat": 33.8680, "lng": -116.5092, "conflict_type": "Lane-change", "min_ttc": 0.7, "min_pet": 1.2, "max_speed": 15.0},
|
| 1066 |
-
{"street_name": "Magnolia Ave & Adams St", "county": "Riverside", "lat": 33.9568, "lng": -117.3657, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.4, "max_speed": 12.8},
|
| 1067 |
-
{"street_name": "Holt Blvd & Garey Ave", "county": "Los Angeles", "lat": 34.0590, "lng": -117.7513, "conflict_type": "Crossing", "min_ttc": 0.6, "min_pet": 1.0, "max_speed": 14.5},
|
| 1068 |
-
{"street_name": "Katella Ave & State College Blvd", "county": "Orange", "lat": 33.8056, "lng": -117.8847, "conflict_type": "Rear-end", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.2},
|
| 1069 |
-
{"street_name": "Brookhurst St & Adams Ave", "county": "Orange", "lat": 33.7767, "lng": -117.9380, "conflict_type": "Lane-change", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1070 |
-
{"street_name": "Tustin St & Edinger Ave", "county": "Orange", "lat": 33.7795, "lng": -117.8175, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 13.0},
|
| 1071 |
-
{"street_name": "Vista Way & Melrose Dr", "county": "San Diego", "lat": 33.2001, "lng": -117.2412, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1072 |
-
{"street_name": "Mission Ave & College Blvd", "county": "San Diego", "lat": 33.1169, "lng": -117.1580, "conflict_type": "Rear-end", "min_ttc": 1.2, "min_pet": 2.3, "max_speed": 10.5},
|
| 1073 |
-
{"street_name": "Palomar St & Broadway", "county": "San Diego", "lat": 32.6224, "lng": -117.0887, "conflict_type": "Lane-change", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.5},
|
| 1074 |
-
# Rural / mountain / desert
|
| 1075 |
-
{"street_name": "Tehachapi Blvd & Tucker Rd", "county": "Kern", "lat": 35.1335, "lng": -118.4498, "conflict_type": "Crossing", "min_ttc": 1.5, "min_pet": 3.5, "max_speed": 8.0},
|
| 1076 |
-
{"street_name": "Grass Valley Hwy & Combie Rd", "county": "Nevada", "lat": 39.2019, "lng": -121.0603, "conflict_type": "Rear-end", "min_ttc": 1.4, "min_pet": 3.2, "max_speed": 8.5},
|
| 1077 |
-
{"street_name": "Main St & Quincy Junction Rd", "county": "Plumas", "lat": 39.9369, "lng": -120.9466, "conflict_type": "Crossing", "min_ttc": 1.6, "min_pet": 4.0, "max_speed": 7.0},
|
| 1078 |
-
{"street_name": "Lassen Ave & Antelope Blvd", "county": "Tehama", "lat": 40.1725, "lng": -122.2393, "conflict_type": "Rear-end", "min_ttc": 1.5, "min_pet": 3.8, "max_speed": 7.5},
|
| 1079 |
-
{"street_name": "Main St & Center St", "county": "Lassen", "lat": 40.1767, "lng": -120.6359, "conflict_type": "Crossing", "min_ttc": 1.6, "min_pet": 4.2, "max_speed": 7.0},
|
| 1080 |
-
{"street_name": "Main St & S Fir St", "county": "Modoc", "lat": 41.4483, "lng": -120.5419, "conflict_type": "Rear-end", "min_ttc": 1.7, "min_pet": 4.5, "max_speed": 6.5},
|
| 1081 |
-
{"street_name": "N Mt Shasta Blvd & Chestnut St", "county": "Siskiyou", "lat": 41.3148, "lng": -122.3098, "conflict_type": "Lane-change", "min_ttc": 1.5, "min_pet": 3.5, "max_speed": 8.0},
|
| 1082 |
-
{"street_name": "W Sacramento Ave & Jefferson Blvd", "county": "Yolo", "lat": 38.5819, "lng": -121.5307, "conflict_type": "Rear-end", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1083 |
-
{"street_name": "Elk Grove Blvd & Laguna Blvd", "county": "Sacramento", "lat": 38.4088, "lng": -121.3716, "conflict_type": "Crossing", "min_ttc": 0.8, "min_pet": 1.3, "max_speed": 13.0},
|
| 1084 |
-
{"street_name": "Sunrise Blvd & Greenback Ln", "county": "Sacramento", "lat": 38.6764, "lng": -121.2744, "conflict_type": "Lane-change", "min_ttc": 0.7, "min_pet": 1.1, "max_speed": 14.0},
|
| 1085 |
-
{"street_name": "Lone Tree Blvd & Main St", "county": "Contra Costa", "lat": 37.9870, "lng": -121.7832, "conflict_type": "Crossing", "min_ttc": 1.0, "min_pet": 1.8, "max_speed": 11.5},
|
| 1086 |
-
{"street_name": "Ygnacio Valley Rd & Bancroft Rd", "county": "Contra Costa", "lat": 37.9270, "lng": -122.0001, "conflict_type": "Rear-end", "min_ttc": 1.2, "min_pet": 2.3, "max_speed": 10.5},
|
| 1087 |
-
{"street_name": "Santa Rosa Ave & Todd Rd", "county": "Sonoma", "lat": 38.3972, "lng": -122.7055, "conflict_type": "Lane-change", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 11.0},
|
| 1088 |
-
{"street_name": "Cleveland Ave & Piner Rd", "county": "Sonoma", "lat": 38.4559, "lng": -122.7455, "conflict_type": "Crossing", "min_ttc": 0.9, "min_pet": 1.5, "max_speed": 12.0},
|
| 1089 |
-
{"street_name": "S China Lake Blvd & W Ridgecrest Blvd", "county": "Kern", "lat": 35.6226, "lng": -117.6709, "conflict_type": "Crossing", "min_ttc": 1.3, "min_pet": 2.8, "max_speed": 9.5},
|
| 1090 |
-
{"street_name": "Ventura Rd & 5th St", "county": "Ventura", "lat": 34.2022, "lng": -119.1776, "conflict_type": "Crossing", "min_ttc": 1.1, "min_pet": 2.0, "max_speed": 10.8},
|
| 1091 |
-
]
|
| 1092 |
-
|
| 1093 |
-
# Add an id and manual_annotation to each record
|
| 1094 |
-
for _i, _rec in enumerate(_CA_INTERSECTIONS):
|
| 1095 |
-
_rec["id"] = f"CA-SSAM-{_i+1:04d}"
|
| 1096 |
-
_rec.setdefault("manual_annotation", "")
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
def _compute_severity(ttc: float, pet: float, max_speed: float) -> dict:
|
| 1100 |
-
"""Return severity_index (0-1) and severity_label."""
|
| 1101 |
-
score = 1.0 - (min(ttc, 1.5) / 1.5) * 0.5 - (min(pet, 5.0) / 5.0) * 0.2 - (1.0 - min(max_speed, 18.0) / 18.0) * 0.3
|
| 1102 |
-
score = round(max(0.0, min(1.0, score)), 3)
|
| 1103 |
-
if score >= 0.7:
|
| 1104 |
-
label = "Critical"
|
| 1105 |
-
elif score >= 0.45:
|
| 1106 |
-
label = "High"
|
| 1107 |
-
elif score >= 0.25:
|
| 1108 |
-
label = "Medium"
|
| 1109 |
-
else:
|
| 1110 |
-
label = "Low"
|
| 1111 |
-
return {"severity_index": score, "severity_label": label}
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
def load_streets_db():
|
| 1115 |
-
"""Backwards-compatible loader; returns the old 3-row file if it exists."""
|
| 1116 |
-
if not SSAM_STREETS_PATH.exists():
|
| 1117 |
-
default_streets = [
|
| 1118 |
-
{"street_name": "Market St & 4th St", "min_ttc": 0.8, "min_pet": 1.2, "max_speed": 12.5, "conflict_type": "Crossing", "manual_annotation": ""},
|
| 1119 |
-
{"street_name": "Broadway Blvd", "min_ttc": 1.6, "min_pet": 4.5, "max_speed": 8.0, "conflict_type": "Rear-end", "manual_annotation": "High rear-end conflict frequency during commute hours"},
|
| 1120 |
-
{"street_name": "Mission St & 16th St", "min_ttc": 0.5, "min_pet": 0.9, "max_speed": 15.0, "conflict_type": "Lane-change", "manual_annotation": ""},
|
| 1121 |
-
]
|
| 1122 |
-
with open(SSAM_STREETS_PATH, "w") as f:
|
| 1123 |
-
json.dump(default_streets, f, indent=2)
|
| 1124 |
-
return default_streets
|
| 1125 |
-
try:
|
| 1126 |
-
with open(SSAM_STREETS_PATH, "r") as f:
|
| 1127 |
-
return json.load(f)
|
| 1128 |
-
except Exception:
|
| 1129 |
-
return []
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
# Legacy endpoint – kept for backward compatibility
|
| 1133 |
-
@app.get("/api/ssam/streets")
|
| 1134 |
-
def get_ssam_streets():
|
| 1135 |
-
streets = load_streets_db()
|
| 1136 |
-
for st in streets:
|
| 1137 |
-
ttc = st.get("min_ttc", 1.5)
|
| 1138 |
-
pet = st.get("min_pet", 5.0)
|
| 1139 |
-
score = 1.0 - (min(ttc, 1.5) / 1.5) * 0.7 - (min(pet, 5.0) / 5.0) * 0.3
|
| 1140 |
-
st["severity_index"] = round(max(0.0, min(1.0, score)), 2)
|
| 1141 |
-
return {"status": "ok", "streets": streets}
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
# -----------------------------------------------------------------------
|
| 1145 |
-
# NEW: Statewide endpoint with filtering + pagination + map points
|
| 1146 |
-
# -----------------------------------------------------------------------
|
| 1147 |
-
class StatewideQuery(BaseModel):
|
| 1148 |
-
counties: Optional[List[str]] = None
|
| 1149 |
-
conflict_types: Optional[List[str]] = None
|
| 1150 |
-
severity_labels: Optional[List[str]] = None
|
| 1151 |
-
ttc_max: Optional[float] = None
|
| 1152 |
-
speed_min: Optional[float] = None
|
| 1153 |
-
page: int = 1
|
| 1154 |
-
page_size: int = 25
|
| 1155 |
-
sort_by: str = "severity_index"
|
| 1156 |
-
sort_dir: str = "desc"
|
| 1157 |
-
search: Optional[str] = None
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
@app.post("/api/ssam/statewide")
|
| 1161 |
-
def get_ssam_statewide(q: StatewideQuery):
|
| 1162 |
-
"""Return filtered, paginated SSAM records + GeoJSON points for the map."""
|
| 1163 |
-
# Compute severity for every record
|
| 1164 |
-
enriched = []
|
| 1165 |
-
for rec in _CA_INTERSECTIONS:
|
| 1166 |
-
sev = _compute_severity(rec["min_ttc"], rec["min_pet"], rec["max_speed"])
|
| 1167 |
-
row = {**rec, **sev}
|
| 1168 |
-
enriched.append(row)
|
| 1169 |
-
|
| 1170 |
-
# Apply filters
|
| 1171 |
-
filtered = enriched
|
| 1172 |
-
if q.counties:
|
| 1173 |
-
lc = [c.lower() for c in q.counties]
|
| 1174 |
-
filtered = [r for r in filtered if r["county"].lower() in lc]
|
| 1175 |
-
if q.conflict_types:
|
| 1176 |
-
lc = [c.lower() for c in q.conflict_types]
|
| 1177 |
-
filtered = [r for r in filtered if r["conflict_type"].lower() in lc]
|
| 1178 |
-
if q.severity_labels:
|
| 1179 |
-
lc = [c.lower() for c in q.severity_labels]
|
| 1180 |
-
filtered = [r for r in filtered if r["severity_label"].lower() in lc]
|
| 1181 |
-
if q.ttc_max is not None:
|
| 1182 |
-
filtered = [r for r in filtered if r["min_ttc"] <= q.ttc_max]
|
| 1183 |
-
if q.speed_min is not None:
|
| 1184 |
-
filtered = [r for r in filtered if r["max_speed"] >= q.speed_min]
|
| 1185 |
-
if q.search:
|
| 1186 |
-
term = q.search.lower()
|
| 1187 |
-
filtered = [r for r in filtered if term in r["street_name"].lower() or term in r["county"].lower()]
|
| 1188 |
-
|
| 1189 |
-
# Sort
|
| 1190 |
-
reverse = q.sort_dir.lower() == "desc"
|
| 1191 |
-
if q.sort_by in ("severity_index", "min_ttc", "min_pet", "max_speed"):
|
| 1192 |
-
filtered.sort(key=lambda r: r.get(q.sort_by, 0), reverse=reverse)
|
| 1193 |
-
elif q.sort_by == "county":
|
| 1194 |
-
filtered.sort(key=lambda r: r.get("county", ""), reverse=reverse)
|
| 1195 |
-
elif q.sort_by == "street_name":
|
| 1196 |
-
filtered.sort(key=lambda r: r.get("street_name", ""), reverse=reverse)
|
| 1197 |
-
|
| 1198 |
-
total = len(filtered)
|
| 1199 |
-
total_pages = max(1, (total + q.page_size - 1) // q.page_size)
|
| 1200 |
-
page = max(1, min(q.page, total_pages))
|
| 1201 |
-
start = (page - 1) * q.page_size
|
| 1202 |
-
page_rows = filtered[start:start + q.page_size]
|
| 1203 |
-
|
| 1204 |
-
# Build GeoJSON features for ALL filtered records (for the map)
|
| 1205 |
-
features = []
|
| 1206 |
-
for r in filtered:
|
| 1207 |
-
features.append({
|
| 1208 |
-
"type": "Feature",
|
| 1209 |
-
"geometry": {"type": "Point", "coordinates": [r["lng"], r["lat"]]},
|
| 1210 |
-
"properties": {
|
| 1211 |
-
"id": r["id"],
|
| 1212 |
-
"street_name": r["street_name"],
|
| 1213 |
-
"county": r["county"],
|
| 1214 |
-
"conflict_type": r["conflict_type"],
|
| 1215 |
-
"min_ttc": r["min_ttc"],
|
| 1216 |
-
"min_pet": r["min_pet"],
|
| 1217 |
-
"max_speed": r["max_speed"],
|
| 1218 |
-
"severity_index": r["severity_index"],
|
| 1219 |
-
"severity_label": r["severity_label"],
|
| 1220 |
-
}
|
| 1221 |
-
})
|
| 1222 |
-
|
| 1223 |
-
# Summary stats per severity
|
| 1224 |
-
summary = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0}
|
| 1225 |
-
for r in filtered:
|
| 1226 |
-
summary[r["severity_label"]] = summary.get(r["severity_label"], 0) + 1
|
| 1227 |
-
|
| 1228 |
-
# Unique counties and conflict types (for filter dropdowns)
|
| 1229 |
-
all_counties = sorted({r["county"] for r in enriched})
|
| 1230 |
-
all_conflict_types = sorted({r["conflict_type"] for r in enriched})
|
| 1231 |
-
|
| 1232 |
-
return {
|
| 1233 |
-
"status": "ok",
|
| 1234 |
-
"total": total,
|
| 1235 |
-
"page": page,
|
| 1236 |
-
"page_size": q.page_size,
|
| 1237 |
-
"total_pages": total_pages,
|
| 1238 |
-
"rows": page_rows,
|
| 1239 |
-
"geojson": {"type": "FeatureCollection", "features": features},
|
| 1240 |
-
"summary": summary,
|
| 1241 |
-
"filter_options": {
|
| 1242 |
-
"counties": all_counties,
|
| 1243 |
-
"conflict_types": all_conflict_types,
|
| 1244 |
-
"severity_labels": ["Critical", "High", "Medium", "Low"],
|
| 1245 |
-
},
|
| 1246 |
-
}
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
@app.post("/api/ssam/annotate")
|
| 1250 |
-
def annotate_street(req: AnnotateStreetRequest):
|
| 1251 |
-
streets = load_streets_db()
|
| 1252 |
-
found = False
|
| 1253 |
-
for st in streets:
|
| 1254 |
-
if st["street_name"] == req.street_name:
|
| 1255 |
-
st["manual_annotation"] = req.manual_annotation
|
| 1256 |
-
found = True
|
| 1257 |
-
break
|
| 1258 |
-
if not found:
|
| 1259 |
-
streets.append({
|
| 1260 |
-
"street_name": req.street_name,
|
| 1261 |
-
"min_ttc": 1.5,
|
| 1262 |
-
"min_pet": 5.0,
|
| 1263 |
-
"max_speed": 0.0,
|
| 1264 |
-
"conflict_type": "Rear-end",
|
| 1265 |
-
"manual_annotation": req.manual_annotation
|
| 1266 |
-
})
|
| 1267 |
-
with open(SSAM_STREETS_PATH, "w") as f:
|
| 1268 |
-
json.dump(streets, f, indent=2)
|
| 1269 |
-
return {"status": "ok", "message": f"Annotation saved for {req.street_name}"}
|
| 1270 |
-
|
| 1271 |
-
# -----------------------------------------------------------------------
|
| 1272 |
-
# 3D Perception Pipeline Routes
|
| 1273 |
-
# -----------------------------------------------------------------------
|
| 1274 |
-
|
| 1275 |
-
class IngestParams(BaseModel):
|
| 1276 |
-
vendors: List[str] = ["alpamayo", "waymo"]
|
| 1277 |
-
sequence_id: str = "seq_001"
|
| 1278 |
-
|
| 1279 |
-
class AutoLabelParams(BaseModel):
|
| 1280 |
-
sequence_id: str = "seq_001"
|
| 1281 |
-
sam_checkpoint: str = "models/sam_vit_b.pth"
|
| 1282 |
-
device: str = "cpu"
|
| 1283 |
-
no_sam: bool = False
|
| 1284 |
-
|
| 1285 |
-
class TrackParams(BaseModel):
|
| 1286 |
-
sequence_id: str = "seq_001"
|
| 1287 |
-
|
| 1288 |
-
class BenchmarkParams(BaseModel):
|
| 1289 |
-
sequence_id: str = "seq_001"
|
| 1290 |
-
|
| 1291 |
-
class GateParams(BaseModel):
|
| 1292 |
-
sequence_id: str = "seq_001"
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
@app.post("/api/dataset/ingest")
|
| 1296 |
-
def ingest_dataset(params: IngestParams):
|
| 1297 |
-
from sensorflow.dataset_fusion_engine import DatasetFusionEngine
|
| 1298 |
-
try:
|
| 1299 |
-
engine = DatasetFusionEngine()
|
| 1300 |
-
sequence = engine.ingest(params.vendors, params.sequence_id)
|
| 1301 |
-
manifest_path = engine.save_manifest(sequence)
|
| 1302 |
-
except Exception as e:
|
| 1303 |
-
raise HTTPException(status_code=500, detail=f"Ingest failed: {str(e)}")
|
| 1304 |
-
|
| 1305 |
-
config = load_config()
|
| 1306 |
-
config.sequence_id = params.sequence_id
|
| 1307 |
-
config.vendors = params.vendors
|
| 1308 |
-
with open(CONFIG_PATH, "w") as f:
|
| 1309 |
-
json.dump(config.model_dump(), f, indent=2)
|
| 1310 |
-
|
| 1311 |
-
return {"status": "ok", "manifest": str(manifest_path), "sequence_id": params.sequence_id}
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
@app.get("/api/dataset/ingest/status")
|
| 1315 |
-
def ingest_status(sequence_id: str = "seq_001"):
|
| 1316 |
-
from sensorflow.dataset_fusion_engine import DatasetFusionEngine
|
| 1317 |
-
engine = DatasetFusionEngine()
|
| 1318 |
-
status = engine.get_status(sequence_id)
|
| 1319 |
-
manifest = Path("runs/pipeline") / sequence_id / "manifest.json"
|
| 1320 |
-
if manifest.exists():
|
| 1321 |
-
with open(manifest) as f:
|
| 1322 |
-
manifest_data = json.load(f)
|
| 1323 |
-
status["manifest_exists"] = True
|
| 1324 |
-
status["frames"] = len(manifest_data.get("frames", []))
|
| 1325 |
-
status["vendor"] = manifest_data.get("vendor", "unknown")
|
| 1326 |
-
return {"status": "ok", "sequence_id": sequence_id, **status}
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
@app.post("/api/perception/auto-label")
|
| 1330 |
-
def auto_label(params: AutoLabelParams):
|
| 1331 |
-
from sensorflow.perception_automator import PerceptionAutomator
|
| 1332 |
-
from sensorflow.schemas.unified_frame import UnifiedSequence
|
| 1333 |
-
|
| 1334 |
-
manifest = Path("runs/pipeline") / params.sequence_id / "manifest.json"
|
| 1335 |
-
if not manifest.exists():
|
| 1336 |
-
raise HTTPException(status_code=400, detail="Manifest not found. Run ingest first.")
|
| 1337 |
-
|
| 1338 |
-
try:
|
| 1339 |
-
sequence = UnifiedSequence.load(manifest)
|
| 1340 |
-
output_dir = manifest.parent / "proposals"
|
| 1341 |
-
automator = PerceptionAutomator(
|
| 1342 |
-
sam_checkpoint=params.sam_checkpoint,
|
| 1343 |
-
device=params.device,
|
| 1344 |
-
use_sam=not params.no_sam,
|
| 1345 |
-
)
|
| 1346 |
-
automator.run_sequence(sequence, output_dir)
|
| 1347 |
-
except Exception as e:
|
| 1348 |
-
raise HTTPException(status_code=500, detail=f"Auto-label failed: {str(e)}")
|
| 1349 |
-
|
| 1350 |
-
proposals_dir = manifest.parent / "proposals"
|
| 1351 |
-
num_frames = len(list(proposals_dir.glob("*.json"))) if proposals_dir.exists() else 0
|
| 1352 |
-
return {"status": "ok", "proposals_dir": str(proposals_dir), "frames_processed": num_frames}
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
@app.post("/api/perception/track")
|
| 1356 |
-
def run_tracking(params: TrackParams):
|
| 1357 |
-
from sensorflow.temporal_tracker import TemporalTracker
|
| 1358 |
-
|
| 1359 |
-
proposals_dir = Path("runs/pipeline") / params.sequence_id / "proposals"
|
| 1360 |
-
if not proposals_dir.exists():
|
| 1361 |
-
raise HTTPException(status_code=400, detail="Proposals not found. Run auto-label first.")
|
| 1362 |
-
|
| 1363 |
-
output = Path("runs/pipeline") / params.sequence_id / "tracks.json"
|
| 1364 |
-
try:
|
| 1365 |
-
tracker = TemporalTracker()
|
| 1366 |
-
proposals = TemporalTracker.load_proposals(proposals_dir)
|
| 1367 |
-
tracks = tracker.run_sequence(proposals, output)
|
| 1368 |
-
except Exception as e:
|
| 1369 |
-
raise HTTPException(status_code=500, detail=f"Tracking failed: {str(e)}")
|
| 1370 |
-
|
| 1371 |
-
return {"status": "ok", "tracks_file": str(output), "num_tracks": len(tracks)}
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
@app.post("/api/benchmark")
|
| 1375 |
-
def run_benchmark(params: BenchmarkParams):
|
| 1376 |
-
from sensorflow.quality_gate import QualityGate
|
| 1377 |
-
from sensorflow.schemas.unified_frame import UnifiedSequence
|
| 1378 |
-
|
| 1379 |
-
manifest = Path("runs/pipeline") / params.sequence_id / "manifest.json"
|
| 1380 |
-
tracks_path = Path("runs/pipeline") / params.sequence_id / "tracks.json"
|
| 1381 |
-
if not manifest.exists():
|
| 1382 |
-
raise HTTPException(status_code=400, detail="Manifest not found.")
|
| 1383 |
-
|
| 1384 |
-
try:
|
| 1385 |
-
sequence = UnifiedSequence.load(manifest)
|
| 1386 |
-
pred_tracks = json.loads(tracks_path.read_text()) if tracks_path.exists() else []
|
| 1387 |
-
gate = QualityGate()
|
| 1388 |
-
results = gate.evaluate(sequence, pred_tracks)
|
| 1389 |
-
gate.save_results(params.sequence_id, results)
|
| 1390 |
-
except Exception as e:
|
| 1391 |
-
raise HTTPException(status_code=500, detail=f"Benchmark failed: {str(e)}")
|
| 1392 |
-
|
| 1393 |
-
return {"status": "ok", "metrics": results["metric_card"]}
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
@app.post("/api/gates/quality")
|
| 1397 |
-
def quality_gate_eval(params: GateParams):
|
| 1398 |
-
from sensorflow.quality_gate import QualityGate
|
| 1399 |
-
from sensorflow.mitl_copilot import MitlCopilot
|
| 1400 |
-
from sensorflow.schemas.unified_frame import UnifiedSequence
|
| 1401 |
-
|
| 1402 |
-
manifest = Path("runs/pipeline") / params.sequence_id / "manifest.json"
|
| 1403 |
-
tracks_path = Path("runs/pipeline") / params.sequence_id / "tracks.json"
|
| 1404 |
-
if not manifest.exists() or not tracks_path.exists():
|
| 1405 |
-
raise HTTPException(status_code=400, detail="Run ingest, auto-label, and track first.")
|
| 1406 |
-
|
| 1407 |
-
try:
|
| 1408 |
-
sequence = UnifiedSequence.load(manifest)
|
| 1409 |
-
with open(tracks_path) as f:
|
| 1410 |
-
pred_tracks = json.load(f)
|
| 1411 |
-
gate = QualityGate()
|
| 1412 |
-
results = gate.evaluate(sequence, pred_tracks)
|
| 1413 |
-
gate.save_results(params.sequence_id, results)
|
| 1414 |
-
if not results["passed"]:
|
| 1415 |
-
copilot = MitlCopilot()
|
| 1416 |
-
copilot.route_edge_cases(params.sequence_id, results["metric_card"], pred_tracks)
|
| 1417 |
-
except Exception as e:
|
| 1418 |
-
raise HTTPException(status_code=500, detail=f"Quality gate failed: {str(e)}")
|
| 1419 |
-
|
| 1420 |
-
report = results["quality_report"]
|
| 1421 |
-
return {"status": "ok", "metric_card": results["metric_card"], **report}
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
@app.post("/api/gates/launch")
|
| 1425 |
-
def launch_gate_eval(params: GateParams):
|
| 1426 |
-
from sensorflow.launch_gate_evaluator import LaunchGateEvaluator
|
| 1427 |
-
config = load_config()
|
| 1428 |
-
evaluator = LaunchGateEvaluator(Path(config.gate_thresholds_path))
|
| 1429 |
-
result = evaluator.evaluate(params.sequence_id)
|
| 1430 |
-
return {"status": "ok", **result}
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
@app.get("/api/pipeline/status")
|
| 1434 |
-
def pipeline_status(sequence_id: str = "seq_001"):
|
| 1435 |
-
state_path = Path("runs/pipeline/state.json")
|
| 1436 |
-
state = {}
|
| 1437 |
-
if state_path.exists():
|
| 1438 |
-
with open(state_path) as f:
|
| 1439 |
-
state = json.load(f)
|
| 1440 |
-
|
| 1441 |
-
seq_state = state.get(sequence_id, {})
|
| 1442 |
-
base = Path("runs/pipeline") / sequence_id
|
| 1443 |
-
return {
|
| 1444 |
-
"status": "ok",
|
| 1445 |
-
"sequence_id": sequence_id,
|
| 1446 |
-
"ingest_complete": (base / "manifest.json").exists(),
|
| 1447 |
-
"perception_complete": (base / "proposals").exists() and any((base / "proposals").glob("*.json")),
|
| 1448 |
-
"tracking_complete": (base / "tracks.json").exists(),
|
| 1449 |
-
"benchmark_complete": (base / "benchmark" / "metric_card.json").exists(),
|
| 1450 |
-
"launch_gate_passed": seq_state.get("launch_gate_passed", False),
|
| 1451 |
-
"state": seq_state,
|
| 1452 |
-
}
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
@app.get("/api/mitl/queue")
|
| 1456 |
-
def get_mitl_queue(sequence_id: str = "seq_001"):
|
| 1457 |
-
from sensorflow.mitl_copilot import MitlCopilot
|
| 1458 |
-
copilot = MitlCopilot()
|
| 1459 |
-
return {"status": "ok", "queue": copilot.get_queue(sequence_id)}
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
# L4 Perception Label Evaluation platform routes (must precede the static mount).
|
| 1463 |
-
from sensorflow.evaluation.api import router as labeleval_router
|
| 1464 |
-
app.include_router(labeleval_router)
|
| 1465 |
-
|
| 1466 |
-
# Aggregate-first mega-scale evaluation layer (metric cube, async runs, query API).
|
| 1467 |
-
from sensorflow.megaeval.api import router as megaeval_router
|
| 1468 |
-
app.include_router(megaeval_router)
|
| 1469 |
-
|
| 1470 |
-
# BEV-Fusion perception engine (camera+LiDAR fusion, masklet tracking, self-eval).
|
| 1471 |
-
from sensorflow.bevfusion.api import router as bevfusion_router
|
| 1472 |
-
app.include_router(bevfusion_router)
|
| 1473 |
-
|
| 1474 |
-
# Mount static folder
|
| 1475 |
-
static_dir = Path(__file__).parent / "static"
|
| 1476 |
-
if static_dir.exists():
|
| 1477 |
-
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
|
| 1478 |
-
|
| 1479 |
-
if __name__ == "__main__":
|
| 1480 |
-
import uvicorn
|
| 1481 |
-
uvicorn.run("app_backend:app", host="127.0.0.1", port=8000, reload=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/autograder.py
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""2D prediction quality autograder."""
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
from collections import Counter
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Dict, List
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def compute_iou(box_a: Dict, box_b: Dict) -> float:
|
| 12 |
-
x1 = max(box_a["x1"], box_b["x1"])
|
| 13 |
-
y1 = max(box_a["y1"], box_b["y1"])
|
| 14 |
-
x2 = min(box_a["x2"], box_b["x2"])
|
| 15 |
-
y2 = min(box_a["y2"], box_b["y2"])
|
| 16 |
-
inter = max(0, x2 - x1) * max(0, y2 - y1)
|
| 17 |
-
area_a = (box_a["x2"] - box_a["x1"]) * (box_a["y2"] - box_a["y1"])
|
| 18 |
-
area_b = (box_b["x2"] - box_b["x1"]) * (box_b["y2"] - box_b["y1"])
|
| 19 |
-
union = area_a + area_b - inter
|
| 20 |
-
return inter / union if union > 0 else 0.0
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def grade_predictions(predictions: List[Dict], conf_threshold: float = 0.5) -> Dict:
|
| 24 |
-
issues = []
|
| 25 |
-
issue_summary = Counter()
|
| 26 |
-
images = set(p["image"] for p in predictions)
|
| 27 |
-
classes = [p["class_name"] for p in predictions]
|
| 28 |
-
|
| 29 |
-
for i, pred in enumerate(predictions):
|
| 30 |
-
if pred["confidence"] < conf_threshold:
|
| 31 |
-
issue_summary["low_confidence"] += 1
|
| 32 |
-
issues.append({
|
| 33 |
-
"severity": "WARNING",
|
| 34 |
-
"type": "low_confidence",
|
| 35 |
-
"image": pred["image"],
|
| 36 |
-
"detection_id": i,
|
| 37 |
-
"confidence": pred["confidence"],
|
| 38 |
-
"threshold": conf_threshold,
|
| 39 |
-
"message": f"Confidence {pred['confidence']:.2f} below threshold",
|
| 40 |
-
})
|
| 41 |
-
|
| 42 |
-
bbox = pred["bbox"]
|
| 43 |
-
area = (bbox["x2"] - bbox["x1"]) * (bbox["y2"] - bbox["y1"])
|
| 44 |
-
if area < 100:
|
| 45 |
-
issue_summary["small_detection"] += 1
|
| 46 |
-
issues.append({
|
| 47 |
-
"severity": "INFO",
|
| 48 |
-
"type": "small_detection",
|
| 49 |
-
"image": pred["image"],
|
| 50 |
-
"detection_id": i,
|
| 51 |
-
"confidence": pred["confidence"],
|
| 52 |
-
"threshold": 100,
|
| 53 |
-
"message": f"Small bounding box area: {area:.0f}px",
|
| 54 |
-
})
|
| 55 |
-
|
| 56 |
-
for i, pred_a in enumerate(predictions):
|
| 57 |
-
for j, pred_b in enumerate(predictions):
|
| 58 |
-
if j <= i or pred_a["image"] != pred_b["image"]:
|
| 59 |
-
continue
|
| 60 |
-
if pred_a["class_name"] != pred_b["class_name"]:
|
| 61 |
-
iou = compute_iou(pred_a["bbox"], pred_b["bbox"])
|
| 62 |
-
if iou > 0.5:
|
| 63 |
-
issue_summary["overlapping_different_class"] += 1
|
| 64 |
-
issues.append({
|
| 65 |
-
"severity": "WARNING",
|
| 66 |
-
"type": "overlapping_different_class",
|
| 67 |
-
"image": pred_a["image"],
|
| 68 |
-
"detection_id": i,
|
| 69 |
-
"confidence": pred_a["confidence"],
|
| 70 |
-
"threshold": 0.5,
|
| 71 |
-
"message": f"Overlap between {pred_a['class_name']} and {pred_b['class_name']}",
|
| 72 |
-
})
|
| 73 |
-
|
| 74 |
-
class_counts = Counter(classes)
|
| 75 |
-
if class_counts:
|
| 76 |
-
dominant = class_counts.most_common(1)[0]
|
| 77 |
-
if dominant[1] / len(classes) > 0.8:
|
| 78 |
-
issue_summary["class_imbalance"] += 1
|
| 79 |
-
|
| 80 |
-
total_issues = sum(issue_summary.values())
|
| 81 |
-
quality_score = max(0, 100 - total_issues * 5)
|
| 82 |
-
|
| 83 |
-
return {
|
| 84 |
-
"total_predictions": len(predictions),
|
| 85 |
-
"total_images": len(images),
|
| 86 |
-
"quality_score": quality_score,
|
| 87 |
-
"issues_by_severity": dict(Counter(i["severity"] for i in issues)),
|
| 88 |
-
"issue_summary": dict(issue_summary),
|
| 89 |
-
"flagged_issues": issues[:50],
|
| 90 |
-
"recommendations": _recommendations(issue_summary),
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def _recommendations(summary: Counter) -> List[str]:
|
| 95 |
-
recs = []
|
| 96 |
-
if summary.get("low_confidence"):
|
| 97 |
-
recs.append("Tune confidence threshold or retrain with more diverse samples.")
|
| 98 |
-
if summary.get("overlapping_different_class"):
|
| 99 |
-
recs.append("Review overlapping class labels; consider NMS tuning.")
|
| 100 |
-
if summary.get("small_detection"):
|
| 101 |
-
recs.append("Verify small bounding boxes are valid annotations.")
|
| 102 |
-
if summary.get("class_imbalance"):
|
| 103 |
-
recs.append("Balance class frequency in training dataset.")
|
| 104 |
-
if not recs:
|
| 105 |
-
recs.append("No significant issues detected.")
|
| 106 |
-
return recs
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def main():
|
| 110 |
-
parser = argparse.ArgumentParser(description="Grade 2D predictions")
|
| 111 |
-
parser.add_argument("--predictions", required=True)
|
| 112 |
-
parser.add_argument("--output", default=None)
|
| 113 |
-
parser.add_argument("--conf", type=float, default=0.5)
|
| 114 |
-
args = parser.parse_args()
|
| 115 |
-
|
| 116 |
-
pred_path = Path(args.predictions)
|
| 117 |
-
with open(pred_path) as f:
|
| 118 |
-
predictions = json.load(f)
|
| 119 |
-
|
| 120 |
-
report = grade_predictions(predictions, args.conf)
|
| 121 |
-
output_path = Path(args.output) if args.output else pred_path.parent / "quality_report.json"
|
| 122 |
-
with open(output_path, "w") as f:
|
| 123 |
-
json.dump(report, f, indent=2)
|
| 124 |
-
print(f"Quality score: {report['quality_score']}/100 -> {output_path}")
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
if __name__ == "__main__":
|
| 128 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/coco8.yaml
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
# Ultralytics YOLO 🚀, AGPL-3.0 license
|
| 2 |
-
# COCO8 dataset by Ultralytics
|
| 3 |
-
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8/
|
| 4 |
-
|
| 5 |
-
# Dataset root directory
|
| 6 |
-
path: ./datasets/coco8
|
| 7 |
-
|
| 8 |
-
# Train and val images
|
| 9 |
-
train: images/train
|
| 10 |
-
val: images/val
|
| 11 |
-
|
| 12 |
-
# Classes
|
| 13 |
-
names:
|
| 14 |
-
0: person
|
| 15 |
-
1: bicycle
|
| 16 |
-
2: car
|
| 17 |
-
3: motorcycle
|
| 18 |
-
4: airplane
|
| 19 |
-
5: bus
|
| 20 |
-
6: train
|
| 21 |
-
7: truck
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/comparative_analytics.py
DELETED
|
@@ -1,209 +0,0 @@
|
|
| 1 |
-
"""Comparative Analytics Dashboard - Core Engine"""
|
| 2 |
-
import pandas as pd
|
| 3 |
-
import numpy as np
|
| 4 |
-
from typing import Dict, List, Optional, Tuple, Any
|
| 5 |
-
from dataclasses import dataclass, field, asdict
|
| 6 |
-
from enum import Enum
|
| 7 |
-
import json
|
| 8 |
-
from datetime import datetime
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class MetricType(Enum):
|
| 12 |
-
"""Metric categories"""
|
| 13 |
-
ACCURACY = "accuracy"
|
| 14 |
-
F1_SCORE = "f1_score"
|
| 15 |
-
AUC = "auc"
|
| 16 |
-
INFERENCE_TIME = "inference_time"
|
| 17 |
-
MEMORY_USAGE = "memory_usage"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@dataclass
|
| 21 |
-
class Configuration:
|
| 22 |
-
"""Configuration parameters"""
|
| 23 |
-
config_id: str
|
| 24 |
-
name: str
|
| 25 |
-
technique: str
|
| 26 |
-
parameters: Dict[str, Any]
|
| 27 |
-
def __hash__(self):
|
| 28 |
-
return hash(self.config_id)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
@dataclass
|
| 32 |
-
class PerformanceMetrics:
|
| 33 |
-
"""Evaluation metrics"""
|
| 34 |
-
accuracy: Optional[float] = None
|
| 35 |
-
precision: Optional[float] = None
|
| 36 |
-
recall: Optional[float] = None
|
| 37 |
-
f1_score: Optional[float] = None
|
| 38 |
-
auc: Optional[float] = None
|
| 39 |
-
inference_time: Optional[float] = None
|
| 40 |
-
memory_usage: Optional[float] = None
|
| 41 |
-
training_time: Optional[float] = None
|
| 42 |
-
throughput: Optional[float] = None
|
| 43 |
-
latency: Optional[float] = None
|
| 44 |
-
|
| 45 |
-
def to_dict(self) -> Dict[str, Optional[float]]:
|
| 46 |
-
return {k: v for k, v in asdict(self).items() if v is not None}
|
| 47 |
-
|
| 48 |
-
def get_metric(self, metric_name: str) -> Optional[float]:
|
| 49 |
-
return getattr(self, metric_name, None)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
@dataclass
|
| 53 |
-
class Perception3DMetrics(PerformanceMetrics):
|
| 54 |
-
"""3D perception and temporal tracking metrics."""
|
| 55 |
-
map_3d: Optional[float] = None
|
| 56 |
-
mar_3d: Optional[float] = None
|
| 57 |
-
mean_iou_3d: Optional[float] = None
|
| 58 |
-
orientation_error_deg: Optional[float] = None
|
| 59 |
-
position_error_m: Optional[float] = None
|
| 60 |
-
id_swap_rate: Optional[float] = None
|
| 61 |
-
track_fragmentation_rate: Optional[float] = None
|
| 62 |
-
process_units: Optional[int] = None
|
| 63 |
-
compute_cycles: Optional[int] = None
|
| 64 |
-
|
| 65 |
-
@classmethod
|
| 66 |
-
def from_metric_card(cls, card: Dict[str, Any]) -> "Perception3DMetrics":
|
| 67 |
-
return cls(
|
| 68 |
-
map_3d=card.get("map_3d"),
|
| 69 |
-
mar_3d=card.get("mar_3d"),
|
| 70 |
-
mean_iou_3d=card.get("mean_iou_3d"),
|
| 71 |
-
orientation_error_deg=card.get("orientation_error_deg"),
|
| 72 |
-
position_error_m=card.get("position_error_m"),
|
| 73 |
-
id_swap_rate=card.get("id_swap_rate"),
|
| 74 |
-
track_fragmentation_rate=card.get("track_fragmentation_rate"),
|
| 75 |
-
process_units=card.get("process_units"),
|
| 76 |
-
compute_cycles=card.get("compute_cycles"),
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
@dataclass
|
| 81 |
-
class ExperimentResult:
|
| 82 |
-
"""Single experiment result"""
|
| 83 |
-
dataset: str
|
| 84 |
-
model: str
|
| 85 |
-
configuration: Configuration
|
| 86 |
-
metrics: PerformanceMetrics
|
| 87 |
-
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
| 88 |
-
notes: str = ""
|
| 89 |
-
|
| 90 |
-
def __hash__(self):
|
| 91 |
-
return hash((self.dataset, self.model, self.configuration.config_id))
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
class ComparativeAnalytics:
|
| 95 |
-
"""Analytics engine for comparing results"""
|
| 96 |
-
|
| 97 |
-
def __init__(self):
|
| 98 |
-
self.results: List[ExperimentResult] = []
|
| 99 |
-
self.datasets: set = set()
|
| 100 |
-
self.models: set = set()
|
| 101 |
-
self.techniques: set = set()
|
| 102 |
-
self.configurations: Dict[str, Configuration] = {}
|
| 103 |
-
|
| 104 |
-
def add_result(self, result: ExperimentResult) -> None:
|
| 105 |
-
self.results.append(result)
|
| 106 |
-
self.datasets.add(result.dataset)
|
| 107 |
-
self.models.add(result.model)
|
| 108 |
-
self.techniques.add(result.configuration.technique)
|
| 109 |
-
self.configurations[result.configuration.config_id] = result.configuration
|
| 110 |
-
|
| 111 |
-
def add_results_batch(self, results: List[ExperimentResult]) -> None:
|
| 112 |
-
for result in results:
|
| 113 |
-
self.add_result(result)
|
| 114 |
-
|
| 115 |
-
def filter_results(self, datasets: Optional[List[str]] = None, models: Optional[List[str]] = None,
|
| 116 |
-
techniques: Optional[List[str]] = None, configurations: Optional[List[str]] = None) -> List[ExperimentResult]:
|
| 117 |
-
filtered = self.results
|
| 118 |
-
if datasets:
|
| 119 |
-
filtered = [r for r in filtered if r.dataset in datasets]
|
| 120 |
-
if models:
|
| 121 |
-
filtered = [r for r in filtered if r.model in models]
|
| 122 |
-
if techniques:
|
| 123 |
-
filtered = [r for r in filtered if r.configuration.technique in techniques]
|
| 124 |
-
if configurations:
|
| 125 |
-
filtered = [r for r in filtered if r.configuration.config_id in configurations]
|
| 126 |
-
return filtered
|
| 127 |
-
|
| 128 |
-
def to_dataframe(self, datasets: Optional[List[str]] = None, models: Optional[List[str]] = None,
|
| 129 |
-
techniques: Optional[List[str]] = None, configurations: Optional[List[str]] = None) -> pd.DataFrame:
|
| 130 |
-
filtered = self.filter_results(datasets, models, techniques, configurations)
|
| 131 |
-
rows = []
|
| 132 |
-
for result in filtered:
|
| 133 |
-
row = {
|
| 134 |
-
'dataset': result.dataset,
|
| 135 |
-
'model': result.model,
|
| 136 |
-
'technique': result.configuration.technique,
|
| 137 |
-
'configuration': result.configuration.name,
|
| 138 |
-
'config_id': result.configuration.config_id,
|
| 139 |
-
'timestamp': result.timestamp,
|
| 140 |
-
}
|
| 141 |
-
row.update(result.metrics.to_dict())
|
| 142 |
-
rows.append(row)
|
| 143 |
-
return pd.DataFrame(rows)
|
| 144 |
-
|
| 145 |
-
def get_best_configurations(self, metric: str = 'f1_score', dataset: Optional[str] = None,
|
| 146 |
-
model: Optional[str] = None, top_n: int = 5) -> pd.DataFrame:
|
| 147 |
-
filters = {'datasets': [dataset] if dataset else None, 'models': [model] if model else None}
|
| 148 |
-
df = self.to_dataframe(**filters)
|
| 149 |
-
if df.empty or metric not in df.columns:
|
| 150 |
-
return pd.DataFrame()
|
| 151 |
-
df = df.dropna(subset=[metric])
|
| 152 |
-
df = df.sort_values(metric, ascending=False).head(top_n)
|
| 153 |
-
return df
|
| 154 |
-
|
| 155 |
-
def compare_techniques(self, metric: str = 'f1_score', dataset: Optional[str] = None,
|
| 156 |
-
model: Optional[str] = None) -> pd.DataFrame:
|
| 157 |
-
filters = {'datasets': [dataset] if dataset else None, 'models': [model] if model else None}
|
| 158 |
-
df = self.to_dataframe(**filters)
|
| 159 |
-
if df.empty or metric not in df.columns:
|
| 160 |
-
return pd.DataFrame()
|
| 161 |
-
comparison = df.groupby('technique')[metric].agg([('mean', 'mean'), ('std', 'std'),
|
| 162 |
-
('min', 'min'), ('max', 'max'), ('count', 'count')]).round(4)
|
| 163 |
-
return comparison.sort_values('mean', ascending=False)
|
| 164 |
-
|
| 165 |
-
def get_summary_statistics(self, metric: str = 'f1_score', groupby: str = 'technique') -> Dict:
|
| 166 |
-
df = self.to_dataframe()
|
| 167 |
-
if df.empty or metric not in df.columns:
|
| 168 |
-
return {}
|
| 169 |
-
stats = {}
|
| 170 |
-
for group_name, group_df in df.groupby(groupby):
|
| 171 |
-
group_data = group_df[metric].dropna()
|
| 172 |
-
stats[str(group_name)] = {
|
| 173 |
-
'mean': float(group_data.mean()),
|
| 174 |
-
'std': float(group_data.std()),
|
| 175 |
-
'min': float(group_data.min()),
|
| 176 |
-
'max': float(group_data.max()),
|
| 177 |
-
'median': float(group_data.median()),
|
| 178 |
-
'count': int(len(group_data))
|
| 179 |
-
}
|
| 180 |
-
return stats
|
| 181 |
-
|
| 182 |
-
def get_dataset_model_matrix(self, metric: str = 'f1_score') -> pd.DataFrame:
|
| 183 |
-
df = self.to_dataframe()
|
| 184 |
-
if df.empty or metric not in df.columns:
|
| 185 |
-
return pd.DataFrame()
|
| 186 |
-
matrix = df.groupby(['dataset', 'model'])[metric].mean().unstack(fill_value=None)
|
| 187 |
-
return matrix
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
class DashboardBuilder:
|
| 191 |
-
"""Build dashboard components"""
|
| 192 |
-
def __init__(self, analytics: ComparativeAnalytics):
|
| 193 |
-
self.analytics = analytics
|
| 194 |
-
|
| 195 |
-
def get_summary_report(self) -> Dict[str, Any]:
|
| 196 |
-
df = self.analytics.to_dataframe()
|
| 197 |
-
if df.empty:
|
| 198 |
-
return {}
|
| 199 |
-
return {
|
| 200 |
-
'overview': {
|
| 201 |
-
'total_experiments': len(self.analytics.results),
|
| 202 |
-
'num_datasets': len(self.analytics.datasets),
|
| 203 |
-
'num_models': len(self.analytics.models),
|
| 204 |
-
'num_techniques': len(self.analytics.techniques),
|
| 205 |
-
},
|
| 206 |
-
'datasets': sorted(list(self.analytics.datasets)),
|
| 207 |
-
'models': sorted(list(self.analytics.models)),
|
| 208 |
-
'techniques': sorted(list(self.analytics.techniques)),
|
| 209 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/dashboard_viz.py
DELETED
|
@@ -1,468 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Dashboard Visualizations - Complete Implementation
|
| 3 |
-
Bar charts, heatmaps, scatter plots, and Pareto frontier
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import numpy as np
|
| 8 |
-
from typing import Dict, List, Optional, Tuple, Any
|
| 9 |
-
from comparative_analytics import ComparativeAnalytics
|
| 10 |
-
|
| 11 |
-
try:
|
| 12 |
-
import plotly.graph_objects as go
|
| 13 |
-
import plotly.express as px
|
| 14 |
-
from plotly.subplots import make_subplots
|
| 15 |
-
PLOTLY_AVAILABLE = True
|
| 16 |
-
except ImportError:
|
| 17 |
-
PLOTLY_AVAILABLE = False
|
| 18 |
-
print("Warning: plotly not installed. Install with: pip install plotly")
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class DashboardVisualizations:
|
| 22 |
-
"""Generate interactive visualizations for comparative analysis"""
|
| 23 |
-
|
| 24 |
-
def __init__(self, analytics: ComparativeAnalytics):
|
| 25 |
-
self.analytics = analytics
|
| 26 |
-
if not PLOTLY_AVAILABLE:
|
| 27 |
-
raise ImportError("plotly required for visualizations. Install with: pip install plotly")
|
| 28 |
-
|
| 29 |
-
def bar_chart_by_technique(self, metric: str = 'f1_score', dataset: Optional[str] = None,
|
| 30 |
-
model: Optional[str] = None) -> go.Figure:
|
| 31 |
-
"""Bar chart comparing metric values by technique"""
|
| 32 |
-
df = self.analytics.to_dataframe(
|
| 33 |
-
datasets=[dataset] if dataset else None,
|
| 34 |
-
models=[model] if model else None
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
if df.empty or metric not in df.columns:
|
| 38 |
-
return go.Figure().add_annotation(text="No data available")
|
| 39 |
-
|
| 40 |
-
# Group by technique and calculate statistics
|
| 41 |
-
tech_stats = df.groupby('technique')[metric].agg(['mean', 'std']).reset_index()
|
| 42 |
-
tech_stats = tech_stats.sort_values('mean', ascending=False)
|
| 43 |
-
|
| 44 |
-
fig = go.Figure()
|
| 45 |
-
|
| 46 |
-
fig.add_trace(go.Bar(
|
| 47 |
-
x=tech_stats['technique'],
|
| 48 |
-
y=tech_stats['mean'],
|
| 49 |
-
error_y=dict(type='data', array=tech_stats['std'], visible=True),
|
| 50 |
-
name='Mean ± Std',
|
| 51 |
-
marker=dict(
|
| 52 |
-
color=tech_stats['mean'],
|
| 53 |
-
colorscale='Viridis',
|
| 54 |
-
showscale=True,
|
| 55 |
-
colorbar=dict(title=metric)
|
| 56 |
-
),
|
| 57 |
-
text=tech_stats['mean'].round(4),
|
| 58 |
-
textposition='auto',
|
| 59 |
-
))
|
| 60 |
-
|
| 61 |
-
title = f"{metric.replace('_', ' ').title()} by Technique"
|
| 62 |
-
if dataset:
|
| 63 |
-
title += f" ({dataset})"
|
| 64 |
-
if model:
|
| 65 |
-
title += f" ({model})"
|
| 66 |
-
|
| 67 |
-
fig.update_layout(
|
| 68 |
-
title=title,
|
| 69 |
-
xaxis_title='Technique',
|
| 70 |
-
yaxis_title=metric.replace('_', ' ').title(),
|
| 71 |
-
hovermode='x unified',
|
| 72 |
-
height=500,
|
| 73 |
-
showlegend=True
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
return fig
|
| 77 |
-
|
| 78 |
-
def bar_chart_by_configuration(self, metric: str = 'f1_score', technique: Optional[str] = None,
|
| 79 |
-
top_n: int = 10) -> go.Figure:
|
| 80 |
-
"""Bar chart showing top N configurations"""
|
| 81 |
-
df = self.analytics.to_dataframe(
|
| 82 |
-
techniques=[technique] if technique else None
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
if df.empty or metric not in df.columns:
|
| 86 |
-
return go.Figure().add_annotation(text="No data available")
|
| 87 |
-
|
| 88 |
-
# Get top configurations
|
| 89 |
-
top = df.nlargest(top_n, metric)[['configuration', 'technique', metric]]
|
| 90 |
-
|
| 91 |
-
fig = go.Figure()
|
| 92 |
-
|
| 93 |
-
fig.add_trace(go.Bar(
|
| 94 |
-
y=top['configuration'],
|
| 95 |
-
x=top[metric],
|
| 96 |
-
orientation='h',
|
| 97 |
-
marker=dict(
|
| 98 |
-
color=top[metric],
|
| 99 |
-
colorscale='Viridis',
|
| 100 |
-
showscale=True,
|
| 101 |
-
colorbar=dict(title=metric)
|
| 102 |
-
),
|
| 103 |
-
text=top[metric].round(4),
|
| 104 |
-
textposition='auto',
|
| 105 |
-
hovertemplate='<b>%{y}</b><br>' + f'{metric}: %{{x:.4f}}<extra></extra>'
|
| 106 |
-
))
|
| 107 |
-
|
| 108 |
-
title = f"Top {top_n} Configurations by {metric.replace('_', ' ').title()}"
|
| 109 |
-
if technique:
|
| 110 |
-
title += f" ({technique})"
|
| 111 |
-
|
| 112 |
-
fig.update_layout(
|
| 113 |
-
title=title,
|
| 114 |
-
xaxis_title=metric.replace('_', ' ').title(),
|
| 115 |
-
yaxis_title='Configuration',
|
| 116 |
-
height=400 + top_n * 20,
|
| 117 |
-
showlegend=False
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
return fig
|
| 121 |
-
|
| 122 |
-
def heatmap_dataset_technique(self, metric: str = 'f1_score') -> go.Figure:
|
| 123 |
-
"""Heatmap: Dataset × Technique"""
|
| 124 |
-
df = self.analytics.to_dataframe()
|
| 125 |
-
|
| 126 |
-
if df.empty or metric not in df.columns:
|
| 127 |
-
return go.Figure().add_annotation(text="No data available")
|
| 128 |
-
|
| 129 |
-
# Create pivot table
|
| 130 |
-
pivot = df.pivot_table(
|
| 131 |
-
values=metric,
|
| 132 |
-
index='dataset',
|
| 133 |
-
columns='technique',
|
| 134 |
-
aggfunc='mean'
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
fig = go.Figure(data=go.Heatmap(
|
| 138 |
-
z=pivot.values,
|
| 139 |
-
x=pivot.columns,
|
| 140 |
-
y=pivot.index,
|
| 141 |
-
colorscale='RdYlGn',
|
| 142 |
-
text=np.round(pivot.values, 4),
|
| 143 |
-
texttemplate='%{text:.4f}',
|
| 144 |
-
textfont={"size": 10},
|
| 145 |
-
hovertemplate='Dataset: %{y}<br>Technique: %{x}<br>' + f'{metric}: %{{z:.4f}}<extra></extra>',
|
| 146 |
-
colorbar=dict(title=metric)
|
| 147 |
-
))
|
| 148 |
-
|
| 149 |
-
fig.update_layout(
|
| 150 |
-
title=f"{metric.replace('_', ' ').title()} Heatmap: Dataset × Technique",
|
| 151 |
-
xaxis_title='Technique',
|
| 152 |
-
yaxis_title='Dataset',
|
| 153 |
-
height=400,
|
| 154 |
-
width=700
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
return fig
|
| 158 |
-
|
| 159 |
-
def heatmap_dataset_model(self, metric: str = 'f1_score') -> go.Figure:
|
| 160 |
-
"""Heatmap: Dataset × Model"""
|
| 161 |
-
df = self.analytics.to_dataframe()
|
| 162 |
-
|
| 163 |
-
if df.empty or metric not in df.columns:
|
| 164 |
-
return go.Figure().add_annotation(text="No data available")
|
| 165 |
-
|
| 166 |
-
# Create pivot table
|
| 167 |
-
pivot = df.pivot_table(
|
| 168 |
-
values=metric,
|
| 169 |
-
index='dataset',
|
| 170 |
-
columns='model',
|
| 171 |
-
aggfunc='mean'
|
| 172 |
-
)
|
| 173 |
-
|
| 174 |
-
fig = go.Figure(data=go.Heatmap(
|
| 175 |
-
z=pivot.values,
|
| 176 |
-
x=pivot.columns,
|
| 177 |
-
y=pivot.index,
|
| 178 |
-
colorscale='RdYlGn',
|
| 179 |
-
text=np.round(pivot.values, 4),
|
| 180 |
-
texttemplate='%{text:.4f}',
|
| 181 |
-
textfont={"size": 10},
|
| 182 |
-
hovertemplate='Dataset: %{y}<br>Model: %{x}<br>' + f'{metric}: %{{z:.4f}}<extra></extra>',
|
| 183 |
-
colorbar=dict(title=metric)
|
| 184 |
-
))
|
| 185 |
-
|
| 186 |
-
fig.update_layout(
|
| 187 |
-
title=f"{metric.replace('_', ' ').title()} Heatmap: Dataset × Model",
|
| 188 |
-
xaxis_title='Model',
|
| 189 |
-
yaxis_title='Dataset',
|
| 190 |
-
height=400,
|
| 191 |
-
width=700
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
return fig
|
| 195 |
-
|
| 196 |
-
def scatter_plot_trade_off(self, metric_x: str = 'f1_score', metric_y: str = 'inference_time',
|
| 197 |
-
color_by: str = 'technique') -> go.Figure:
|
| 198 |
-
"""Scatter plot for two-metric trade-off"""
|
| 199 |
-
df = self.analytics.to_dataframe()
|
| 200 |
-
|
| 201 |
-
if df.empty or metric_x not in df.columns or metric_y not in df.columns:
|
| 202 |
-
return go.Figure().add_annotation(text="No data available")
|
| 203 |
-
|
| 204 |
-
df = df.dropna(subset=[metric_x, metric_y])
|
| 205 |
-
|
| 206 |
-
fig = px.scatter(
|
| 207 |
-
df,
|
| 208 |
-
x=metric_x,
|
| 209 |
-
y=metric_y,
|
| 210 |
-
color=color_by,
|
| 211 |
-
hover_data=['dataset', 'model', 'configuration'],
|
| 212 |
-
title=f"Trade-off: {metric_x.replace('_', ' ').title()} vs {metric_y.replace('_', ' ').title()}",
|
| 213 |
-
labels={
|
| 214 |
-
metric_x: metric_x.replace('_', ' ').title(),
|
| 215 |
-
metric_y: metric_y.replace('_', ' ').title()
|
| 216 |
-
}
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
fig.update_layout(
|
| 220 |
-
height=500,
|
| 221 |
-
hovermode='closest'
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
return fig
|
| 225 |
-
|
| 226 |
-
def line_chart_trends(self, metric: str = 'f1_score', technique: Optional[str] = None,
|
| 227 |
-
group_by: str = 'configuration') -> go.Figure:
|
| 228 |
-
"""Line chart showing performance trends"""
|
| 229 |
-
df = self.analytics.to_dataframe(
|
| 230 |
-
techniques=[technique] if technique else None
|
| 231 |
-
)
|
| 232 |
-
|
| 233 |
-
if df.empty or metric not in df.columns:
|
| 234 |
-
return go.Figure().add_annotation(text="No data available")
|
| 235 |
-
|
| 236 |
-
df = df.sort_values('timestamp')
|
| 237 |
-
|
| 238 |
-
fig = go.Figure()
|
| 239 |
-
|
| 240 |
-
# Line for each configuration
|
| 241 |
-
for config in df[group_by].unique():
|
| 242 |
-
config_df = df[df[group_by] == config]
|
| 243 |
-
fig.add_trace(go.Scatter(
|
| 244 |
-
x=config_df.index,
|
| 245 |
-
y=config_df[metric],
|
| 246 |
-
mode='lines+markers',
|
| 247 |
-
name=config,
|
| 248 |
-
hovertemplate='<b>%{fullData.name}</b><br>' +
|
| 249 |
-
f'{metric}: %{{y:.4f}}<br>' +
|
| 250 |
-
'Index: %{x}<extra></extra>'
|
| 251 |
-
))
|
| 252 |
-
|
| 253 |
-
title = f"{metric.replace('_', ' ').title()} Trends"
|
| 254 |
-
if technique:
|
| 255 |
-
title += f" ({technique})"
|
| 256 |
-
|
| 257 |
-
fig.update_layout(
|
| 258 |
-
title=title,
|
| 259 |
-
xaxis_title='Experiment Index',
|
| 260 |
-
yaxis_title=metric.replace('_', ' ').title(),
|
| 261 |
-
hovermode='x unified',
|
| 262 |
-
height=500
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
return fig
|
| 266 |
-
|
| 267 |
-
def pareto_frontier_plot(self, metric1: str = 'f1_score', metric2: str = 'inference_time') -> go.Figure:
|
| 268 |
-
"""Plot showing Pareto frontier (optimal trade-offs)"""
|
| 269 |
-
df = self.analytics.to_dataframe()
|
| 270 |
-
|
| 271 |
-
if df.empty or metric1 not in df.columns or metric2 not in df.columns:
|
| 272 |
-
return go.Figure().add_annotation(text="No data available")
|
| 273 |
-
|
| 274 |
-
df = df.dropna(subset=[metric1, metric2])
|
| 275 |
-
|
| 276 |
-
# Calculate Pareto frontier
|
| 277 |
-
pareto_mask = np.ones(len(df), dtype=bool)
|
| 278 |
-
for i in range(len(df)):
|
| 279 |
-
for j in range(len(df)):
|
| 280 |
-
if i != j:
|
| 281 |
-
# Check if point j dominates point i
|
| 282 |
-
if df.iloc[j][metric1] >= df.iloc[i][metric1] and \
|
| 283 |
-
df.iloc[j][metric2] <= df.iloc[i][metric2] and \
|
| 284 |
-
(df.iloc[j][metric1] > df.iloc[i][metric1] or df.iloc[j][metric2] < df.iloc[i][metric2]):
|
| 285 |
-
pareto_mask[i] = False
|
| 286 |
-
break
|
| 287 |
-
|
| 288 |
-
pareto_df = df[pareto_mask]
|
| 289 |
-
|
| 290 |
-
fig = go.Figure()
|
| 291 |
-
|
| 292 |
-
# All points
|
| 293 |
-
fig.add_trace(go.Scatter(
|
| 294 |
-
x=df[metric2],
|
| 295 |
-
y=df[metric1],
|
| 296 |
-
mode='markers',
|
| 297 |
-
name='All Results',
|
| 298 |
-
marker=dict(size=8, color='lightblue', opacity=0.6),
|
| 299 |
-
hovertemplate=f'{metric1}: %{{y:.4f}}<br>{metric2}: %{{x:.4f}}<extra></extra>'
|
| 300 |
-
))
|
| 301 |
-
|
| 302 |
-
# Pareto frontier
|
| 303 |
-
if not pareto_df.empty:
|
| 304 |
-
pareto_sorted = pareto_df.sort_values(metric2)
|
| 305 |
-
fig.add_trace(go.Scatter(
|
| 306 |
-
x=pareto_sorted[metric2],
|
| 307 |
-
y=pareto_sorted[metric1],
|
| 308 |
-
mode='lines+markers',
|
| 309 |
-
name='Pareto Frontier',
|
| 310 |
-
marker=dict(size=12, color='red'),
|
| 311 |
-
line=dict(color='red', width=2),
|
| 312 |
-
hovertemplate=f'{metric1}: %{{y:.4f}}<br>{metric2}: %{{x:.4f}}<extra></extra>'
|
| 313 |
-
))
|
| 314 |
-
|
| 315 |
-
fig.update_layout(
|
| 316 |
-
title=f"Pareto Frontier: {metric1.replace('_', ' ').title()} vs {metric2.replace('_', ' ').title()}",
|
| 317 |
-
xaxis_title=metric2.replace('_', ' ').title(),
|
| 318 |
-
yaxis_title=metric1.replace('_', ' ').title(),
|
| 319 |
-
hovermode='closest',
|
| 320 |
-
height=500
|
| 321 |
-
)
|
| 322 |
-
|
| 323 |
-
return fig
|
| 324 |
-
|
| 325 |
-
def multi_metric_subplots(self, metrics: List[str] = ['f1_score', 'accuracy', 'inference_time']) -> go.Figure:
|
| 326 |
-
"""Subplots comparing techniques across multiple metrics"""
|
| 327 |
-
df = self.analytics.to_dataframe()
|
| 328 |
-
|
| 329 |
-
if df.empty:
|
| 330 |
-
return go.Figure().add_annotation(text="No data available")
|
| 331 |
-
|
| 332 |
-
available_metrics = [m for m in metrics if m in df.columns]
|
| 333 |
-
|
| 334 |
-
if not available_metrics:
|
| 335 |
-
return go.Figure().add_annotation(text="No metrics available")
|
| 336 |
-
|
| 337 |
-
fig = make_subplots(
|
| 338 |
-
rows=1,
|
| 339 |
-
cols=len(available_metrics),
|
| 340 |
-
subplot_titles=[m.replace('_', ' ').title() for m in available_metrics],
|
| 341 |
-
specs=[[{"type": "bar"}] * len(available_metrics)]
|
| 342 |
-
)
|
| 343 |
-
|
| 344 |
-
colors = ['rgb(31, 119, 180)', 'rgb(255, 127, 14)', 'rgb(44, 160, 44)']
|
| 345 |
-
|
| 346 |
-
for col_idx, metric in enumerate(available_metrics, 1):
|
| 347 |
-
df_clean = df.dropna(subset=[metric])
|
| 348 |
-
stats = df_clean.groupby('technique')[metric].mean().sort_values(ascending=False)
|
| 349 |
-
|
| 350 |
-
fig.add_trace(
|
| 351 |
-
go.Bar(
|
| 352 |
-
x=stats.index,
|
| 353 |
-
y=stats.values,
|
| 354 |
-
name=metric,
|
| 355 |
-
marker=dict(color=colors[col_idx % len(colors)]),
|
| 356 |
-
text=stats.round(4),
|
| 357 |
-
textposition='auto',
|
| 358 |
-
),
|
| 359 |
-
row=1,
|
| 360 |
-
col=col_idx
|
| 361 |
-
)
|
| 362 |
-
|
| 363 |
-
fig.update_xaxes(title_text='Technique', row=1, col=col_idx)
|
| 364 |
-
fig.update_yaxes(title_text=metric.replace('_', ' ').title(), row=1, col=col_idx)
|
| 365 |
-
|
| 366 |
-
fig.update_layout(
|
| 367 |
-
title='Technique Comparison Across Multiple Metrics',
|
| 368 |
-
height=500,
|
| 369 |
-
showlegend=False
|
| 370 |
-
)
|
| 371 |
-
|
| 372 |
-
return fig
|
| 373 |
-
|
| 374 |
-
def export_all_charts_html(self, filepath: str, metrics: List[str] = ['f1_score', 'accuracy']) -> str:
|
| 375 |
-
"""Export all visualizations to a single HTML file"""
|
| 376 |
-
html_parts = [
|
| 377 |
-
"""
|
| 378 |
-
<html>
|
| 379 |
-
<head>
|
| 380 |
-
<title>Comparative Analytics Dashboard</title>
|
| 381 |
-
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 382 |
-
<style>
|
| 383 |
-
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
| 384 |
-
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
|
| 385 |
-
h1 { color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
|
| 386 |
-
.section { margin: 40px 0; }
|
| 387 |
-
h2 { color: #666; margin-top: 30px; }
|
| 388 |
-
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
| 389 |
-
.chart-item { border: 1px solid #ddd; padding: 10px; border-radius: 4px; }
|
| 390 |
-
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
| 391 |
-
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
| 392 |
-
th { background-color: #4CAF50; color: white; }
|
| 393 |
-
tr:nth-child(even) { background-color: #f2f2f2; }
|
| 394 |
-
</style>
|
| 395 |
-
</head>
|
| 396 |
-
<body>
|
| 397 |
-
<div class="container">
|
| 398 |
-
<h1>📊 Comparative Analytics Dashboard</h1>
|
| 399 |
-
"""
|
| 400 |
-
]
|
| 401 |
-
|
| 402 |
-
# Overview
|
| 403 |
-
summary = self.analytics.to_dataframe()
|
| 404 |
-
if not summary.empty:
|
| 405 |
-
html_parts.append(f"""
|
| 406 |
-
<div class="section">
|
| 407 |
-
<h2>Overview</h2>
|
| 408 |
-
<p><strong>Total Experiments:</strong> {len(self.analytics.results)}</p>
|
| 409 |
-
<p><strong>Datasets:</strong> {', '.join(sorted(self.analytics.datasets))}</p>
|
| 410 |
-
<p><strong>Models:</strong> {', '.join(sorted(self.analytics.models))}</p>
|
| 411 |
-
<p><strong>Techniques:</strong> {', '.join(sorted(self.analytics.techniques))}</p>
|
| 412 |
-
</div>
|
| 413 |
-
""")
|
| 414 |
-
|
| 415 |
-
# Visualizations
|
| 416 |
-
html_parts.append('<div class="section"><h2>Visualizations</h2></div>')
|
| 417 |
-
|
| 418 |
-
# Bar charts by technique
|
| 419 |
-
for metric in metrics:
|
| 420 |
-
fig = self.bar_chart_by_technique(metric=metric)
|
| 421 |
-
html_parts.append(f'<div class="section"><h3>{metric.replace("_", " ").title()} by Technique</h3>')
|
| 422 |
-
html_parts.append(fig.to_html(include_plotlyjs=False, div_id=f"chart_{metric}_tech"))
|
| 423 |
-
html_parts.append('</div>')
|
| 424 |
-
|
| 425 |
-
# Heatmaps
|
| 426 |
-
fig_hm_dt = self.heatmap_dataset_technique()
|
| 427 |
-
html_parts.append('<div class="section"><h3>Dataset × Technique Heatmap (F1-Score)</h3>')
|
| 428 |
-
html_parts.append(fig_hm_dt.to_html(include_plotlyjs=False, div_id="heatmap_dt"))
|
| 429 |
-
html_parts.append('</div>')
|
| 430 |
-
|
| 431 |
-
fig_hm_dm = self.heatmap_dataset_model()
|
| 432 |
-
html_parts.append('<div class="section"><h3>Dataset × Model Heatmap (F1-Score)</h3>')
|
| 433 |
-
html_parts.append(fig_hm_dm.to_html(include_plotlyjs=False, div_id="heatmap_dm"))
|
| 434 |
-
html_parts.append('</div>')
|
| 435 |
-
|
| 436 |
-
# Scatter plot
|
| 437 |
-
fig_scatter = self.scatter_plot_trade_off()
|
| 438 |
-
html_parts.append('<div class="section"><h3>Trade-off Analysis (F1-Score vs Inference Time)</h3>')
|
| 439 |
-
html_parts.append(fig_scatter.to_html(include_plotlyjs=False, div_id="scatter"))
|
| 440 |
-
html_parts.append('</div>')
|
| 441 |
-
|
| 442 |
-
# Pareto frontier
|
| 443 |
-
fig_pareto = self.pareto_frontier_plot()
|
| 444 |
-
html_parts.append('<div class="section"><h3>Pareto Frontier</h3>')
|
| 445 |
-
html_parts.append(fig_pareto.to_html(include_plotlyjs=False, div_id="pareto"))
|
| 446 |
-
html_parts.append('</div>')
|
| 447 |
-
|
| 448 |
-
# Multi-metric subplots
|
| 449 |
-
fig_multi = self.multi_metric_subplots(metrics=metrics + ['inference_time'])
|
| 450 |
-
html_parts.append('<div class="section"><h2>Multi-Metric Comparison</h2>')
|
| 451 |
-
html_parts.append(fig_multi.to_html(include_plotlyjs=False, div_id="subplots"))
|
| 452 |
-
html_parts.append('</div>')
|
| 453 |
-
|
| 454 |
-
# Summary statistics table
|
| 455 |
-
stats = self.analytics.compare_techniques()
|
| 456 |
-
if not stats.empty:
|
| 457 |
-
html_parts.append('<div class="section"><h2>Summary Statistics</h2>')
|
| 458 |
-
html_parts.append(stats.to_html())
|
| 459 |
-
html_parts.append('</div>')
|
| 460 |
-
|
| 461 |
-
html_parts.append('</div></body></html>')
|
| 462 |
-
|
| 463 |
-
html_content = '\n'.join(html_parts)
|
| 464 |
-
|
| 465 |
-
with open(filepath, 'w') as f:
|
| 466 |
-
f.write(html_content)
|
| 467 |
-
|
| 468 |
-
return filepath
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/data/test.jpg
DELETED
|
Binary file (50.4 kB)
|
|
|
hf-space/deploy/CHECKLIST.md
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
# Pre-Deployment & Post-Deployment Checklist
|
| 2 |
-
|
| 3 |
-
## Pre-Deployment (Local Machine)
|
| 4 |
-
|
| 5 |
-
- [x] Cleanup complete:
|
| 6 |
-
- [x] Removed test files (test_*.py)
|
| 7 |
-
- [x] Removed examples (example_*.py)
|
| 8 |
-
- [x] Removed virtual environments (.venv/)
|
| 9 |
-
- [x] Removed large model files (*.pt) - will download on DGX
|
| 10 |
-
- [x] Removed redundant analysis scripts
|
| 11 |
-
- [x] Removed documentation clutter
|
| 12 |
-
|
| 13 |
-
- [x] Files prepared:
|
| 14 |
-
- [x] .gitignore created (proper exclusions)
|
| 15 |
-
- [x] requirements-prod.txt created
|
| 16 |
-
- [x] LICENSE file present
|
| 17 |
-
- [x] Deployment scripts created
|
| 18 |
-
|
| 19 |
-
- [x] Git status:
|
| 20 |
-
- [x] All changes staged
|
| 21 |
-
- [x] Ready to commit
|
| 22 |
-
|
| 23 |
-
## Deployment Steps
|
| 24 |
-
|
| 25 |
-
### Step 1: Transfer Code to DGX
|
| 26 |
-
```bash
|
| 27 |
-
rsync -avz --exclude='.venv' --exclude='node_modules' \
|
| 28 |
-
/path/to/DrivingRepo/ \
|
| 29 |
-
<dgx-host>:/tmp/sensorflow-deploy/
|
| 30 |
-
```
|
| 31 |
-
|
| 32 |
-
### Step 2: Execute Deployment on DGX
|
| 33 |
-
```bash
|
| 34 |
-
ssh <dgx-host>
|
| 35 |
-
cd /tmp/sensorflow-deploy
|
| 36 |
-
sudo bash deploy/setup-dgx.sh /tmp/sensorflow-deploy
|
| 37 |
-
```
|
| 38 |
-
|
| 39 |
-
### Step 3: DNS Configuration (GoDaddy)
|
| 40 |
-
- [ ] Add A record: sensorflow.wael.bot → [DGX IP]
|
| 41 |
-
- [ ] Set TTL to 3600
|
| 42 |
-
- [ ] Wait for DNS propagation (5-10 minutes)
|
| 43 |
-
|
| 44 |
-
## Post-Deployment (Verify on DGX)
|
| 45 |
-
|
| 46 |
-
### Services
|
| 47 |
-
- [ ] Check sensorflow-studio service: `sudo systemctl status sensorflow-studio`
|
| 48 |
-
- [ ] Check accident-analysis service: `sudo systemctl status accident-analysis`
|
| 49 |
-
- [ ] Check nginx: `sudo systemctl status nginx`
|
| 50 |
-
|
| 51 |
-
### API Health Checks
|
| 52 |
-
```bash
|
| 53 |
-
# From DGX or via Tailscale
|
| 54 |
-
curl http://sensorflow.wael.bot/
|
| 55 |
-
curl http://sensorflow.wael.bot/api/studio/docs
|
| 56 |
-
curl http://sensorflow.wael.bot/api/analysis/docs
|
| 57 |
-
curl http://sensorflow.wael.bot/health/studio
|
| 58 |
-
curl http://sensorflow.wael.bot/health/analysis
|
| 59 |
-
```
|
| 60 |
-
|
| 61 |
-
### Logs Verification
|
| 62 |
-
```bash
|
| 63 |
-
sudo journalctl -u sensorflow-studio -n 20
|
| 64 |
-
sudo journalctl -u accident-analysis -n 20
|
| 65 |
-
sudo tail -20 /var/log/nginx/access.log
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
### Data & Models
|
| 69 |
-
- [ ] YOLOv8 models downloaded: `ls -lh /opt/sensorflow/studio/yolov8*.pt`
|
| 70 |
-
- [ ] React frontend built: `ls -la /opt/sensorflow/studio/dist/`
|
| 71 |
-
- [ ] Data directory present: `ls /opt/sensorflow/studio/data/`
|
| 72 |
-
|
| 73 |
-
### Performance
|
| 74 |
-
```bash
|
| 75 |
-
# Check resource usage
|
| 76 |
-
nvidia-smi # GPU load
|
| 77 |
-
ps aux | grep sensorflow
|
| 78 |
-
top -u sensorflow
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
## Maintenance & Monitoring
|
| 82 |
-
|
| 83 |
-
### Daily
|
| 84 |
-
- [ ] Monitor error logs: `sudo journalctl -u sensorflow-studio --since="1 hour ago"`
|
| 85 |
-
- [ ] Check disk usage: `df -h /opt/sensorflow/`
|
| 86 |
-
- [ ] Verify API responding: `curl http://sensorflow.wael.bot/health/studio`
|
| 87 |
-
|
| 88 |
-
### Weekly
|
| 89 |
-
- [ ] Review full logs: `sudo journalctl -u sensorflow-studio -p warning`
|
| 90 |
-
- [ ] Check model accuracy: Review `/opt/sensorflow/studio/runs/detect/`
|
| 91 |
-
- [ ] Backup database (if applicable)
|
| 92 |
-
|
| 93 |
-
### Monthly
|
| 94 |
-
- [ ] Review performance metrics
|
| 95 |
-
- [ ] Update dependencies: `pip list --outdated`
|
| 96 |
-
- [ ] Test disaster recovery
|
| 97 |
-
|
| 98 |
-
## Troubleshooting Quick Reference
|
| 99 |
-
|
| 100 |
-
| Issue | Command |
|
| 101 |
-
|-------|---------|
|
| 102 |
-
| Service won't start | `sudo journalctl -u sensorflow-studio -n 50` |
|
| 103 |
-
| Port conflict | `sudo lsof -i :8001` or `:8002` |
|
| 104 |
-
| Nginx error | `sudo nginx -t && sudo systemctl restart nginx` |
|
| 105 |
-
| DNS not resolving | `nslookup sensorflow.wael.bot` |
|
| 106 |
-
| High memory usage | `ps aux --sort=-%mem \| grep sensorflow` |
|
| 107 |
-
| Can't access API | Check firewall: `sudo ufw status` |
|
| 108 |
-
| Models not found | `bash /opt/sensorflow/studio/deploy/download-models.sh /opt/sensorflow/studio` |
|
| 109 |
-
|
| 110 |
-
## Rollback Procedure
|
| 111 |
-
|
| 112 |
-
If deployment fails:
|
| 113 |
-
|
| 114 |
-
1. Stop services:
|
| 115 |
-
```bash
|
| 116 |
-
sudo systemctl stop sensorflow-studio accident-analysis
|
| 117 |
-
```
|
| 118 |
-
|
| 119 |
-
2. Remove deployment:
|
| 120 |
-
```bash
|
| 121 |
-
sudo rm -rf /opt/sensorflow
|
| 122 |
-
```
|
| 123 |
-
|
| 124 |
-
3. Re-run deployment with fixed parameters:
|
| 125 |
-
```bash
|
| 126 |
-
sudo bash /tmp/sensorflow-deploy/deploy/setup-dgx.sh /tmp/sensorflow-deploy
|
| 127 |
-
```
|
| 128 |
-
|
| 129 |
-
## Completion Checklist
|
| 130 |
-
|
| 131 |
-
- [ ] DNS resolves to DGX
|
| 132 |
-
- [ ] Frontend accessible at sensorflow.wael.bot
|
| 133 |
-
- [ ] Studio API responding at /api/studio
|
| 134 |
-
- [ ] Analysis API responding at /api/analysis
|
| 135 |
-
- [ ] WebSocket connection works for telemetry
|
| 136 |
-
- [ ] Models downloaded and verified
|
| 137 |
-
- [ ] Services auto-restart on failure
|
| 138 |
-
- [ ] Logs are being captured
|
| 139 |
-
- [ ] Performance is acceptable
|
| 140 |
-
- [ ] Documentation updated (this file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/DEPLOYMENT.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 1 |
-
# Sensorflow Deployment Guide - DGX Spark
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
This deployment sets up two persistent FastAPI services on your DGX Spark machine via Tailscale:
|
| 6 |
-
- **Sensorflow Studio**: YOLOv8 training and inference service (port 8001)
|
| 7 |
-
- **Accident Analysis Platform**: Data analysis and WebSocket API (port 8002)
|
| 8 |
-
- **Frontend**: React dashboard via Vite (served at `/`)
|
| 9 |
-
|
| 10 |
-
All services are reverse-proxied through nginx at `sensorflow.wael.bot`
|
| 11 |
-
|
| 12 |
-
## Prerequisites
|
| 13 |
-
|
| 14 |
-
- DGX Spark machine accessible via Tailscale
|
| 15 |
-
- SSH access configured (credentials in local SSH config)
|
| 16 |
-
- Ubuntu/Debian-based Linux on DGX
|
| 17 |
-
- sudo privileges on DGX
|
| 18 |
-
- Node.js 18+ (for frontend build)
|
| 19 |
-
- Python 3.9+
|
| 20 |
-
|
| 21 |
-
## Quick Start
|
| 22 |
-
|
| 23 |
-
### 1. Prepare Code on Local Machine
|
| 24 |
-
|
| 25 |
-
```bash
|
| 26 |
-
cd /path/to/DrivingRepo
|
| 27 |
-
# Cleanup already done, verify:
|
| 28 |
-
ls -la | grep -v "test_\|example_\|\.venv\|yolov8.*\.pt"
|
| 29 |
-
```
|
| 30 |
-
|
| 31 |
-
### 2. Transfer Code to DGX via Tailscale SSH
|
| 32 |
-
|
| 33 |
-
```bash
|
| 34 |
-
# Using your SSH config
|
| 35 |
-
rsync -avz --exclude='.venv' --exclude='node_modules' \
|
| 36 |
-
/path/to/DrivingRepo/ \
|
| 37 |
-
<your-dgx-host>:/tmp/sensorflow-deploy/
|
| 38 |
-
```
|
| 39 |
-
|
| 40 |
-
Replace `<your-dgx-host>` with your DGX Tailscale hostname or IP.
|
| 41 |
-
|
| 42 |
-
### 3. Run Deployment on DGX
|
| 43 |
-
|
| 44 |
-
```bash
|
| 45 |
-
# SSH into DGX
|
| 46 |
-
ssh <your-dgx-host>
|
| 47 |
-
|
| 48 |
-
# Run deployment script
|
| 49 |
-
cd /tmp/sensorflow-deploy
|
| 50 |
-
chmod +x deploy/setup-dgx.sh
|
| 51 |
-
sudo bash deploy/setup-dgx.sh /tmp/sensorflow-deploy
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
### 4. Configure DNS (GoDaddy)
|
| 55 |
-
|
| 56 |
-
Add an A record pointing to your DGX Spark's external IP or Tailscale IP:
|
| 57 |
-
- **Domain**: sensorflow.wael.bot
|
| 58 |
-
- **Type**: A
|
| 59 |
-
- **Value**: Your DGX Spark's IP
|
| 60 |
-
- **TTL**: 3600
|
| 61 |
-
|
| 62 |
-
If using Tailscale:
|
| 63 |
-
- Get your Tailscale IP: `tailscale ip -4`
|
| 64 |
-
- Use this IP for the A record
|
| 65 |
-
|
| 66 |
-
## Service Management
|
| 67 |
-
|
| 68 |
-
Check service status:
|
| 69 |
-
```bash
|
| 70 |
-
sudo systemctl status sensorflow-studio accident-analysis
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
View logs:
|
| 74 |
-
```bash
|
| 75 |
-
# Studio backend
|
| 76 |
-
sudo journalctl -u sensorflow-studio -f
|
| 77 |
-
|
| 78 |
-
# Analysis backend
|
| 79 |
-
sudo journalctl -u accident-analysis -f
|
| 80 |
-
|
| 81 |
-
# Nginx
|
| 82 |
-
sudo tail -f /var/log/nginx/error.log
|
| 83 |
-
```
|
| 84 |
-
|
| 85 |
-
Restart services:
|
| 86 |
-
```bash
|
| 87 |
-
sudo systemctl restart sensorflow-studio accident-analysis
|
| 88 |
-
```
|
| 89 |
-
|
| 90 |
-
Stop services:
|
| 91 |
-
```bash
|
| 92 |
-
sudo systemctl stop sensorflow-studio accident-analysis
|
| 93 |
-
```
|
| 94 |
-
|
| 95 |
-
## API Endpoints
|
| 96 |
-
|
| 97 |
-
### Studio (YOLOv8)
|
| 98 |
-
- `POST /api/studio/train` - Start training
|
| 99 |
-
- `GET /api/studio/train/logs` - Stream training logs
|
| 100 |
-
- `POST /api/studio/infer` - Run inference
|
| 101 |
-
- `POST /api/studio/export` - Export model
|
| 102 |
-
- Interactive docs: `/api/studio/docs`
|
| 103 |
-
|
| 104 |
-
### Analysis (Accident Data)
|
| 105 |
-
- `POST /api/analysis/upload` - Upload accident data
|
| 106 |
-
- `GET /api/analysis/analyze` - Analyze data
|
| 107 |
-
- `WS /ws/telemetry` - Real-time telemetry WebSocket
|
| 108 |
-
- Interactive docs: `/api/analysis/docs`
|
| 109 |
-
|
| 110 |
-
### Frontend
|
| 111 |
-
- `GET /` - React dashboard
|
| 112 |
-
|
| 113 |
-
## Directory Structure on DGX
|
| 114 |
-
|
| 115 |
-
```
|
| 116 |
-
/opt/sensorflow/
|
| 117 |
-
├── studio/
|
| 118 |
-
│ ├── app_backend.py
|
| 119 |
-
│ ├── main.py
|
| 120 |
-
│ ├── venv/ # Python venv
|
| 121 |
-
│ ├── dist/ # Built React app
|
| 122 |
-
│ ├── src/ # React source
|
| 123 |
-
│ ├── requirements-prod.txt
|
| 124 |
-
│ └── deploy/
|
| 125 |
-
├── analysis/
|
| 126 |
-
│ ├── main.py
|
| 127 |
-
│ ├── venv/
|
| 128 |
-
│ └── accident_*.py
|
| 129 |
-
└── models/
|
| 130 |
-
├── yolov8n.pt
|
| 131 |
-
└── yolov8m.pt
|
| 132 |
-
```
|
| 133 |
-
|
| 134 |
-
## Monitoring
|
| 135 |
-
|
| 136 |
-
Check if services are responding:
|
| 137 |
-
```bash
|
| 138 |
-
curl http://sensorflow.wael.bot/health/studio
|
| 139 |
-
curl http://sensorflow.wael.bot/health/analysis
|
| 140 |
-
curl http://sensorflow.wael.bot/
|
| 141 |
-
```
|
| 142 |
-
|
| 143 |
-
Monitor system resources:
|
| 144 |
-
```bash
|
| 145 |
-
# On DGX
|
| 146 |
-
nvidia-smi # GPU usage
|
| 147 |
-
top -u sensorflow # CPU/Memory
|
| 148 |
-
```
|
| 149 |
-
|
| 150 |
-
## Troubleshooting
|
| 151 |
-
|
| 152 |
-
### Services fail to start
|
| 153 |
-
```bash
|
| 154 |
-
sudo journalctl -u sensorflow-studio -n 50
|
| 155 |
-
sudo systemctl status sensorflow-studio
|
| 156 |
-
```
|
| 157 |
-
|
| 158 |
-
### Port conflicts
|
| 159 |
-
```bash
|
| 160 |
-
sudo lsof -i :8001 # Check port 8001
|
| 161 |
-
sudo lsof -i :8002 # Check port 8002
|
| 162 |
-
```
|
| 163 |
-
|
| 164 |
-
### Nginx issues
|
| 165 |
-
```bash
|
| 166 |
-
sudo nginx -t # Test config
|
| 167 |
-
sudo systemctl restart nginx
|
| 168 |
-
tail -f /var/log/nginx/error.log
|
| 169 |
-
```
|
| 170 |
-
|
| 171 |
-
### Model download failed
|
| 172 |
-
```bash
|
| 173 |
-
# Manual download
|
| 174 |
-
cd /opt/sensorflow/studio
|
| 175 |
-
bash deploy/download-models.sh .
|
| 176 |
-
```
|
| 177 |
-
|
| 178 |
-
## Updates & Maintenance
|
| 179 |
-
|
| 180 |
-
To update code:
|
| 181 |
-
```bash
|
| 182 |
-
# On DGX
|
| 183 |
-
cd /opt/sensorflow/studio
|
| 184 |
-
git pull # if using git
|
| 185 |
-
# or rsync again from local machine
|
| 186 |
-
|
| 187 |
-
# Restart services
|
| 188 |
-
sudo systemctl restart sensorflow-studio accident-analysis
|
| 189 |
-
```
|
| 190 |
-
|
| 191 |
-
## License
|
| 192 |
-
|
| 193 |
-
This project is released under **CC BY-NC-SA 4.0**.
|
| 194 |
-
See LICENSE file for details.
|
| 195 |
-
|
| 196 |
-
## Support
|
| 197 |
-
|
| 198 |
-
For issues or questions, check:
|
| 199 |
-
1. Service logs: `sudo journalctl -u sensorflow-studio`
|
| 200 |
-
2. Nginx config: `/etc/nginx/sites-available/sensorflow.conf`
|
| 201 |
-
3. Python venv activation: `/opt/sensorflow/studio/venv/bin/python`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/accident-analysis.service
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
[Unit]
|
| 2 |
-
Description=Accident Analysis Platform API
|
| 3 |
-
After=network.target
|
| 4 |
-
StartLimitIntervalSec=0
|
| 5 |
-
|
| 6 |
-
[Service]
|
| 7 |
-
Type=simple
|
| 8 |
-
User=sensorflow
|
| 9 |
-
WorkingDirectory=/opt/sensorflow/analysis
|
| 10 |
-
ExecStart=/opt/sensorflow/analysis/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8002 --workers 4
|
| 11 |
-
Restart=always
|
| 12 |
-
RestartSec=10
|
| 13 |
-
StandardOutput=journal
|
| 14 |
-
StandardError=journal
|
| 15 |
-
|
| 16 |
-
[Install]
|
| 17 |
-
WantedBy=multi-user.target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/download-models.sh
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Download YOLOv8 models required for deployment
|
| 3 |
-
|
| 4 |
-
set -e
|
| 5 |
-
|
| 6 |
-
MODEL_DIR="${1:-.}"
|
| 7 |
-
mkdir -p "$MODEL_DIR"
|
| 8 |
-
|
| 9 |
-
echo "Downloading YOLOv8 models..."
|
| 10 |
-
|
| 11 |
-
# YOLOv8 Nano (small/fast)
|
| 12 |
-
if [ ! -f "$MODEL_DIR/yolov8n.pt" ]; then
|
| 13 |
-
echo "Downloading yolov8n.pt..."
|
| 14 |
-
wget -q https://github.com/ultralytics/assets/releases/download/v8.1.0/yolov8n.pt -O "$MODEL_DIR/yolov8n.pt"
|
| 15 |
-
echo "✓ yolov8n.pt downloaded"
|
| 16 |
-
fi
|
| 17 |
-
|
| 18 |
-
# YOLOv8 Medium (balanced)
|
| 19 |
-
if [ ! -f "$MODEL_DIR/yolov8m.pt" ]; then
|
| 20 |
-
echo "Downloading yolov8m.pt..."
|
| 21 |
-
wget -q https://github.com/ultralytics/assets/releases/download/v8.1.0/yolov8m.pt -O "$MODEL_DIR/yolov8m.pt"
|
| 22 |
-
echo "✓ yolov8m.pt downloaded"
|
| 23 |
-
fi
|
| 24 |
-
|
| 25 |
-
echo "Models ready at $MODEL_DIR"
|
| 26 |
-
|
| 27 |
-
# SAM ViT-B checkpoint for 3D perception pipeline
|
| 28 |
-
SAM_DIR="$MODEL_DIR/models"
|
| 29 |
-
mkdir -p "$SAM_DIR"
|
| 30 |
-
if [ ! -f "$SAM_DIR/sam_vit_b.pth" ]; then
|
| 31 |
-
echo "Downloading SAM ViT-B checkpoint..."
|
| 32 |
-
wget -q https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth -O "$SAM_DIR/sam_vit_b.pth"
|
| 33 |
-
echo "✓ sam_vit_b.pth downloaded"
|
| 34 |
-
fi
|
| 35 |
-
|
| 36 |
-
echo "All models ready."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/huggingface/README.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Sensorflow Studio
|
| 3 |
-
emoji: 🛰️
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: false
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
# Sensorflow Studio
|
| 12 |
-
|
| 13 |
-
An autonomous-driving data platform: dataset exploration, label evaluation,
|
| 14 |
-
MITL (model-in-the-loop) review tooling, and the SSAM Safety Dashboard for
|
| 15 |
-
statewide surrogate-safety analytics.
|
| 16 |
-
|
| 17 |
-
## What's served
|
| 18 |
-
|
| 19 |
-
- `/` — legacy static UI (dataset / training studio)
|
| 20 |
-
- `/dashboard/` — React (Vite) frontend, including the SSAM Safety Dashboard
|
| 21 |
-
- `/api/...` — FastAPI backend endpoints (e.g. `POST /api/ssam/statewide`)
|
| 22 |
-
|
| 23 |
-
## Notes for this Space
|
| 24 |
-
|
| 25 |
-
This is a CPU Space running a slim dependency set. Features requiring GPU or
|
| 26 |
-
local services — YOLO training, SAM-assisted annotation, and the Ollama
|
| 27 |
-
copilot — are not functional here; the dashboards and analytics APIs are.
|
| 28 |
-
|
| 29 |
-
Deployed from the project repository via
|
| 30 |
-
`deploy/huggingface/deploy.py` (uploads files with `huggingface_hub`; the
|
| 31 |
-
frontend is built inside the Docker image).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/huggingface/deploy.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python
|
| 2 |
-
"""Deploy Sensorflow Studio to a Hugging Face Space (Docker SDK).
|
| 3 |
-
|
| 4 |
-
Idempotent / re-runnable: creates the Space if missing, uploads the current
|
| 5 |
-
repo state (frontend is built inside the Docker image on HF's side), waits for
|
| 6 |
-
the build, and smoke-tests the deployed endpoints.
|
| 7 |
-
|
| 8 |
-
Usage:
|
| 9 |
-
.venv/bin/python deploy/huggingface/deploy.py [--space-name NAME] [--no-wait]
|
| 10 |
-
|
| 11 |
-
Requires a Hugging Face token with write access (`hf auth login` or HF_TOKEN).
|
| 12 |
-
"""
|
| 13 |
-
import argparse
|
| 14 |
-
import json
|
| 15 |
-
import sys
|
| 16 |
-
import time
|
| 17 |
-
import urllib.request
|
| 18 |
-
from pathlib import Path
|
| 19 |
-
|
| 20 |
-
from huggingface_hub import HfApi
|
| 21 |
-
from huggingface_hub.errors import LocalTokenNotFoundError
|
| 22 |
-
|
| 23 |
-
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 24 |
-
|
| 25 |
-
# Mirrors .dockerignore, plus files that must not reach the Space repo as-is.
|
| 26 |
-
IGNORE_PATTERNS = [
|
| 27 |
-
"node_modules/**",
|
| 28 |
-
".venv/**",
|
| 29 |
-
".git/**",
|
| 30 |
-
"runs/**",
|
| 31 |
-
"dist/**",
|
| 32 |
-
"models/**",
|
| 33 |
-
"*.pt",
|
| 34 |
-
"**/*.pt",
|
| 35 |
-
"**/__pycache__/**",
|
| 36 |
-
".pytest_cache/**",
|
| 37 |
-
".claude/**",
|
| 38 |
-
".DS_Store",
|
| 39 |
-
"**/.DS_Store",
|
| 40 |
-
"README.md", # root project README; the Space README is uploaded separately
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
TERMINAL_ERROR_STAGES = {"BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR", "DELETING"}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def http_check(url: str, method: str = "GET", body: dict | None = None) -> str:
|
| 47 |
-
data = json.dumps(body).encode() if body is not None else None
|
| 48 |
-
req = urllib.request.Request(
|
| 49 |
-
url, data=data, method=method,
|
| 50 |
-
headers={"Content-Type": "application/json"} if body is not None else {},
|
| 51 |
-
)
|
| 52 |
-
try:
|
| 53 |
-
with urllib.request.urlopen(req, timeout=30) as resp:
|
| 54 |
-
snippet = resp.read(200).decode(errors="replace").replace("\n", " ")
|
| 55 |
-
return f"{resp.status} {snippet[:120]}"
|
| 56 |
-
except Exception as exc: # noqa: BLE001 - smoke test, report anything
|
| 57 |
-
return f"FAILED: {exc}"
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def main() -> int:
|
| 61 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 62 |
-
parser.add_argument("--space-name", default="sensorflow-studio")
|
| 63 |
-
parser.add_argument("--no-wait", action="store_true",
|
| 64 |
-
help="upload only; don't wait for the build")
|
| 65 |
-
parser.add_argument("--timeout-minutes", type=float, default=15)
|
| 66 |
-
args = parser.parse_args()
|
| 67 |
-
|
| 68 |
-
api = HfApi()
|
| 69 |
-
try:
|
| 70 |
-
user = api.whoami()["name"]
|
| 71 |
-
except LocalTokenNotFoundError:
|
| 72 |
-
print("ERROR: no Hugging Face token found. Run `hf auth login` "
|
| 73 |
-
"(or set HF_TOKEN) with a WRITE token, then re-run this script.")
|
| 74 |
-
return 1
|
| 75 |
-
|
| 76 |
-
repo_id = f"{user}/{args.space_name}"
|
| 77 |
-
print(f"Deploying to Space: {repo_id}")
|
| 78 |
-
|
| 79 |
-
api.create_repo(repo_id, repo_type="space", space_sdk="docker", exist_ok=True)
|
| 80 |
-
|
| 81 |
-
print("Uploading repository files...")
|
| 82 |
-
api.upload_folder(
|
| 83 |
-
folder_path=str(REPO_ROOT),
|
| 84 |
-
repo_id=repo_id,
|
| 85 |
-
repo_type="space",
|
| 86 |
-
ignore_patterns=IGNORE_PATTERNS,
|
| 87 |
-
commit_message="Deploy Sensorflow Studio",
|
| 88 |
-
)
|
| 89 |
-
# The Space README (with docker frontmatter) replaces the project README.
|
| 90 |
-
api.upload_file(
|
| 91 |
-
path_or_fileobj=str(REPO_ROOT / "deploy" / "huggingface" / "README.md"),
|
| 92 |
-
path_in_repo="README.md",
|
| 93 |
-
repo_id=repo_id,
|
| 94 |
-
repo_type="space",
|
| 95 |
-
commit_message="Update Space README",
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
space_url = f"https://{user}-{args.space_name.replace('_', '-')}.hf.space"
|
| 99 |
-
print(f"Space page: https://huggingface.co/spaces/{repo_id}")
|
| 100 |
-
print(f"App URL: {space_url}/")
|
| 101 |
-
|
| 102 |
-
if args.no_wait:
|
| 103 |
-
return 0
|
| 104 |
-
|
| 105 |
-
print("Waiting for the Space build (this can take several minutes)...")
|
| 106 |
-
deadline = time.time() + args.timeout_minutes * 60
|
| 107 |
-
stage = None
|
| 108 |
-
while time.time() < deadline:
|
| 109 |
-
runtime = api.get_space_runtime(repo_id)
|
| 110 |
-
if runtime.stage != stage:
|
| 111 |
-
stage = runtime.stage
|
| 112 |
-
print(f" stage: {stage}")
|
| 113 |
-
if stage == "RUNNING":
|
| 114 |
-
break
|
| 115 |
-
if stage in TERMINAL_ERROR_STAGES:
|
| 116 |
-
print(f"Build/runtime failed (stage={stage}). Check logs at "
|
| 117 |
-
f"https://huggingface.co/spaces/{repo_id}?logs=build")
|
| 118 |
-
return 2
|
| 119 |
-
time.sleep(15)
|
| 120 |
-
else:
|
| 121 |
-
print(f"Timed out after {args.timeout_minutes} min (last stage: {stage}).")
|
| 122 |
-
return 3
|
| 123 |
-
|
| 124 |
-
print("Space is RUNNING. Verifying endpoints...")
|
| 125 |
-
print(f" GET / -> {http_check(space_url + '/')}")
|
| 126 |
-
print(f" GET /dashboard/ -> {http_check(space_url + '/dashboard/')}")
|
| 127 |
-
print(f" POST /api/ssam/statewide -> "
|
| 128 |
-
f"{http_check(space_url + '/api/ssam/statewide', 'POST', {'page': 1, 'page_size': 1})}")
|
| 129 |
-
return 0
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
if __name__ == "__main__":
|
| 133 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/huggingface/requirements-space.txt
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
# Slim dependency set for the Hugging Face Space (CPU, limited build time).
|
| 2 |
-
# Heavy GPU/vision deps from requirements.txt (torch, ultralytics, opencv) are
|
| 3 |
-
# lazily imported inside route handlers and intentionally excluded here.
|
| 4 |
-
# Versions mirror the working local .venv where pinned in requirements.txt.
|
| 5 |
-
fastapi==0.140.0
|
| 6 |
-
uvicorn==0.51.0
|
| 7 |
-
pydantic==2.13.4
|
| 8 |
-
httpx==0.28.1
|
| 9 |
-
numpy==2.5.1
|
| 10 |
-
scipy
|
| 11 |
-
pandas==3.0.5
|
| 12 |
-
PyYAML==6.0.3
|
| 13 |
-
python-multipart==0.0.32
|
| 14 |
-
scikit-learn
|
| 15 |
-
requests==2.34.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/nginx.conf
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
upstream studio_backend {
|
| 2 |
-
server 127.0.0.1:8001;
|
| 3 |
-
}
|
| 4 |
-
|
| 5 |
-
upstream analysis_backend {
|
| 6 |
-
server 127.0.0.1:8002;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
server {
|
| 10 |
-
listen 80;
|
| 11 |
-
server_name sensorflow.wael.bot;
|
| 12 |
-
client_max_body_size 100M;
|
| 13 |
-
|
| 14 |
-
# Redirect HTTP to HTTPS (when cert is ready)
|
| 15 |
-
# return 301 https://$server_name$request_uri;
|
| 16 |
-
|
| 17 |
-
# Sensorflow Studio API
|
| 18 |
-
location /api/studio/ {
|
| 19 |
-
proxy_pass http://studio_backend/;
|
| 20 |
-
proxy_set_header Host $host;
|
| 21 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 22 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 23 |
-
proxy_set_header X-Forwarded-Proto $scheme;
|
| 24 |
-
proxy_read_timeout 300s;
|
| 25 |
-
proxy_connect_timeout 300s;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
# Accident Analysis API
|
| 29 |
-
location /api/analysis/ {
|
| 30 |
-
proxy_pass http://analysis_backend/;
|
| 31 |
-
proxy_set_header Host $host;
|
| 32 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 33 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 34 |
-
proxy_set_header X-Forwarded-Proto $scheme;
|
| 35 |
-
proxy_read_timeout 300s;
|
| 36 |
-
proxy_connect_timeout 300s;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
# WebSocket support for Accident Analysis
|
| 40 |
-
location /ws/ {
|
| 41 |
-
proxy_pass http://analysis_backend/ws/;
|
| 42 |
-
proxy_http_version 1.1;
|
| 43 |
-
proxy_set_header Upgrade $http_upgrade;
|
| 44 |
-
proxy_set_header Connection "upgrade";
|
| 45 |
-
proxy_set_header Host $host;
|
| 46 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 47 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 48 |
-
proxy_read_timeout 86400;
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
# React Frontend
|
| 52 |
-
location / {
|
| 53 |
-
root /opt/sensorflow/studio/dist;
|
| 54 |
-
try_files $uri $uri/ /index.html;
|
| 55 |
-
expires 1h;
|
| 56 |
-
add_header Cache-Control "public, max-age=3600";
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
# Health checks
|
| 60 |
-
location /health/studio {
|
| 61 |
-
proxy_pass http://studio_backend/docs;
|
| 62 |
-
access_log off;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
location /health/analysis {
|
| 66 |
-
proxy_pass http://analysis_backend/docs;
|
| 67 |
-
access_log off;
|
| 68 |
-
}
|
| 69 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/sensorflow-studio.service
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
[Unit]
|
| 2 |
-
Description=Sensorflow Studio Backend (YOLOv8 Training/Inference)
|
| 3 |
-
After=network.target
|
| 4 |
-
StartLimitIntervalSec=0
|
| 5 |
-
|
| 6 |
-
[Service]
|
| 7 |
-
Type=simple
|
| 8 |
-
User=sensorflow
|
| 9 |
-
WorkingDirectory=/opt/sensorflow/studio
|
| 10 |
-
ExecStart=/opt/sensorflow/studio/venv/bin/uvicorn app_backend:app --host 127.0.0.1 --port 8001 --workers 4
|
| 11 |
-
Restart=always
|
| 12 |
-
RestartSec=10
|
| 13 |
-
StandardOutput=journal
|
| 14 |
-
StandardError=journal
|
| 15 |
-
|
| 16 |
-
[Install]
|
| 17 |
-
WantedBy=multi-user.target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/deploy/setup-dgx.sh
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Sensorflow DGX Spark Deployment Setup
|
| 3 |
-
# Run this on the DGX Spark machine
|
| 4 |
-
|
| 5 |
-
set -e
|
| 6 |
-
|
| 7 |
-
APP_BASE="/opt/sensorflow"
|
| 8 |
-
STUDIO_DIR="$APP_BASE/studio"
|
| 9 |
-
ANALYSIS_DIR="$APP_BASE/analysis"
|
| 10 |
-
REPO_URL="${1:-$(cd /tmp && pwd)}"
|
| 11 |
-
|
| 12 |
-
echo "🚀 Sensorflow Deployment to DGX Spark"
|
| 13 |
-
echo "======================================="
|
| 14 |
-
echo "Base directory: $APP_BASE"
|
| 15 |
-
echo "Repo source: $REPO_URL"
|
| 16 |
-
|
| 17 |
-
# Create application user if needed
|
| 18 |
-
if ! id "sensorflow" &>/dev/null; then
|
| 19 |
-
echo "Creating sensorflow system user..."
|
| 20 |
-
sudo useradd -r -s /bin/bash -d $APP_BASE sensorflow
|
| 21 |
-
fi
|
| 22 |
-
|
| 23 |
-
# Setup directories
|
| 24 |
-
echo "Setting up directories..."
|
| 25 |
-
sudo mkdir -p $STUDIO_DIR $ANALYSIS_DIR
|
| 26 |
-
sudo chown -R sensorflow:sensorflow $APP_BASE
|
| 27 |
-
|
| 28 |
-
# Copy code
|
| 29 |
-
echo "Copying application code..."
|
| 30 |
-
sudo cp -r "$REPO_URL"/* $STUDIO_DIR/ 2>/dev/null || true
|
| 31 |
-
mkdir -p $ANALYSIS_DIR
|
| 32 |
-
sudo cp $STUDIO_DIR/main.py $ANALYSIS_DIR/
|
| 33 |
-
|
| 34 |
-
# Create Python virtual environments
|
| 35 |
-
echo "Creating virtual environments..."
|
| 36 |
-
sudo -u sensorflow python3 -m venv $STUDIO_DIR/venv
|
| 37 |
-
sudo -u sensorflow python3 -m venv $ANALYSIS_DIR/venv
|
| 38 |
-
|
| 39 |
-
# Install dependencies
|
| 40 |
-
echo "Installing Python dependencies..."
|
| 41 |
-
sudo -u sensorflow $STUDIO_DIR/venv/bin/pip install --upgrade pip setuptools wheel
|
| 42 |
-
sudo -u sensorflow $STUDIO_DIR/venv/bin/pip install -r $STUDIO_DIR/requirements-prod.txt
|
| 43 |
-
sudo -u sensorflow $ANALYSIS_DIR/venv/bin/pip install -r $STUDIO_DIR/requirements-prod.txt
|
| 44 |
-
|
| 45 |
-
# Download models
|
| 46 |
-
echo "Downloading YOLOv8 models..."
|
| 47 |
-
sudo -u sensorflow bash $STUDIO_DIR/deploy/download-models.sh $STUDIO_DIR
|
| 48 |
-
|
| 49 |
-
# Build React frontend
|
| 50 |
-
echo "Building React frontend..."
|
| 51 |
-
cd $STUDIO_DIR
|
| 52 |
-
if [ -f "package.json" ]; then
|
| 53 |
-
npm install
|
| 54 |
-
npm run build
|
| 55 |
-
echo "Frontend built at $STUDIO_DIR/dist"
|
| 56 |
-
fi
|
| 57 |
-
|
| 58 |
-
# Setup systemd services
|
| 59 |
-
echo "Installing systemd services..."
|
| 60 |
-
sudo cp $STUDIO_DIR/deploy/sensorflow-studio.service /etc/systemd/system/
|
| 61 |
-
sudo cp $STUDIO_DIR/deploy/accident-analysis.service /etc/systemd/system/
|
| 62 |
-
sudo systemctl daemon-reload
|
| 63 |
-
|
| 64 |
-
# Setup nginx
|
| 65 |
-
echo "Configuring nginx..."
|
| 66 |
-
sudo cp $STUDIO_DIR/deploy/nginx.conf /etc/nginx/sites-available/sensorflow.conf
|
| 67 |
-
sudo ln -sf /etc/nginx/sites-available/sensorflow.conf /etc/nginx/sites-enabled/ 2>/dev/null || true
|
| 68 |
-
sudo nginx -t
|
| 69 |
-
sudo systemctl restart nginx
|
| 70 |
-
|
| 71 |
-
# Start services
|
| 72 |
-
echo "Starting services..."
|
| 73 |
-
sudo systemctl enable sensorflow-studio accident-analysis
|
| 74 |
-
sudo systemctl start sensorflow-studio accident-analysis
|
| 75 |
-
sudo systemctl status sensorflow-studio accident-analysis
|
| 76 |
-
|
| 77 |
-
echo ""
|
| 78 |
-
echo "✅ Deployment Complete!"
|
| 79 |
-
echo "======================================="
|
| 80 |
-
echo "Services:"
|
| 81 |
-
echo " - Sensorflow Studio: http://sensorflow.wael.bot/api/studio"
|
| 82 |
-
echo " - Accident Analysis: http://sensorflow.wael.bot/api/analysis"
|
| 83 |
-
echo " - Frontend: http://sensorflow.wael.bot"
|
| 84 |
-
echo ""
|
| 85 |
-
echo "Monitor logs with:"
|
| 86 |
-
echo " sudo journalctl -u sensorflow-studio -f"
|
| 87 |
-
echo " sudo journalctl -u accident-analysis -f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/index.html
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en" data-theme="dark">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>Sensorflow Studio — L4 Perception Label Evaluation</title>
|
| 7 |
-
<script type="module" src="/src/index.tsx"></script>
|
| 8 |
-
</head>
|
| 9 |
-
<body><div id="root"></div></body>
|
| 10 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/infer.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Inference script for YOLO Auto-Labeler.
|
| 4 |
-
Runs predictions on images and outputs structured JSON labels + annotated images.
|
| 5 |
-
Usage: python infer.py --source data/test.jpg --weights runs/detect/coco_finetuned/weights/best.pt
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import argparse
|
| 9 |
-
import json
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from ultralytics import YOLO
|
| 12 |
-
from taxonomy import TAXONOMY_MAP
|
| 13 |
-
|
| 14 |
-
def main():
|
| 15 |
-
parser = argparse.ArgumentParser(description="Run YOLOv8 inference and save structured predictions")
|
| 16 |
-
parser.add_argument("--source", type=str, required=True, help="Path to image file or directory")
|
| 17 |
-
parser.add_argument("--weights", type=str, default="runs/detect/coco_finetuned/weights/best.pt", help="Path to model weights")
|
| 18 |
-
parser.add_argument("--conf", type=float, default=0.25, help="Confidence threshold")
|
| 19 |
-
parser.add_argument("--iou", type=float, default=0.45, help="NMS IoU threshold")
|
| 20 |
-
parser.add_argument("--device", default="0", help="GPU device ID, or cpu/-1 for CPU")
|
| 21 |
-
parser.add_argument("--output", type=str, default="runs/infer", help="Directory to save predictions and images")
|
| 22 |
-
|
| 23 |
-
args = parser.parse_args()
|
| 24 |
-
|
| 25 |
-
# Resolve device parameter
|
| 26 |
-
if args.device == "dgx-spark":
|
| 27 |
-
print("⚡ [DGX-Spark] Connected to Tailscale MagicDNS endpoint (dgx-spark.tail16d8d9.ts.net)")
|
| 28 |
-
print("⚡ [DGX-Spark] Offloading inference workload to NVIDIA DGX Spark remote cluster.")
|
| 29 |
-
device = "0"
|
| 30 |
-
else:
|
| 31 |
-
try:
|
| 32 |
-
device = int(args.device)
|
| 33 |
-
if device < 0:
|
| 34 |
-
device = "cpu"
|
| 35 |
-
except ValueError:
|
| 36 |
-
device = args.device
|
| 37 |
-
|
| 38 |
-
print(f"Loading model: {args.weights}")
|
| 39 |
-
model = YOLO(args.weights)
|
| 40 |
-
|
| 41 |
-
# Create output directory
|
| 42 |
-
output_dir = Path(args.output)
|
| 43 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 44 |
-
|
| 45 |
-
# Identify source images
|
| 46 |
-
source_path = Path(args.source)
|
| 47 |
-
if source_path.is_dir():
|
| 48 |
-
image_files = []
|
| 49 |
-
for ext in ["*.jpg", "*.jpeg", "*.png", "*.bmp"]:
|
| 50 |
-
image_files.extend(source_path.glob(ext))
|
| 51 |
-
image_files.extend(source_path.glob(ext.upper()))
|
| 52 |
-
image_files = sorted(list(set(image_files)))
|
| 53 |
-
else:
|
| 54 |
-
image_files = [source_path]
|
| 55 |
-
|
| 56 |
-
if not image_files:
|
| 57 |
-
print(f"No images found at source: {args.source}")
|
| 58 |
-
return
|
| 59 |
-
|
| 60 |
-
print(f"Running inference on {len(image_files)} image(s)...")
|
| 61 |
-
|
| 62 |
-
predictions = []
|
| 63 |
-
|
| 64 |
-
for img_path in image_files:
|
| 65 |
-
print(f" Predicting: {img_path.name}")
|
| 66 |
-
results = model.predict(
|
| 67 |
-
source=str(img_path),
|
| 68 |
-
conf=args.conf,
|
| 69 |
-
iou=args.iou,
|
| 70 |
-
device=device,
|
| 71 |
-
verbose=False
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
# Save annotated image
|
| 75 |
-
result = results[0]
|
| 76 |
-
annotated_path = output_dir / f"annotated_{img_path.name}"
|
| 77 |
-
result.save(filename=str(annotated_path))
|
| 78 |
-
|
| 79 |
-
# Extract structured predictions
|
| 80 |
-
for box in result.boxes:
|
| 81 |
-
class_id = int(box.cls[0])
|
| 82 |
-
class_name = model.names[class_id]
|
| 83 |
-
confidence = float(box.conf[0])
|
| 84 |
-
bbox_coords = box.xyxy[0].tolist() # [x1, y1, x2, y2]
|
| 85 |
-
|
| 86 |
-
pred = {
|
| 87 |
-
"image": img_path.name,
|
| 88 |
-
"class_id": class_id,
|
| 89 |
-
"class_name": class_name,
|
| 90 |
-
"taxonomy": TAXONOMY_MAP.get(class_name, "Unknown"),
|
| 91 |
-
"confidence": confidence,
|
| 92 |
-
"bbox": {
|
| 93 |
-
"x1": bbox_coords[0],
|
| 94 |
-
"y1": bbox_coords[1],
|
| 95 |
-
"x2": bbox_coords[2],
|
| 96 |
-
"y2": bbox_coords[3],
|
| 97 |
-
},
|
| 98 |
-
}
|
| 99 |
-
predictions.append(pred)
|
| 100 |
-
|
| 101 |
-
# Save predictions as JSON
|
| 102 |
-
predictions_file = output_dir / "predictions.json"
|
| 103 |
-
with open(predictions_file, "w") as f:
|
| 104 |
-
json.dump(predictions, f, indent=2)
|
| 105 |
-
|
| 106 |
-
print(f"✓ Inference complete! Processed {len(image_files)} images.")
|
| 107 |
-
print(f"✓ Predictions saved to: {predictions_file}")
|
| 108 |
-
print(f"✓ Annotated images saved to: {output_dir}/")
|
| 109 |
-
|
| 110 |
-
if __name__ == "__main__":
|
| 111 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/main.py
DELETED
|
@@ -1,386 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accident Analysis API Gateway - Simplified Version
|
| 3 |
-
For demonstration purposes, providing core functionality only.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import io
|
| 7 |
-
import logging
|
| 8 |
-
from typing import Dict, Any, List, Optional
|
| 9 |
-
from fastapi import FastAPI, UploadFile, File, HTTPException, WebSocket, WebSocketDisconnect
|
| 10 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 11 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
-
import pandas as pd
|
| 13 |
-
import asyncio
|
| 14 |
-
from contextlib import asynccontextmanager
|
| 15 |
-
|
| 16 |
-
# Simplified implementations to avoid dependency issues
|
| 17 |
-
class MockAccidentValidator:
|
| 18 |
-
def __init__(self, df):
|
| 19 |
-
self.df = df
|
| 20 |
-
|
| 21 |
-
def validate_schema(self):
|
| 22 |
-
class Report:
|
| 23 |
-
def __init__(self):
|
| 24 |
-
self.valid = True
|
| 25 |
-
self.errors = []
|
| 26 |
-
self.quality_score = 0.95
|
| 27 |
-
return Report()
|
| 28 |
-
|
| 29 |
-
class MockAccidentDataCleaner:
|
| 30 |
-
def __init__(self, df):
|
| 31 |
-
self.df = df
|
| 32 |
-
|
| 33 |
-
def clean_dataset(self):
|
| 34 |
-
# Remove any rows with missing values
|
| 35 |
-
return self.df.dropna()
|
| 36 |
-
|
| 37 |
-
class MockAccidentAnalysisEngine:
|
| 38 |
-
def __init__(self, df):
|
| 39 |
-
self.df = df
|
| 40 |
-
|
| 41 |
-
def run_full_pipeline(self):
|
| 42 |
-
return {
|
| 43 |
-
"metrics": {
|
| 44 |
-
"total_records": len(self.df),
|
| 45 |
-
"geospatial": {
|
| 46 |
-
"hotspots": [
|
| 47 |
-
{"lat": 37.7749, "lng": -122.4194, "count": 25},
|
| 48 |
-
{"lat": 34.0522, "lng": -118.2437, "count": 18}
|
| 49 |
-
]
|
| 50 |
-
}
|
| 51 |
-
},
|
| 52 |
-
"summary": "Analysis complete for dataset"
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
class MockAccidentInsights:
|
| 56 |
-
def __init__(self, metrics):
|
| 57 |
-
self.metrics = metrics
|
| 58 |
-
|
| 59 |
-
def get_risk_profile(self):
|
| 60 |
-
return {
|
| 61 |
-
"risk_index": 0.75,
|
| 62 |
-
"high_risk_factors": ["urban", "night_hours"],
|
| 63 |
-
"recommendations": [
|
| 64 |
-
"Increase street lighting in high-risk areas",
|
| 65 |
-
"Implement additional safety measures near hotspots"
|
| 66 |
-
],
|
| 67 |
-
"confidence_score": 0.89
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
class MockAccidentReport:
|
| 71 |
-
def __init__(self, metrics, insights):
|
| 72 |
-
self.metrics = metrics
|
| 73 |
-
self.insights = insights
|
| 74 |
-
|
| 75 |
-
def export_to_html(self):
|
| 76 |
-
return f"""
|
| 77 |
-
<html>
|
| 78 |
-
<head><title>Executive Report</title></head>
|
| 79 |
-
<body>
|
| 80 |
-
<h1>Accident Analysis Executive Report</h1>
|
| 81 |
-
<p>Total Records Processed: {self.metrics['metrics']['total_records']}</p>
|
| 82 |
-
<p>Risk Index: {self.insights.get('risk_index', 'N/A')}</p>
|
| 83 |
-
<h2>Key Findings</h2>
|
| 84 |
-
<ul>
|
| 85 |
-
<li>High-risk factors identified</li>
|
| 86 |
-
<li>Hotspot locations provided</li>
|
| 87 |
-
</ul>
|
| 88 |
-
</body>
|
| 89 |
-
</html>
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
# Logging configuration
|
| 93 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 94 |
-
logger = logging.getLogger(__name__)
|
| 95 |
-
|
| 96 |
-
# Lifecycle management for the application
|
| 97 |
-
@asynccontextmanager
|
| 98 |
-
async def lifespan(app: FastAPI):
|
| 99 |
-
logger.info("Application starting up")
|
| 100 |
-
yield
|
| 101 |
-
logger.info("Application shutting down")
|
| 102 |
-
|
| 103 |
-
app = FastAPI(
|
| 104 |
-
title="Accident Analysis Platform",
|
| 105 |
-
description="Simplified API Engine for demonstration purposes",
|
| 106 |
-
version="1.0.0",
|
| 107 |
-
lifespan=lifespan
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
# Add CORS middleware to handle cross-origin requests
|
| 111 |
-
app.add_middleware(
|
| 112 |
-
CORSMiddleware,
|
| 113 |
-
allow_origins=["*"],
|
| 114 |
-
allow_credentials=True,
|
| 115 |
-
allow_methods=["*"],
|
| 116 |
-
allow_headers=["*"],
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
# Thread-safe cache with expiration for processed datasets
|
| 120 |
-
class EngineCache:
|
| 121 |
-
"""Thread-safe cache implementation with TTL for processed datasets."""
|
| 122 |
-
|
| 123 |
-
def __init__(self):
|
| 124 |
-
self._cache: Dict[str, Dict] = {}
|
| 125 |
-
self._lock = asyncio.Lock()
|
| 126 |
-
|
| 127 |
-
async def set(self, key: str, value: Dict[str, Any], ttl: int = 3600):
|
| 128 |
-
"""Set a cached value with TTL."""
|
| 129 |
-
async with self._lock:
|
| 130 |
-
self._cache[key] = {
|
| 131 |
-
"data": value,
|
| 132 |
-
"timestamp": asyncio.get_event_loop().time(),
|
| 133 |
-
"ttl": ttl
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
async def get(self, key: str) -> Optional[Dict[str, Any]]:
|
| 137 |
-
"""Get a cached value, checking TTL."""
|
| 138 |
-
async with self._lock:
|
| 139 |
-
if key not in self._cache:
|
| 140 |
-
return None
|
| 141 |
-
|
| 142 |
-
item = self._cache[key]
|
| 143 |
-
current_time = asyncio.get_event_loop().time()
|
| 144 |
-
|
| 145 |
-
# Check if expired
|
| 146 |
-
if current_time - item["timestamp"] > item["ttl"]:
|
| 147 |
-
del self._cache[key]
|
| 148 |
-
return None
|
| 149 |
-
|
| 150 |
-
return item["data"]
|
| 151 |
-
|
| 152 |
-
async def cleanup_expired(self):
|
| 153 |
-
"""Remove expired cache entries."""
|
| 154 |
-
async with self._lock:
|
| 155 |
-
current_time = asyncio.get_event_loop().time()
|
| 156 |
-
expired_keys = [
|
| 157 |
-
key for key, item in self._cache.items()
|
| 158 |
-
if current_time - item["timestamp"] > item["ttl"]
|
| 159 |
-
]
|
| 160 |
-
for key in expired_keys:
|
| 161 |
-
del self._cache[key]
|
| 162 |
-
|
| 163 |
-
# Global cache instance
|
| 164 |
-
engine_cache = EngineCache()
|
| 165 |
-
|
| 166 |
-
class ConnectionManager:
|
| 167 |
-
"""Manages WebSocket clients subscribing to real-time telemetry updates."""
|
| 168 |
-
|
| 169 |
-
def __init__(self):
|
| 170 |
-
self.active_connections: List[WebSocket] = []
|
| 171 |
-
self._lock = asyncio.Lock()
|
| 172 |
-
|
| 173 |
-
async def connect(self, websocket: WebSocket):
|
| 174 |
-
"""Add a new WebSocket connection."""
|
| 175 |
-
await websocket.accept()
|
| 176 |
-
async with self._lock:
|
| 177 |
-
self.active_connections.append(websocket)
|
| 178 |
-
|
| 179 |
-
def disconnect(self, websocket: WebSocket):
|
| 180 |
-
"""Remove a disconnected WebSocket connection."""
|
| 181 |
-
try:
|
| 182 |
-
with self._lock:
|
| 183 |
-
self.active_connections.remove(websocket)
|
| 184 |
-
except ValueError:
|
| 185 |
-
pass # Already disconnected
|
| 186 |
-
|
| 187 |
-
async def broadcast(self, message: str):
|
| 188 |
-
"""Broadcast a message to all active connections."""
|
| 189 |
-
async with self._lock:
|
| 190 |
-
# Create list of tasks to avoid potential modification during iteration
|
| 191 |
-
tasks = []
|
| 192 |
-
for connection in self.active_connections.copy():
|
| 193 |
-
try:
|
| 194 |
-
tasks.append(connection.send_text(message))
|
| 195 |
-
except Exception as e:
|
| 196 |
-
logger.warning(f"Failed to send to WebSocket: {e}")
|
| 197 |
-
# Connection might be closed, remove it
|
| 198 |
-
try:
|
| 199 |
-
self.active_connections.remove(connection)
|
| 200 |
-
except ValueError:
|
| 201 |
-
pass # Already removed
|
| 202 |
-
|
| 203 |
-
# Execute all sends concurrently
|
| 204 |
-
await asyncio.gather(*tasks, return_exceptions=True)
|
| 205 |
-
|
| 206 |
-
manager = ConnectionManager()
|
| 207 |
-
|
| 208 |
-
@app.post("/api/analyze")
|
| 209 |
-
async def analyze_accident_data(file: UploadFile = File(...)):
|
| 210 |
-
"""
|
| 211 |
-
Uploads a SWITRS formatted CSV dataset, runs validation, cleaning, and initiates the engine.
|
| 212 |
-
|
| 213 |
-
Returns:
|
| 214 |
-
JSON response with analysis results
|
| 215 |
-
"""
|
| 216 |
-
logger.info(f"Received file upload for analysis: {file.filename}")
|
| 217 |
-
|
| 218 |
-
if not file.filename or not file.filename.endswith(".csv"):
|
| 219 |
-
raise HTTPException(status_code=400, detail="Only CSV datasets are accepted.")
|
| 220 |
-
|
| 221 |
-
try:
|
| 222 |
-
# Validate file size (limit to 50MB)
|
| 223 |
-
content = await file.read()
|
| 224 |
-
if len(content) > 50 * 1024 * 1024: # 50 MB limit
|
| 225 |
-
raise HTTPException(status_code=413, detail="File too large. Maximum size is 50MB.")
|
| 226 |
-
|
| 227 |
-
# Parse CSV with error handling
|
| 228 |
-
df = pd.read_csv(io.BytesIO(content))
|
| 229 |
-
logger.info(f"CSV loaded successfully, shape: {df.shape}")
|
| 230 |
-
|
| 231 |
-
# Mock Validation
|
| 232 |
-
validator = MockAccidentValidator(df)
|
| 233 |
-
report = validator.validate_schema()
|
| 234 |
-
if not report.valid:
|
| 235 |
-
logger.warning(f"Dataset validation failed: {report.errors}")
|
| 236 |
-
|
| 237 |
-
# Mock Cleaning
|
| 238 |
-
cleaner = MockAccidentDataCleaner(df)
|
| 239 |
-
clean_df = cleaner.clean_dataset()
|
| 240 |
-
|
| 241 |
-
if clean_df.empty:
|
| 242 |
-
raise HTTPException(status_code=422, detail="Cleaning produced an empty dataset.")
|
| 243 |
-
|
| 244 |
-
# Mock Analysis
|
| 245 |
-
engine = MockAccidentAnalysisEngine(clean_df)
|
| 246 |
-
metrics = engine.run_full_pipeline()
|
| 247 |
-
|
| 248 |
-
# Mock Insights
|
| 249 |
-
insights = MockAccidentInsights(metrics)
|
| 250 |
-
profile = insights.get_risk_profile()
|
| 251 |
-
|
| 252 |
-
# Cache for sub-resource queries with TTL (1 hour)
|
| 253 |
-
await engine_cache.set("latest", {
|
| 254 |
-
"metrics": metrics,
|
| 255 |
-
"insights": profile,
|
| 256 |
-
"processing_time": len(clean_df) # for reference
|
| 257 |
-
}, ttl=3600)
|
| 258 |
-
|
| 259 |
-
# Broadcast trigger over WebSockets
|
| 260 |
-
broadcast_message = f"New dataset processed. Total records: {len(clean_df)}. Risk index: {profile.get('risk_index', 'N/A')}"
|
| 261 |
-
await manager.broadcast(broadcast_message)
|
| 262 |
-
|
| 263 |
-
return {
|
| 264 |
-
"status": "success",
|
| 265 |
-
"records_processed": len(clean_df),
|
| 266 |
-
"quality_score": report.quality_score,
|
| 267 |
-
"insights": profile,
|
| 268 |
-
"processing_time_seconds": len(clean_df) # Add processing info
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
except pd.errors.EmptyDataError:
|
| 272 |
-
raise HTTPException(status_code=400, detail="Empty CSV file provided.")
|
| 273 |
-
except pd.errors.ParserError as e:
|
| 274 |
-
raise HTTPException(status_code=400, detail=f"Invalid CSV format: {str(e)}")
|
| 275 |
-
except Exception as e:
|
| 276 |
-
logger.error(f"Error in dataset ingestion pipeline: {str(e)}", exc_info=True)
|
| 277 |
-
raise HTTPException(status_code=500, detail="Pipeline error occurred during processing")
|
| 278 |
-
|
| 279 |
-
@app.get("/api/hotspots")
|
| 280 |
-
async def get_hotspots():
|
| 281 |
-
"""
|
| 282 |
-
Retrieves geographic hotspots from the latest processed run.
|
| 283 |
-
|
| 284 |
-
Returns:
|
| 285 |
-
JSON response with geospatial hotspots
|
| 286 |
-
"""
|
| 287 |
-
try:
|
| 288 |
-
cached_data = await engine_cache.get("latest")
|
| 289 |
-
if not cached_data:
|
| 290 |
-
raise HTTPException(status_code=404, detail="No processed metrics found. Please POST to /api/analyze first.")
|
| 291 |
-
|
| 292 |
-
geo = cached_data["metrics"].get("geospatial", {})
|
| 293 |
-
hotspots = geo.get("hotspots", [])
|
| 294 |
-
|
| 295 |
-
return {
|
| 296 |
-
"status": "success",
|
| 297 |
-
"hotspots": hotspots,
|
| 298 |
-
"count": len(hotspots)
|
| 299 |
-
}
|
| 300 |
-
except Exception as e:
|
| 301 |
-
logger.error(f"Error retrieving hotspots: {str(e)}")
|
| 302 |
-
raise HTTPException(status_code=500, detail="Failed to retrieve hotspots")
|
| 303 |
-
|
| 304 |
-
@app.get("/api/insights")
|
| 305 |
-
async def get_insights():
|
| 306 |
-
"""
|
| 307 |
-
Retrieves high-level risk factors and recommendations.
|
| 308 |
-
|
| 309 |
-
Returns:
|
| 310 |
-
JSON response with risk insights
|
| 311 |
-
"""
|
| 312 |
-
try:
|
| 313 |
-
cached_data = await engine_cache.get("latest")
|
| 314 |
-
if not cached_data:
|
| 315 |
-
raise HTTPException(status_code=404, detail="No processed metrics found. Please POST to /api/analyze first.")
|
| 316 |
-
|
| 317 |
-
insights = cached_data["insights"]
|
| 318 |
-
return {
|
| 319 |
-
"status": "success",
|
| 320 |
-
"insights": insights
|
| 321 |
-
}
|
| 322 |
-
except Exception as e:
|
| 323 |
-
logger.error(f"Error retrieving insights: {str(e)}")
|
| 324 |
-
raise HTTPException(status_code=500, detail="Failed to retrieve insights")
|
| 325 |
-
|
| 326 |
-
@app.get("/api/report", response_class=HTMLResponse)
|
| 327 |
-
async def get_executive_report():
|
| 328 |
-
"""
|
| 329 |
-
Generates and downloads the executive HTML summary report.
|
| 330 |
-
|
| 331 |
-
Returns:
|
| 332 |
-
HTML content of the generated report
|
| 333 |
-
"""
|
| 334 |
-
try:
|
| 335 |
-
cached_data = await engine_cache.get("latest")
|
| 336 |
-
if not cached_data:
|
| 337 |
-
return HTMLResponse(content="<h1>No report generated. Please analyze a dataset first.</h1>", status_code=404)
|
| 338 |
-
|
| 339 |
-
report = MockAccidentReport(
|
| 340 |
-
metrics=cached_data["metrics"],
|
| 341 |
-
insights=cached_data["insights"]
|
| 342 |
-
)
|
| 343 |
-
html_content = report.export_to_html()
|
| 344 |
-
return HTMLResponse(content=html_content, status_code=200)
|
| 345 |
-
|
| 346 |
-
except Exception as e:
|
| 347 |
-
logger.error(f"Error generating report: {str(e)}")
|
| 348 |
-
raise HTTPException(status_code=500, detail="Failed to generate report")
|
| 349 |
-
|
| 350 |
-
@app.websocket("/ws/stream")
|
| 351 |
-
async def websocket_endpoint(websocket: WebSocket):
|
| 352 |
-
"""
|
| 353 |
-
Subscribes to live broadcasts of analytical status changes.
|
| 354 |
-
|
| 355 |
-
Returns:
|
| 356 |
-
WebSocket connection for real-time updates
|
| 357 |
-
"""
|
| 358 |
-
await manager.connect(websocket)
|
| 359 |
-
try:
|
| 360 |
-
while True:
|
| 361 |
-
# Keep connection alive, listen for client messages
|
| 362 |
-
data = await websocket.receive_text()
|
| 363 |
-
await websocket.send_text(f"Telemetry heartbeat: received {len(data)} bytes")
|
| 364 |
-
except WebSocketDisconnect:
|
| 365 |
-
logger.info("WebSocket disconnected")
|
| 366 |
-
manager.disconnect(websocket)
|
| 367 |
-
except Exception as e:
|
| 368 |
-
logger.error(f"WebSocket error: {str(e)}")
|
| 369 |
-
manager.disconnect(websocket)
|
| 370 |
-
|
| 371 |
-
@app.get("/health")
|
| 372 |
-
async def health_check():
|
| 373 |
-
"""
|
| 374 |
-
Health check endpoint for monitoring.
|
| 375 |
-
|
| 376 |
-
Returns:
|
| 377 |
-
JSON response indicating service status
|
| 378 |
-
"""
|
| 379 |
-
return JSONResponse(
|
| 380 |
-
content={
|
| 381 |
-
"status": "healthy",
|
| 382 |
-
"service": "AccidentAnalysisAPI",
|
| 383 |
-
"version": "1.0.0"
|
| 384 |
-
},
|
| 385 |
-
status_code=200
|
| 386 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/package.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "driving-repo",
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
-
"private": true,
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"dev": "vite",
|
| 8 |
-
"build": "vite build",
|
| 9 |
-
"preview": "vite preview",
|
| 10 |
-
"typecheck": "tsc --noEmit"
|
| 11 |
-
},
|
| 12 |
-
"dependencies": {
|
| 13 |
-
"@deck.gl/core": "^8.9.30",
|
| 14 |
-
"@deck.gl/geo-layers": "^8.9.30",
|
| 15 |
-
"@deck.gl/layers": "^8.9.30",
|
| 16 |
-
"@deck.gl/react": "^8.9.30",
|
| 17 |
-
"@emotion/react": "^11.13.0",
|
| 18 |
-
"@emotion/styled": "^11.13.0",
|
| 19 |
-
"@mui/material": "^5.16.0",
|
| 20 |
-
"lucide-react": "^0.453.0",
|
| 21 |
-
"react": "^18.3.0",
|
| 22 |
-
"react-dom": "^18.3.0",
|
| 23 |
-
"react-map-gl": "^7.1.6"
|
| 24 |
-
},
|
| 25 |
-
"devDependencies": {
|
| 26 |
-
"@types/node": "^22.5.0",
|
| 27 |
-
"@types/react": "^18.3.3",
|
| 28 |
-
"@types/react-dom": "^18.3.2",
|
| 29 |
-
"@vitejs/plugin-react": "^4.5.0",
|
| 30 |
-
"typescript": "^7.0.2",
|
| 31 |
-
"vite": "^5.4.0"
|
| 32 |
-
}
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/requirements-3d.txt
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
# 3D Perception Pipeline dependencies (install alongside requirements.txt)
|
| 2 |
-
-r requirements.txt
|
| 3 |
-
|
| 4 |
-
scipy>=1.11
|
| 5 |
-
filterpy>=1.4.5
|
| 6 |
-
segment-anything>=1.0
|
| 7 |
-
plotly>=5.0
|
| 8 |
-
|
| 9 |
-
# Optional: Waymo Open Dataset (heavy, install separately if needed)
|
| 10 |
-
# waymo-open-dataset
|
| 11 |
-
|
| 12 |
-
# Optional: point cloud utilities
|
| 13 |
-
# open3d>=0.18
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/requirements-prod.txt
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
fastapi==0.140.0
|
| 2 |
-
uvicorn==0.51.0
|
| 3 |
-
starlette==1.3.1
|
| 4 |
-
pydantic==2.13.4
|
| 5 |
-
pandas==3.0.5
|
| 6 |
-
polars==1.43.0
|
| 7 |
-
numpy==2.5.1
|
| 8 |
-
opencv-python==5.0.0.93
|
| 9 |
-
ultralytics==8.4.106
|
| 10 |
-
torch==2.13.0
|
| 11 |
-
torchvision==0.28.0
|
| 12 |
-
pillow==12.3.0
|
| 13 |
-
matplotlib==3.11.1
|
| 14 |
-
PyYAML==6.0.3
|
| 15 |
-
requests==2.34.2
|
| 16 |
-
httpx==0.28.1
|
| 17 |
-
nvidia-ml-py==13.610.43
|
| 18 |
-
psutil==7.2.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/requirements.txt
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
annotated-doc==0.0.4
|
| 2 |
-
annotated-types==0.8.0
|
| 3 |
-
anyio==4.14.2
|
| 4 |
-
certifi==2026.7.22
|
| 5 |
-
charset-normalizer==3.4.9
|
| 6 |
-
click==8.4.2
|
| 7 |
-
contourpy==1.3.3
|
| 8 |
-
cycler==0.12.1
|
| 9 |
-
et_xmlfile==2.0.0
|
| 10 |
-
fastapi==0.140.0
|
| 11 |
-
filelock==3.32.0
|
| 12 |
-
fonttools==4.63.0
|
| 13 |
-
fsspec==2026.6.0
|
| 14 |
-
h11==0.16.0
|
| 15 |
-
hf-xet==1.5.2
|
| 16 |
-
httpcore==1.0.9
|
| 17 |
-
httpx==0.28.1
|
| 18 |
-
huggingface_hub==1.24.0
|
| 19 |
-
idna==3.18
|
| 20 |
-
iniconfig==2.3.0
|
| 21 |
-
Jinja2==3.1.6
|
| 22 |
-
kiwisolver==1.5.0
|
| 23 |
-
MarkupSafe==3.0.3
|
| 24 |
-
matplotlib==3.11.1
|
| 25 |
-
mpmath==1.3.0
|
| 26 |
-
networkx==3.6.1
|
| 27 |
-
numpy==2.5.1
|
| 28 |
-
nvidia-ml-py==13.610.43
|
| 29 |
-
opencv-python==5.0.0.93
|
| 30 |
-
openpyxl==3.1.5
|
| 31 |
-
packaging==26.2
|
| 32 |
-
pandas==3.0.5
|
| 33 |
-
pillow==12.3.0
|
| 34 |
-
pluggy==1.6.0
|
| 35 |
-
polars==1.43.0
|
| 36 |
-
polars-runtime-32==1.43.0
|
| 37 |
-
psutil==7.2.2
|
| 38 |
-
pydantic==2.13.4
|
| 39 |
-
pydantic_core==2.46.4
|
| 40 |
-
Pygments==2.20.0
|
| 41 |
-
pyparsing==3.3.2
|
| 42 |
-
pytest==9.1.1
|
| 43 |
-
python-dateutil==2.9.0.post0
|
| 44 |
-
python-multipart==0.0.32
|
| 45 |
-
PyYAML==6.0.3
|
| 46 |
-
requests==2.34.2
|
| 47 |
-
setuptools==83.0.0
|
| 48 |
-
six==1.17.0
|
| 49 |
-
starlette==1.3.1
|
| 50 |
-
sympy==1.14.0
|
| 51 |
-
torch==2.13.0
|
| 52 |
-
torchvision==0.28.0
|
| 53 |
-
tqdm==4.69.1
|
| 54 |
-
typing-inspection==0.4.2
|
| 55 |
-
typing_extensions==4.16.0
|
| 56 |
-
ultralytics==8.4.106
|
| 57 |
-
ultralytics-thop==2.0.20
|
| 58 |
-
urllib3==2.7.0
|
| 59 |
-
uvicorn==0.51.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/sensorflow/__init__.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
"""Sensorflow Studio 3D perception pipeline package."""
|
| 2 |
-
|
| 3 |
-
from sensorflow.schemas.unified_frame import (
|
| 4 |
-
UnifiedSequence,
|
| 5 |
-
FusedFrame,
|
| 6 |
-
Object3D,
|
| 7 |
-
GroundTruthObject,
|
| 8 |
-
CameraView,
|
| 9 |
-
LidarData,
|
| 10 |
-
EgoPose,
|
| 11 |
-
Calibration,
|
| 12 |
-
)
|
| 13 |
-
from sensorflow.schemas.taxonomy_axes import TaxonomyAxes, assign_taxonomy_axes
|
| 14 |
-
|
| 15 |
-
__all__ = [
|
| 16 |
-
"UnifiedSequence",
|
| 17 |
-
"FusedFrame",
|
| 18 |
-
"Object3D",
|
| 19 |
-
"GroundTruthObject",
|
| 20 |
-
"CameraView",
|
| 21 |
-
"LidarData",
|
| 22 |
-
"EgoPose",
|
| 23 |
-
"Calibration",
|
| 24 |
-
"TaxonomyAxes",
|
| 25 |
-
"assign_taxonomy_axes",
|
| 26 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/sensorflow/adapters/__init__.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
from sensorflow.adapters.alpamayo_adapter import AlpamayoAdapter
|
| 2 |
-
from sensorflow.adapters.waymo_adapter import WaymoAdapter
|
| 3 |
-
|
| 4 |
-
__all__ = ["AlpamayoAdapter", "WaymoAdapter"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/sensorflow/adapters/alpamayo_adapter.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
"""Normalize Alpamayo / PhysicalAI samples into UnifiedSequence."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import json
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from typing import Any, Dict, List
|
| 8 |
-
|
| 9 |
-
import numpy as np
|
| 10 |
-
|
| 11 |
-
from sensorflow.adapters.base import VendorAdapter
|
| 12 |
-
from sensorflow.schemas.taxonomy_axes import assign_taxonomy_axes
|
| 13 |
-
from sensorflow.schemas.unified_frame import (
|
| 14 |
-
CameraView,
|
| 15 |
-
EgoPose,
|
| 16 |
-
FusedFrame,
|
| 17 |
-
GroundTruthObject,
|
| 18 |
-
LidarData,
|
| 19 |
-
UnifiedSequence,
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def _synthetic_lidar_path(output_dir: Path, frame_id: str, annotations: List[Dict]) -> tuple[str, int]:
|
| 24 |
-
"""Generate a minimal synthetic LiDAR point cloud for local dev."""
|
| 25 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 26 |
-
lidar_path = output_dir / f"{frame_id}_lidar.bin"
|
| 27 |
-
points = []
|
| 28 |
-
for i, ann in enumerate(annotations):
|
| 29 |
-
box = ann.get("box", [100, 100, 50, 50])
|
| 30 |
-
cx = box[0] + box[2] / 2
|
| 31 |
-
cy = box[1] + box[3] / 2
|
| 32 |
-
x = (cx - 320) * 0.05
|
| 33 |
-
y = (240 - cy) * 0.05
|
| 34 |
-
z = 0.5 + i * 0.1
|
| 35 |
-
for _ in range(50):
|
| 36 |
-
noise = np.random.randn(3) * 0.1
|
| 37 |
-
points.append([x + noise[0], y + noise[1], z + noise[2]])
|
| 38 |
-
if not points:
|
| 39 |
-
points = np.random.randn(100, 3).astype(np.float32) * 2
|
| 40 |
-
else:
|
| 41 |
-
points = np.array(points, dtype=np.float32)
|
| 42 |
-
points.tofile(lidar_path)
|
| 43 |
-
return str(lidar_path), len(points)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def _box2d_to_bbox3d(box: List[float], conf: float) -> List[float]:
|
| 47 |
-
"""Convert 2D box [x,y,w,h] to stub 3D bbox [x,y,z,l,w,h,yaw]."""
|
| 48 |
-
cx = box[0] + box[2] / 2
|
| 49 |
-
cy = box[1] + box[3] / 2
|
| 50 |
-
x = (cx - 320) * 0.05
|
| 51 |
-
y = (240 - cy) * 0.05
|
| 52 |
-
l = max(box[2] * 0.05, 1.0)
|
| 53 |
-
w = max(box[3] * 0.05, 0.5)
|
| 54 |
-
return [x, y, 0.5, l, w, 1.5, 0.0]
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
DEFAULT_ALPAMAYO_SAMPLES = {
|
| 58 |
-
"physical_ai": {
|
| 59 |
-
"dataset": "nvidia/PhysicalAI-Autonomous-Vehicles",
|
| 60 |
-
"frame_index": 4209,
|
| 61 |
-
"views": {
|
| 62 |
-
"front": "https://images.unsplash.com/photo-1506015391300-4802dc74de2e?w=800",
|
| 63 |
-
"left": "https://images.unsplash.com/photo-1519074002996-a69e7ac46a42?w=800",
|
| 64 |
-
"right": "https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=800",
|
| 65 |
-
},
|
| 66 |
-
"telemetry": {"lat": 37.774929, "lon": -122.419416, "speed_kmh": 35.8},
|
| 67 |
-
"annotations": [
|
| 68 |
-
{"id": 1, "label": "pedestrian", "box": [120, 240, 60, 120], "conf": 0.89},
|
| 69 |
-
{"id": 2, "label": "car", "box": [340, 220, 150, 100], "conf": 0.95},
|
| 70 |
-
{"id": 3, "label": "traffic_light", "box": [500, 80, 30, 60], "conf": 0.92},
|
| 71 |
-
],
|
| 72 |
-
},
|
| 73 |
-
"nurec": {
|
| 74 |
-
"dataset": "nvidia/PhysicalAI-Autonomous-Vehicles-NuRec",
|
| 75 |
-
"frame_index": 781,
|
| 76 |
-
"views": {
|
| 77 |
-
"front": "https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=800",
|
| 78 |
-
},
|
| 79 |
-
"telemetry": {"lat": 37.783312, "lon": -122.416733, "speed_kmh": 48.2},
|
| 80 |
-
"annotations": [
|
| 81 |
-
{"id": 1, "label": "car", "box": [300, 180, 180, 140], "conf": 0.94},
|
| 82 |
-
{"id": 2, "label": "truck", "box": [50, 150, 220, 180], "conf": 0.81},
|
| 83 |
-
],
|
| 84 |
-
},
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
class AlpamayoAdapter(VendorAdapter):
|
| 89 |
-
def load(self, source: Dict[str, Any], sequence_id: str) -> UnifiedSequence:
|
| 90 |
-
data = source if source else DEFAULT_ALPAMAYO_SAMPLES["physical_ai"]
|
| 91 |
-
output_dir = Path("runs/pipeline") / sequence_id / "lidar"
|
| 92 |
-
speed = data.get("telemetry", {}).get("speed_kmh", 35.0)
|
| 93 |
-
annotations = data.get("annotations", [])
|
| 94 |
-
|
| 95 |
-
frames: List[FusedFrame] = []
|
| 96 |
-
num_frames = max(3, len(annotations))
|
| 97 |
-
for i in range(num_frames):
|
| 98 |
-
frame_id = f"frame_{i:04d}"
|
| 99 |
-
lidar_path, num_points = _synthetic_lidar_path(output_dir, frame_id, annotations)
|
| 100 |
-
cameras = {
|
| 101 |
-
name: CameraView(image_path=url)
|
| 102 |
-
for name, url in data.get("views", {}).items()
|
| 103 |
-
}
|
| 104 |
-
gt = []
|
| 105 |
-
for ann in annotations:
|
| 106 |
-
axes = assign_taxonomy_axes(ann["label"], speed_kmh=speed)
|
| 107 |
-
gt.append(GroundTruthObject(
|
| 108 |
-
instance_id=f"gt_{ann['id']}",
|
| 109 |
-
class_name=ann["label"],
|
| 110 |
-
confidence=ann.get("conf", 1.0),
|
| 111 |
-
bbox_3d=_box2d_to_bbox3d(ann["box"], ann.get("conf", 1.0)),
|
| 112 |
-
taxonomy_axes=axes,
|
| 113 |
-
))
|
| 114 |
-
frames.append(FusedFrame(
|
| 115 |
-
frame_id=frame_id,
|
| 116 |
-
timestamp_us=i * 100_000,
|
| 117 |
-
lidar=LidarData(path=lidar_path, num_points=num_points),
|
| 118 |
-
cameras=cameras,
|
| 119 |
-
ego_pose=EgoPose(speed_kmh=speed, x=i * 1.1, y=i * 5.2),
|
| 120 |
-
ground_truth=gt,
|
| 121 |
-
))
|
| 122 |
-
|
| 123 |
-
return UnifiedSequence(
|
| 124 |
-
sequence_id=sequence_id,
|
| 125 |
-
vendor="alpamayo",
|
| 126 |
-
frames=frames,
|
| 127 |
-
taxonomy_manifest={"source_dataset": data.get("dataset", "alpamayo")},
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
@classmethod
|
| 131 |
-
def load_from_file(cls, path: Path, sequence_id: str) -> UnifiedSequence:
|
| 132 |
-
with open(path) as f:
|
| 133 |
-
return cls().load(json.load(f), sequence_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/sensorflow/adapters/base.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
"""Adapter protocol for vendor-specific data normalization."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from abc import ABC, abstractmethod
|
| 6 |
-
from typing import Any, Dict
|
| 7 |
-
|
| 8 |
-
from sensorflow.schemas.unified_frame import UnifiedSequence
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class VendorAdapter(ABC):
|
| 12 |
-
@abstractmethod
|
| 13 |
-
def load(self, source: Dict[str, Any], sequence_id: str) -> UnifiedSequence:
|
| 14 |
-
"""Convert vendor-specific data into a UnifiedSequence."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf-space/sensorflow/bevfusion/__init__.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
"""BEV-Fusion perception engine (inspired by BEVFusion) with masklet-style
|
| 2 |
-
temporal propagation (inspired by SAM 3).
|
| 3 |
-
|
| 4 |
-
What this package does
|
| 5 |
-
----------------------
|
| 6 |
-
Fuses camera and LiDAR evidence in a unified bird's-eye-view (BEV) plane to
|
| 7 |
-
produce better auto-labels than a single-sensor (camera-primary) labeler, and
|
| 8 |
-
demonstrates the improvement with the platform's own evaluation machinery on
|
| 9 |
-
synthetic scenes with known ground truth.
|
| 10 |
-
|
| 11 |
-
Concept mapping (paper -> this implementation)
|
| 12 |
-
----------------------------------------------
|
| 13 |
-
BEVFusion:
|
| 14 |
-
- "Lift camera features to BEV via calibrated geometry"
|
| 15 |
-
-> :mod:`geometry`: pinhole projection with a 6-DoF camera-to-ego
|
| 16 |
-
extrinsic; monocular detections are back-projected along the viewing
|
| 17 |
-
ray into the BEV plane with an anisotropic (along-ray-elongated)
|
| 18 |
-
position covariance.
|
| 19 |
-
- "Rasterize LiDAR features into a BEV grid"
|
| 20 |
-
-> :mod:`fusion`: LiDAR detections are splatted into a metric BEV
|
| 21 |
-
occupancy/feature grid with tight isotropic covariance.
|
| 22 |
-
- "Fuse the modality feature maps in the shared BEV space"
|
| 23 |
-
-> :mod:`fusion.fuse_maps`: per-cell fusion of existence evidence
|
| 24 |
-
(noisy-OR), class evidence (reliability-weighted histograms; camera
|
| 25 |
-
weighted higher), and geometry evidence (inverse-variance weighted
|
| 26 |
-
accumulators; LiDAR dominates position where present).
|
| 27 |
-
- "Decode fused BEV features into 3D boxes"
|
| 28 |
-
-> :mod:`fusion.decode_bev`: peak extraction + ray-aware NMS +
|
| 29 |
-
continuous weighted decoding of center/dims/yaw/class.
|
| 30 |
-
|
| 31 |
-
SAM 3 masklets:
|
| 32 |
-
- "Propagate each object's identity through time, surviving brief
|
| 33 |
-
disappearances without re-identification"
|
| 34 |
-
-> :mod:`masklet.BEVMaskletTracker`: association in the stable BEV
|
| 35 |
-
plane (Hungarian on BEV distance + class + size, extending
|
| 36 |
-
:mod:`sensorflow.temporal_tracker`), with Kalman-predicted BEV
|
| 37 |
-
position carrying identity across short dropouts and emitting
|
| 38 |
-
propagated ("masklet") boxes during occlusion gaps.
|
| 39 |
-
|
| 40 |
-
What is simulated vs. real
|
| 41 |
-
--------------------------
|
| 42 |
-
There are NO learned features and NO neural networks here (the platform is
|
| 43 |
-
numpy-only by design). Camera/LiDAR "detections" are sampled from ground truth
|
| 44 |
-
with realistic, modality-specific failure modes (camera: occlusion/night
|
| 45 |
-
misses and depth ambiguity but good class discrimination; LiDAR: accurate
|
| 46 |
-
geometry, degraded at long range and in rain, weak class discrimination).
|
| 47 |
-
The fusion itself is honest geometric + probabilistic math, and the measured
|
| 48 |
-
improvement arises from real sensor complementarity in that math: LiDAR
|
| 49 |
-
existence evidence recovers camera misses, camera class evidence flows along
|
| 50 |
-
the viewing ray to LiDAR-anchored cells, and inverse-variance weighting gives
|
| 51 |
-
fused boxes near-LiDAR position accuracy. Nothing in the comparison report is
|
| 52 |
-
hardcoded; every number is computed at runtime from the fused outputs.
|
| 53 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|