whassan commited on
Commit
f1c3ff2
·
verified ·
1 Parent(s): 4b5fa98

Deploy Sensorflow Studio from fix/legacy-help-chatbot (f7ddaf4)

Browse files

Push 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
Files changed (50) hide show
  1. README.md +6 -0
  2. app_backend.py +922 -142
  3. docs/execution-integrity-audit.md +99 -0
  4. hf-space/.dockerignore +0 -12
  5. hf-space/.gitattributes +0 -35
  6. hf-space/.gitignore +0 -72
  7. hf-space/ANTIGRAVITY_PROMPT.md +0 -192
  8. hf-space/ARCHITECTURE.md +0 -78
  9. hf-space/CRASH_DATA_GUIDE.md +0 -378
  10. hf-space/DASHBOARD_README.md +0 -393
  11. hf-space/DATA_EXTRACTION_QUICK_REF.md +0 -348
  12. hf-space/Dockerfile +0 -51
  13. hf-space/FILES_CREATED.txt +0 -135
  14. hf-space/LICENSE +0 -17
  15. hf-space/README.md +0 -31
  16. hf-space/TOOLKIT_SUMMARY.md +0 -440
  17. hf-space/accident_analysis.py +0 -99
  18. hf-space/accident_cleaner.py +0 -62
  19. hf-space/accident_importer.py +0 -103
  20. hf-space/accident_insights.py +0 -54
  21. hf-space/accident_report.py +0 -81
  22. hf-space/accident_validator.py +0 -83
  23. hf-space/app_backend.py +0 -1481
  24. hf-space/autograder.py +0 -128
  25. hf-space/coco8.yaml +0 -21
  26. hf-space/comparative_analytics.py +0 -209
  27. hf-space/dashboard_viz.py +0 -468
  28. hf-space/data/test.jpg +0 -0
  29. hf-space/deploy/CHECKLIST.md +0 -140
  30. hf-space/deploy/DEPLOYMENT.md +0 -201
  31. hf-space/deploy/accident-analysis.service +0 -17
  32. hf-space/deploy/download-models.sh +0 -36
  33. hf-space/deploy/huggingface/README.md +0 -31
  34. hf-space/deploy/huggingface/deploy.py +0 -133
  35. hf-space/deploy/huggingface/requirements-space.txt +0 -15
  36. hf-space/deploy/nginx.conf +0 -69
  37. hf-space/deploy/sensorflow-studio.service +0 -17
  38. hf-space/deploy/setup-dgx.sh +0 -87
  39. hf-space/index.html +0 -10
  40. hf-space/infer.py +0 -111
  41. hf-space/main.py +0 -386
  42. hf-space/package.json +0 -33
  43. hf-space/requirements-3d.txt +0 -13
  44. hf-space/requirements-prod.txt +0 -18
  45. hf-space/requirements.txt +0 -59
  46. hf-space/sensorflow/__init__.py +0 -26
  47. hf-space/sensorflow/adapters/__init__.py +0 -4
  48. hf-space/sensorflow/adapters/alpamayo_adapter.py +0 -133
  49. hf-space/sensorflow/adapters/base.py +0 -14
  50. 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
- # 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",
@@ -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 != '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:
@@ -196,23 +445,22 @@ def get_train_status():
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:
@@ -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
- 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):
@@ -283,76 +714,146 @@ def serve_image(filename: str):
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):
@@ -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
- 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")
@@ -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(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")
@@ -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
- 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,
@@ -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
- 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")
@@ -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": (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
@@ -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
- """