From deac984180e54dcb904f415c8f2e095b8b1661a7 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 25 Aug 2026 14:56:08 +0800
Subject: [PATCH] feat(console): auto-select cuda for detection workflows

---
 scripts/serve_workbench_console.py |   90 ++++++++++++++++++++++++++++++++++++--------
 1 files changed, 73 insertions(+), 17 deletions(-)

diff --git a/scripts/serve_workbench_console.py b/scripts/serve_workbench_console.py
index b1d12ca..6a97f99 100644
--- a/scripts/serve_workbench_console.py
+++ b/scripts/serve_workbench_console.py
@@ -78,6 +78,10 @@
 POINTCLOUD_CLASS_CODES = {1, 2, 5, 6, 15, 16}
 POINTCLOUD_CPU_ENVIRONMENT = "05-3d-pointcloud"
 POINTCLOUD_GPU_ENVIRONMENT = "05-3d-pointcloud-gpu"
+OBJECT_DETECTION_CPU_ENVIRONMENT = "01-object-detection"
+OBJECT_DETECTION_GPU_ENVIRONMENT = "01-object-detection-cuda"
+CHANGE_DETECTION_CPU_ENVIRONMENT = "00-change-detection"
+CHANGE_DETECTION_GPU_ENVIRONMENT = "00-change-detection-cuda"
 
 
 class ApiError(ValueError):
@@ -165,6 +169,54 @@
     return {"python": str(cpu_python), "device": "cpu", "environment": POINTCLOUD_CPU_ENVIRONMENT, "torchVersion": "unknown"}
 
 
+def object_detection_execution_environment(root: Path) -> dict[str, str]:
+    """Choose the fixed object-detection CUDA environment only after probing it."""
+    cpu_python = root / ".venvs" / OBJECT_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe"
+    gpu_python = root / ".venvs" / OBJECT_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe"
+    if gpu_python.is_file():
+        try:
+            probe = subprocess.run(
+                [str(gpu_python), "-c", "import json, torch; print(json.dumps({'cuda': bool(torch.cuda.is_available()), 'torch': torch.__version__}))"],
+                cwd=root,
+                capture_output=True,
+                text=True,
+                timeout=20,
+                check=False,
+            )
+            payload = json.loads(probe.stdout.strip().splitlines()[-1]) if probe.returncode == 0 and probe.stdout.strip() else {}
+            if payload.get("cuda") is True and isinstance(payload.get("torch"), str):
+                return {"python": str(gpu_python), "device": "cuda", "environment": OBJECT_DETECTION_GPU_ENVIRONMENT, "torchVersion": payload["torch"]}
+        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, IndexError):
+            pass
+    if not cpu_python.is_file():
+        raise ApiError("Object-detection CPU virtual environment is unavailable. Run the capability setup first.")
+    return {"python": str(cpu_python), "device": "cpu", "environment": OBJECT_DETECTION_CPU_ENVIRONMENT, "torchVersion": "unknown"}
+
+
+def change_detection_execution_environment(root: Path) -> dict[str, str]:
+    """Choose the fixed ChangeStar CUDA environment only after probing it."""
+    cpu_python = root / ".venvs" / CHANGE_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe"
+    gpu_python = root / ".venvs" / CHANGE_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe"
+    if gpu_python.is_file():
+        try:
+            probe = subprocess.run(
+                [str(gpu_python), "-c", "import json, torch; print(json.dumps({'cuda': bool(torch.cuda.is_available()), 'torch': torch.__version__}))"],
+                cwd=root,
+                capture_output=True,
+                text=True,
+                timeout=20,
+                check=False,
+            )
+            payload = json.loads(probe.stdout.strip().splitlines()[-1]) if probe.returncode == 0 and probe.stdout.strip() else {}
+            if payload.get("cuda") is True and isinstance(payload.get("torch"), str):
+                return {"python": str(gpu_python), "device": "cuda", "environment": CHANGE_DETECTION_GPU_ENVIRONMENT, "torchVersion": payload["torch"]}
+        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, IndexError):
+            pass
+    if not cpu_python.is_file():
+        raise ApiError("Change-detection CPU virtual environment is unavailable. Run the capability setup first.")
+    return {"python": str(cpu_python), "device": "cpu", "environment": CHANGE_DETECTION_CPU_ENVIRONMENT, "torchVersion": "unknown"}
+
+
 def trajectory_runs(root: Path) -> list[dict[str, Any]]:
     output_root = root / "shared" / "outputs" / "15-trajectory-analysis"
     records: list[dict[str, Any]] = []
@@ -204,11 +256,13 @@
         except ValueError:
             continue
         run_id = artifact.name if artifact != output_root else "baseline"
