meaculpitt commited on
Commit
1da02b5
Β·
verified Β·
1 Parent(s): 52a75e1

scorevision: push artifact

Browse files
Files changed (2) hide show
  1. chute_config.yml +8 -2
  2. miner.py +39 -53
chute_config.yml CHANGED
@@ -7,10 +7,16 @@ Image:
7
  NodeSelector:
8
  gpu_count: 1
9
  min_vram_gb_per_gpu: 16
 
 
 
 
 
 
10
 
11
  Chute:
12
- timeout_seconds: 300
13
  concurrency: 4
14
  max_instances: 5
15
  scaling_threshold: 0.5
16
- shutdown_after_seconds: 600
 
7
  NodeSelector:
8
  gpu_count: 1
9
  min_vram_gb_per_gpu: 16
10
+ max_hourly_price_per_gpu: 0.50
11
+ exclude:
12
+ - 5090
13
+ - b200
14
+ - h200
15
+ - mi300x
16
 
17
  Chute:
18
+ timeout_seconds: 900
19
  concurrency: 4
20
  max_instances: 5
21
  scaling_threshold: 0.5
22
+ shutdown_after_seconds: 288000
miner.py CHANGED
@@ -1,7 +1,8 @@
1
  """
2
- Score Vision SN44 β€” Unified miner v2 (2026-03-28).
3
- Dual-model: vehicle (YOLO11s) + person (YOLO11s).
4
- Optimized for latency: parallel threading + configurable TTA.
 
5
 
6
  Vehicle model (vehicle_weights.onnx):
7
  Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
@@ -36,10 +37,8 @@ def _preload_cuda_libs():
36
  pass
37
  if not lib_dirs:
38
  return
39
- # Set LD_LIBRARY_PATH for subprocesses
40
  existing = os.environ.get('LD_LIBRARY_PATH', '')
41
  os.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs + ([existing] if existing else []))
42
- # Pre-load .so files with RTLD_GLOBAL so dlopen() finds them
43
  for lib_dir in lib_dirs:
44
  for so in sorted(_glob.glob(os.path.join(lib_dir, 'lib*.so*'))):
45
  try:
@@ -60,7 +59,6 @@ def _try_gpu_ort():
60
  import time as _t
61
  _t0 = _t.time()
62
 
63
- # Diagnostic: torch + CUDA
64
  try:
65
  import torch
66
  print(f"[GPU_SETUP] torch={torch.__version__} cuda={torch.cuda.is_available()} "
@@ -70,7 +68,6 @@ def _try_gpu_ort():
70
  except Exception as e:
71
  print(f"[GPU_SETUP] torch check failed: {e}", flush=True)
72
 
73
- # Check current ORT providers
74
  try:
75
  import onnxruntime as _ort
76
  providers = _ort.get_available_providers()
@@ -82,7 +79,6 @@ def _try_gpu_ort():
82
  print("[GPU_SETUP] onnxruntime not importable", flush=True)
83
  return
84
 
85
- # List key packages for diagnostics
86
  result = _subprocess.run(
87
  [_sys.executable, '-m', 'pip', 'list', '--format=columns'],
88
  capture_output=True, text=True, timeout=15
@@ -94,14 +90,12 @@ def _try_gpu_ort():
94
 
95
  print("[GPU_SETUP] Attempting onnxruntime-gpu install...", flush=True)
96
  try:
97
- # Uninstall CPU onnxruntime
98
  r1 = _subprocess.run(
99
  [_sys.executable, '-m', 'pip', 'uninstall', 'onnxruntime', '-y'],
100
  capture_output=True, text=True, timeout=30
101
  )
102
  print(f"[GPU_SETUP] uninstall rc={r1.returncode}", flush=True)
103
 
104
- # Install GPU version (no-deps to avoid reinstalling torch etc)
105
  r2 = _subprocess.run(
106
  [_sys.executable, '-m', 'pip', 'install', 'onnxruntime-gpu', '--no-deps'],
107
  capture_output=True, text=True, timeout=180
@@ -122,12 +116,10 @@ def _try_gpu_ort():
122
  )
123
  return
124
 
125
- # Clear cached onnxruntime modules
126
  for key in list(_sys.modules.keys()):
127
  if 'onnxruntime' in key:
128
  del _sys.modules[key]
129
 
130
- # Reimport and check
131
  import onnxruntime as _ort2
132
  new_providers = _ort2.get_available_providers()
133
  _dt = _t.time() - _t0
@@ -135,7 +127,6 @@ def _try_gpu_ort():
135
 
136
  except Exception as e:
137
  print(f"[GPU_SETUP] EXCEPTION: {e}", flush=True)
138
- # Try to restore CPU onnxruntime
139
  try:
140
  for key in list(_sys.modules.keys()):
141
  if 'onnxruntime' in key:
@@ -172,31 +163,33 @@ logger = logging.getLogger(__name__)
172
  # ── Vehicle config ──────────────────────────────────────────────────────────
173
  VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
174
  VEH_NUM_CLASSES = 4
175
- # VEH_IMG_SIZE: now read dynamically from model input shape in __init__
176
  VEH_CONF_PER_CLASS = {0: 0.15, 1: 0.30, 2: 0.20, 3: 0.15}
177
  VEH_CONF_DEFAULT = 0.35
178
  VEH_TTA_CONF = 0.10
179
  VEH_WBF_IOU = 0.40
180
 
181
  # ── Person config (TTA consensus) ───────────────────────────────────────────
182
- PER_CONF_LOW = 0.32 # Detection threshold (both passes)
183
- PER_CONF_HIGH = 0.55 # Above this = unconditionally kept
184
- PER_CONSENSUS_IOU = 0.50 # Cross-view IoU to confirm low-conf box
185
- PER_WBF_IOU = 0.40 # Final WBF dedup (unchanged)
186
- PER_RTF_BUDGET = 8.0 # Conservative RTF budget (gate is 10s)
187
-
188
- # ── Person box sanity filters (matching top scorers) ────────────────────────
189
- PER_MIN_WH = 8 # Minimum box width or height (pixels)
190
- PER_MIN_AREA = 14 * 14 # Minimum box area (pixels^2)
191
- PER_MAX_ASPECT = 8.0 # Maximum aspect ratio
192
- PER_MAX_AREA_RATIO = 0.80 # Maximum box area / image area
193
 
194
  # ── Shared ──────────────���───────────────────────────────────────────────────
195
  WBF_SKIP_THR = 0.0001
196
 
197
  # ── Speed config ────────────────────────────────────────────────────────────
198
- ENABLE_TTA = False # Set True to re-enable 2-pass TTA (doubles inference time)
199
- ENABLE_PARALLEL = True # Run vehicle + person in parallel threads
 
 
 
200
 
201
 
202
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
@@ -335,9 +328,22 @@ class Miner:
335
  def __init__(self, path_hf_repo: Path) -> None:
336
  self.path_hf_repo = path_hf_repo
337
 
338
- # Vehicle model (YOLO11s, 4 classes)
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  self.veh_session = ort.InferenceSession(
340
- str(path_hf_repo / "vehicle_weights.onnx"),
341
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
342
  )
343
  self.veh_input_name = self.veh_session.get_inputs()[0].name
@@ -345,7 +351,7 @@ class Miner:
345
  self.veh_h = int(veh_shape[2])
346
  self.veh_w = int(veh_shape[3])
347
 
348
- # Person model (YOLO11s, 1 class)
349
  self.per_session = ort.InferenceSession(
350
  str(path_hf_repo / "person_weights.onnx"),
351
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
@@ -366,7 +372,7 @@ class Miner:
366
  logger.info(f"TTA={ENABLE_TTA} PARALLEL={ENABLE_PARALLEL}")
367
 
368
  def __repr__(self) -> str:
369
- return "Unified Miner v2 β€” dual-model vehicle+person (parallel, TTA-configurable)"
370
 
371
  # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
372
 
@@ -431,23 +437,19 @@ class Miner:
431
  all_l.append(out_cls)
432
 
433
  if ENABLE_TTA:
434
- # Pass 1: original
435
  _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
436
- # Pass 2: hflip
437
  flipped = cv2.flip(image_bgr, 1)
438
  bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
439
  if len(bx):
440
  bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
441
  _collect(bx, sc, cl)
442
  else:
443
- # Single pass β€” use per-class conf thresholds directly
444
  bx, confs, cls_ids = self._veh_run_pass(image_bgr, VEH_TTA_CONF)
445
  _collect(bx, confs, cls_ids)
446
 
447
  if not all_b:
448
  return []
449
 
450
- # Always apply WBF β€” deduplicates overlapping anchors from raw ONNX output
451
  fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
452
 
453
  if len(fb) == 0:
@@ -517,12 +519,10 @@ class Miner:
517
  def _infer_person(self, image_bgr):
518
  oh, ow = image_bgr.shape[:2]
519
 
520
- # Pass 1: original
521
  t_start = time.monotonic()
522
  boxes_orig, confs_orig = self._per_run_pass(image_bgr, PER_CONF_LOW)
523
  elapsed = time.monotonic() - t_start
524
 
525
- # Pass 2: hflip β€” only if RTF budget allows
526
  has_flip = False
527
  if elapsed < PER_RTF_BUDGET / 2:
528
  flipped = cv2.flip(image_bgr, 1)
@@ -533,21 +533,16 @@ class Miner:
533
  else:
534
  logger.warning(f"[person TTA] skipping flip β€” pass1 took {elapsed*1000:.0f}ms")
535
 
536
- # ── Consensus filtering (v2 β€” original-primary, flip supplements) ──
537
  kept_b, kept_s = [], []
538
 
539
- # Always keep ALL original boxes at conf >= PER_CONF_LOW
540
  for i in range(len(boxes_orig)):
541
  kept_b.append(boxes_orig[i])
542
  kept_s.append(confs_orig[i])
543
 
544
  if has_flip:
545
- # From flip view: only add HIGH-conf boxes that original MISSED
546
- # (IoU < 0.3 with all original boxes = truly new detection)
547
  for i in range(len(boxes_flip)):
548
  if confs_flip[i] < PER_CONF_HIGH:
549
  continue
550
- # Check if this flip box overlaps any original box
551
  if len(boxes_orig) > 0:
552
  xx1 = np.maximum(boxes_flip[i, 0], boxes_orig[:, 0])
553
  yy1 = np.maximum(boxes_flip[i, 1], boxes_orig[:, 1])
@@ -558,15 +553,13 @@ class Miner:
558
  a2 = (boxes_orig[:, 2] - boxes_orig[:, 0]) * (boxes_orig[:, 3] - boxes_orig[:, 1])
559
  iou = inter / (a1 + a2 - inter + 1e-9)
560
  if np.max(iou) >= 0.3:
561
- continue # Already detected by original β€” skip
562
- # Truly new high-conf detection from flip
563
  kept_b.append(boxes_flip[i])
564
  kept_s.append(confs_flip[i])
565
 
566
  if not kept_b:
567
  return []
568
 
569
- # ── WBF dedup (merges near-duplicate detections) ────────────────
570
  kept_b = np.array(kept_b)
571
  kept_s = np.array(kept_s)
572
  norm = kept_b.copy()
@@ -582,7 +575,6 @@ class Miner:
582
  fb[:, [0, 2]] *= ow
583
  fb[:, [1, 3]] *= oh
584
 
585
- # ── Sanity filter (remove degenerate boxes) ─────────────────────
586
  img_area = float(oh * ow)
587
  sane = []
588
  for i in range(len(fb)):
@@ -617,12 +609,10 @@ class Miner:
617
  ))
618
  return out
619
 
620
-
621
  # ── Unified inference ───────────────────────────────────────────────────
622
 
623
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
624
  if ENABLE_PARALLEL:
625
- # Run both models in parallel threads
626
  veh_future = self._executor.submit(self._infer_vehicle, image_bgr)
627
  per_future = self._executor.submit(self._infer_person, image_bgr)
628
  vehicle_boxes = veh_future.result()
@@ -637,7 +627,6 @@ class Miner:
637
  REPLAY_MAX = 100
638
 
639
  def _replay_save(self, batch_images, results):
640
- """Save validator query images + our predictions to replay buffer (background)."""
641
  try:
642
  ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
643
  query_dir = self.REPLAY_DIR / ts
@@ -665,7 +654,6 @@ class Miner:
665
  pass
666
 
667
  def _replay_prune(self):
668
- """Keep only the most recent REPLAY_MAX queries."""
669
  try:
670
  dirs = sorted(
671
  [d for d in self.REPLAY_DIR.iterdir() if d.is_dir()],
@@ -702,7 +690,6 @@ class Miner:
702
  logger.info(f"[miner] predict_batch: {len(batch_images)} images, "
703
  f"{sum(len(r.boxes) for r in results)} total boxes, {dt_total:.0f}ms")
704
 
705
- # Save to replay buffer (background thread)
706
  threading.Thread(
707
  target=self._replay_save,
708
  args=(batch_images, results),
@@ -710,5 +697,4 @@ class Miner:
710
  ).start()
711
 
712
  return results
713
- # GPU runtime installer v1 β€” 20260328_184247
714
- # redeploy 20260330_012200_wbf_fix
 
1
  """