+        device = str(metadata.get("device") or "cpu").lower()
+        execution = "GPU" if device.startswith("cuda") else "CPU"
         records.append(
             {
                 "id": run_id,
                 "label": "既有基线结果" if run_id == "baseline" else run_id,
-                "note": "CPU 基线:人员与常见车辆;树木不在当前模型有效类别内。",
+                "note": f"{execution} 基线:人员与常见车辆;树木不在当前模型有效类别内。",
                 "artifactRoot": relative_path(root, artifact),
                 "inputRoot": input_root,
                 "createdAt": str(metadata.get("created_at") or ""),
@@ -250,10 +304,12 @@
         registered_before_path = artifact / registered_before_name if registered_before_name else None
         registered_after_path = artifact / registered_after_name if registered_after_name else None
         run_id = artifact.name
+        device = str(metadata.get("device") or "cpu").lower()
+        execution = "GPU" if device.startswith("cuda") else "CPU"
         record = {
                 "id": run_id,
                 "label": run_id,
-                "note": "ChangeStar CPU 变化栅格与 GeoAI 像素坐标图斑;结果需人工复核。",
+                "note": f"ChangeStar {execution} 变化栅格与 GeoAI 像素坐标图斑;结果需人工复核。",
                 "artifactRoot": relative_path(root, artifact),
                 "beforeImage": relative_path(root, before_path),
                 "afterImage": relative_path(root, after_path),
@@ -1165,11 +1221,9 @@
         for name, content in decoded:
             (raw_root / name).write_bytes(content)
         output = self.root / "shared" / "outputs" / "01-object-detection" / "runs" / run_id
-        python = self.root / ".venvs" / "01-object-detection" / "Scripts" / "python.exe"
-        if not python.is_file():
-            raise ApiError("Object-detection virtual environment is unavailable. Run the capability setup first.")
+        execution = object_detection_execution_environment(self.root)
         with RUN_LOCK:
-            self.run_command([str(python), str(self.root / "capabilities" / "01-object-detection" / "run_detection.py"), "--input", str(raw_root), "--output", str(output)], 1200)
+            self.run_command([execution["python"], str(self.root / "capabilities" / "01-object-detection" / "run_detection.py"), "--input", str(raw_root), "--output", str(output), "--device", execution["device"]], 1200)
         if not (output / "run_metadata.json").is_file():
             raise ApiError("Detection script finished without the expected result metadata.")
         return next(item for item in detection_runs(self.root) if item["id"] == run_id)
@@ -1219,19 +1273,18 @@
             after_path.write_bytes(decoded_after[1])
         processed_root = self.root / "shared" / "data" / "processed" / "00-change-detection" / run_id
         output = self.root / "shared" / "outputs" / "00-change-detection" / "runs" / run_id
-        python = self.root / ".venvs" / "00-change-detection" / "Scripts" / "python.exe"
-        if not python.is_file():
-            raise ApiError("Change-detection virtual environment is unavailable. Run the capability setup first.")
+        execution = change_detection_execution_environment(self.root)
         with RUN_LOCK:
             self.run_command(
                 [
-                    str(python),
+                    execution["python"],
                     str(self.root / "capabilities" / "00-change-detection" / "run_change_detection.py"),
                     "--before", str(before_path),
                     "--after", str(after_path),
                     "--threshold", f"{threshold:.4f}",
                     "--max-dimension", str(max_dimension),
                     "--processing-mode", processing_mode,
+                    "--device", execution["device"],
                     "--processed-output", str(processed_root),
                     "--output", str(output),
                 ],
@@ -1428,21 +1481,21 @@
         processing_mode: str,
         max_dimension: int,
     ) -> None:
-        python = self.root / ".venvs" / "00-change-detection" / "Scripts" / "python.exe"
+        execution = change_detection_execution_environment(self.root)
+        python = execution["python"]
         try:
-            if not python.is_file():
-                raise ApiError("Change-detection virtual environment is unavailable. Run the capability setup first.")
-            self._update_scan_job(run_id, status="running", phase="inference")
+            self._update_scan_job(run_id, status="running", phase="inference", device=execution["device"], environment=execution["environment"], torchVersion=execution["torchVersion"])
             with RUN_LOCK:
                 self.run_command(
                     [
-                        str(python),
+                        python,
                         str(self.root / "capabilities" / "00-change-detection" / "run_change_detection.py"),
                         "--before", str(before_path),
                         "--after", str(after_path),
                         "--threshold", "0.5000",
                         "--max-dimension", str(max_dimension),
                         "--processing-mode", processing_mode,
+                        "--device", execution["device"],
                         "--processed-output", str(processed_root),
                         "--output", str(inference_output),
                     ],
@@ -1455,7 +1508,7 @@
                 inference_metadata_path.write_text(json.dumps(inference_metadata, ensure_ascii=False, indent=2), encoding="utf-8")
                 self._update_scan_job(run_id, phase="parameter-scan")
                 command = [
-                    str(python),
+                    python,
                     str(self.root / "capabilities" / "00-change-detection" / "scan_change_detection_parameters.py"),
                     "--run-dir", str(inference_output),
                     "--output", str(scan_output),
@@ -1467,7 +1520,7 @@
                 self.run_command(command, SCAN_JOB_TIMEOUT)
                 self._update_scan_job(run_id, phase="vectorization")
                 vector_command = [
-                    str(python),
+                    python,
                     str(self.root / "capabilities" / "00-change-detection" / "materialize_parameter_scan_candidates.py"),
                     "--scan-dir", str(scan_output),
                 ]
@@ -1484,6 +1537,9 @@
                 "minimum_areas": areas,
                 "processing_mode": processing_mode,
                 "max_dimension": max_dimension,
+                "device": execution["device"],
+                "environment": execution["environment"],
+                "torch_version": execution["torchVersion"],
                 "raw_input_dir": relative_path(self.root, before_path.parent.parent),
                 "processed_input_dir": relative_path(self.root, processed_root),
             }

--
Gitblit v1.9.3