2
+ Score Vision SN44 β€” Unified miner v3 (2026-03-31).
3
+ Dual-model: vehicle (YOLO11m INT8 1280) + person (YOLO11s INT8 640).
4
+ Vehicle weights loaded from secondary HF repo (meaculpitt/ScoreVision-Vehicle).
5
+ Person weights loaded from primary HF repo (template downloads automatically).
6
 
7
  Vehicle model (vehicle_weights.onnx):
8
  Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
 
37
  pass
38
  if not lib_dirs:
39
  return
 
40
  existing = os.environ.get('LD_LIBRARY_PATH', '')
41
  os.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs + ([existing] if existing else []))
 
42
  for lib_dir in lib_dirs:
43
  for so in sorted(_glob.glob(os.path.join(lib_dir, 'lib*.so*'))):
44
  try:
 
59
  import time as _t
60
  _t0 = _t.time()
61
 
 
62
  try:
63
  import torch
64
  print(f"[GPU_SETUP] torch={torch.__version__} cuda={torch.cuda.is_available()} "
 
68
  except Exception as e:
69
  print(f"[GPU_SETUP] torch check failed: {e}", flush=True)
70
 
 
71
  try:
72
  import onnxruntime as _ort
73
  providers = _ort.get_available_providers()
 
79
  print("[GPU_SETUP] onnxruntime not importable", flush=True)
80
  return
81
 
 
82
  result = _subprocess.run(
83
  [_sys.executable, '-m', 'pip', 'list', '--format=columns'],
84
  capture_output=True, text=True, timeout=15
 
90
 
91
  print("[GPU_SETUP] Attempting onnxruntime-gpu install...", flush=True)
92
  try:
 
93
  r1 = _subprocess.run(
94
  [_sys.executable, '-m', 'pip', 'uninstall', 'onnxruntime', '-y'],
95
  capture_output=True, text=True, timeout=30
96
  )
97
  print(f"[GPU_SETUP] uninstall rc={r1.returncode}", flush=True)
98
 
 
99
  r2 = _subprocess.run(
100
  [_sys.executable, '-m', 'pip', 'install', 'onnxruntime-gpu', '--no-deps'],
101
  capture_output=True, text=True, timeout=180
 
116
  )
117
  return
118
 
 
119
  for key in list(_sys.modules.keys()):
120
  if 'onnxruntime' in key:
121
  del _sys.modules[key]
122
 
 
123
  import onnxruntime as _ort2
124
  new_providers = _ort2.get_available_providers()
125
  _dt = _t.time() - _t0
 
127
 
128
  except Exception as e:
129
  print(f"[GPU_SETUP] EXCEPTION: {e}", flush=True)
 
130
  try:
131
  for key in list(_sys.modules.keys()):
132
  if 'onnxruntime' in key:
 
163
  # ── Vehicle config ──────────────────────────────────────────────────────────
164
  VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
165
  VEH_NUM_CLASSES = 4
 
166
  VEH_CONF_PER_CLASS = {0: 0.15, 1: 0.30, 2: 0.20, 3: 0.15}
167
  VEH_CONF_DEFAULT = 0.35
168
  VEH_TTA_CONF = 0.10
169
  VEH_WBF_IOU = 0.40
170
 
171
  # ── Person config (TTA consensus) ───────────────────────────────────────────
172
+ PER_CONF_LOW = 0.32
173
+ PER_CONF_HIGH = 0.55
174
+ PER_CONSENSUS_IOU = 0.50
175
+ PER_WBF_IOU = 0.40
176
+ PER_RTF_BUDGET = 8.0
177
+
178
+ # ── Person box sanity filters ──────────────────────────────────────────────
179
+ PER_MIN_WH = 8
180
+ PER_MIN_AREA = 14 * 14
181
+ PER_MAX_ASPECT = 8.0
182
+ PER_MAX_AREA_RATIO = 0.80
183
 
184
  # ── Shared ──────────────���───────────────────────────────────────────────────
185
  WBF_SKIP_THR = 0.0001
186
 
187
  # ── Speed config ────────────────────────────────────────────────────────────
188
+ ENABLE_TTA = False
189
+ ENABLE_PARALLEL = True
190
+
191
+ # ── Secondary HF repo for vehicle weights ───────────────────────────────────
192
+ VEHICLE_HF_REPO = "meaculpitt/ScoreVision-Vehicle"
193
 
194
 
195
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
 
328
  def __init__(self, path_hf_repo: Path) -> None:
329
  self.path_hf_repo = path_hf_repo
330
 
331
+ # Vehicle model β€” download from secondary HF repo with safety guard
332
+ t0 = time.monotonic()
333
+ try:
334
+ from huggingface_hub import snapshot_download as _sd
335
+ veh_path = Path(_sd(VEHICLE_HF_REPO))
336
+ veh_weights = str(veh_path / "vehicle_weights.onnx")
337
+ logger.info(f"[init] Vehicle weights from {VEHICLE_HF_REPO} in {time.monotonic()-t0:.1f}s")
338
+ except Exception as e:
339
+ # Fallback: try loading from primary repo (backward compat)
340
+ logger.warning(f"[init] Vehicle secondary repo failed ({e}), trying primary repo")
341
+ veh_weights = str(path_hf_repo / "vehicle_weights.onnx")
342
+ if not Path(veh_weights).exists():
343
+ raise FileNotFoundError(f"vehicle_weights.onnx not found in primary or secondary repo") from e
344
+
345
  self.veh_session = ort.InferenceSession(
346
+ veh_weights,
347
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
348
  )
349
  self.veh_input_name = self.veh_session.get_inputs()[0].name
 
351
  self.veh_h = int(veh_shape[2])
352
  self.veh_w = int(veh_shape[3])
353
 
354
+ # Person model β€” from primary HF repo (template downloads automatically)
355
  self.per_session = ort.InferenceSession(
356
  str(path_hf_repo / "person_weights.onnx"),
357
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
 
372
  logger.info(f"TTA={ENABLE_TTA} PARALLEL={ENABLE_PARALLEL}")
373
 
374
  def __repr__(self) -> str:
375
+ return "Unified Miner v3 β€” dual-model vehicle+person (repo-split, parallel, TTA-configurable)"
376
 
377
  # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
378
 
 
437
  all_l.append(out_cls)
438
 
439
  if ENABLE_TTA:
 
440
  _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
 
441
  flipped = cv2.flip(image_bgr, 1)
442
  bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
443
  if len(bx):
444
  bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
445
  _collect(bx, sc, cl)
446
  else:
 
447
  bx, confs, cls_ids = self._veh_run_pass(image_bgr, VEH_TTA_CONF)
448
  _collect(bx, confs, cls_ids)
449
 
450
  if not all_b:
451
  return []
452
 
 
453
  fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
454
 
455
  if len(fb) == 0:
 
519
  def _infer_person(self, image_bgr):
520
  oh, ow = image_bgr.shape[:2]
521
 
 
522
  t_start = time.monotonic()
523
  boxes_orig, confs_orig = self._per_run_pass(image_bgr, PER_CONF_LOW)
524
  elapsed = time.monotonic() - t_start
525
 
 
526
  has_flip = False
527
  if elapsed < PER_RTF_BUDGET / 2:
528
  flipped = cv2.flip(image_bgr, 1)
 
533
  else:
534
  logger.warning(f"[person TTA] skipping flip β€” pass1 took {elapsed*1000:.0f}ms")
535
 
 
536
  kept_b, kept_s = [], []
537
 
 
538
  for i in range(len(boxes_orig)):
539
  kept_b.append(boxes_orig[i])
540
  kept_s.append(confs_orig[i])
541
 
542
  if has_flip:
 
 
543
  for i in range(len(boxes_flip)):
544
  if confs_flip[i] < PER_CONF_HIGH:
545
  continue
 
546
  if len(boxes_orig) > 0:
547
  xx1 = np.maximum(boxes_flip[i, 0], boxes_orig[:, 0])
548
  yy1 = np.maximum(boxes_flip[i, 1], boxes_orig[:, 1])
 
553
  a2 = (boxes_orig[:, 2] - boxes_orig[:, 0]) * (boxes_orig[:, 3] - boxes_orig[:, 1])
554
  iou = inter / (a1 + a2 - inter + 1e-9)
555
  if np.max(iou) >= 0.3:
556
+ continue
 
557
  kept_b.append(boxes_flip[i])
558
  kept_s.append(confs_flip[i])
559
 
560
  if not kept_b:
561
  return []
562
 
 
563
  kept_b = np.array(kept_b)
564
  kept_s = np.array(kept_s)
565
  norm = kept_b.copy()
 
575
  fb[:, [0, 2]] *= ow
576
  fb[:, [1, 3]] *= oh
577
 
 
578
  img_area = float(oh * ow)
579
  sane = []
580
  for i in range(len(fb)):
 
609
  ))
610
  return out
611
 
 
612
  # ── Unified inference ───────────────────────────────────────────────────
613
 
614
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
615
  if ENABLE_PARALLEL:
 
616
  veh_future = self._executor.submit(self._infer_vehicle, image_bgr)
617
  per_future = self._executor.submit(self._infer_person, image_bgr)
618
  vehicle_boxes = veh_future.result()
 
627
  REPLAY_MAX = 100
628
 
629
  def _replay_save(self, batch_images, results):
 
630
  try:
631
  ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
632
  query_dir = self.REPLAY_DIR / ts
 
654
  pass
655
 
656
  def _replay_prune(self):
 
657
  try:
658
  dirs = sorted(
659
  [d for d in self.REPLAY_DIR.iterdir() if d.is_dir()],
 
690
  logger.info(f"[miner] predict_batch: {len(batch_images)} images, "
691
  f"{sum(len(r.boxes) for r in results)} total boxes, {dt_total:.0f}ms")
692
 
 
693
  threading.Thread(
694
  target=self._replay_save,
695
  args=(batch_images, results),
 
697
  ).start()
698
 
699
  return results
700
+ # Miner v3 β€” repo-split deploy 20260331