shuishen
7 days ago faf5be476037d3baf4da3515789906a176f9b040
scripts/serve_workbench_console.py
@@ -76,6 +76,8 @@
POINTCLOUD_INFERENCE_JOBS_LOCK = threading.Lock()
MAX_ANNOTATION_LABELS = 400_000
POINTCLOUD_CLASS_CODES = {1, 2, 5, 6, 15, 16}
POINTCLOUD_CPU_ENVIRONMENT = "05-3d-pointcloud"
POINTCLOUD_GPU_ENVIRONMENT = "05-3d-pointcloud-gpu"
class ApiError(ValueError):
@@ -128,6 +130,39 @@
        for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()
def pointcloud_execution_environment(root: Path, requested_device: str = "auto") -> dict[str, str]:
    """Select only a fixed point-cloud interpreter after a short CUDA probe.
    The console never accepts a browser-supplied Python path.  A failed or
    unavailable GPU environment is an expected condition for ``auto`` and
    falls back to the retained CPU environment.
    """
    if requested_device not in {"auto", "cpu", "cuda"}:
        raise ApiError("Point-cloud device must be auto, cpu, or cuda.")
    cpu_python = root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe"
    gpu_python = root / ".venvs" / POINTCLOUD_GPU_ENVIRONMENT / "Scripts" / "python.exe"
    if requested_device != "cpu" and 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": POINTCLOUD_GPU_ENVIRONMENT, "torchVersion": payload["torch"]}
        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, IndexError):
            pass
    if requested_device == "cuda":
        raise ApiError("CUDA was requested, but the fixed point-cloud GPU environment is unavailable.")
    if not cpu_python.is_file():
        raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.")
    return {"python": str(cpu_python), "device": "cpu", "environment": POINTCLOUD_CPU_ENVIRONMENT, "torchVersion": "unknown"}
def trajectory_runs(root: Path) -> list[dict[str, Any]]:
@@ -441,11 +476,10 @@
        return dict(value) if value else None
def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, device: str) -> None:
def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, execution: dict[str, str]) -> None:
    with POINTCLOUD_TRAINING_JOBS_LOCK:
        POINTCLOUD_TRAINING_JOBS[job_id].update({"status": "running", "stage": "training", "startedAt": datetime.now(UTC).isoformat()})
    python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
    command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", device]
    command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", execution["device"]]
    try:
        with RUN_LOCK:
            completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False)
@@ -521,11 +555,10 @@
        raise ApiError("LAS/LAZ 文件没有点记录。请选择包含实际 RGB 点位的完整点云文件,而不是空分块。")
def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path) -> None:
def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path, execution: dict[str, str]) -> None:
    with POINTCLOUD_INFERENCE_JOBS_LOCK:
        POINTCLOUD_INFERENCE_JOBS[job_id].update({"status": "running", "stage": "inference", "startedAt": datetime.now(UTC).isoformat()})
    python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
    command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", "cpu"]
    command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", execution["device"]]
    try:
        with RUN_LOCK:
            completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False)
@@ -1643,9 +1676,9 @@
            source_bytes[name] = raw_path.stat().st_size
            shutil.copyfile(raw_path, processed_root / name)
        output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id
        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
        python = self.root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe"
        if not python.is_file():
            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
            raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.")
        command = [str(python), str(self.root / "capabilities" / "05-3d-pointcloud" / "run_pointcloud_understanding.py"), "--input", str(processed_root), "--output", str(output), "--ground-up-axis", "z"]
        with RUN_LOCK:
            self.run_command(command, 900)
@@ -1710,15 +1743,13 @@
        record = load_json(annotation)
        if record.get("schema_version") != 1:
            raise ApiError("The selected annotation revision is unavailable.")
        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
        if not python.is_file():
            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
        execution = pointcloud_execution_environment(self.root, device)
        job_id = uuid4().hex
        output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / make_run_id("semantic-model")
        job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "device": device, "createdAt": datetime.now(UTC).isoformat()}
        job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "requestedDevice": device, "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat()}
        with POINTCLOUD_TRAINING_JOBS_LOCK:
            POINTCLOUD_TRAINING_JOBS[job_id] = job
        thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, device), daemon=True, name=f"pointcloud-training-{job_id[:8]}")
        thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, execution), daemon=True, name=f"pointcloud-training-{job_id[:8]}")
        thread.start()
        return dict(job)
@@ -1735,9 +1766,7 @@
        if Path(name).suffix.lower() not in suffixes:
            raise ApiError("Model inference requires a PLY, PCD, XYZ, LAS, or LAZ point cloud.")
        validate_pointcloud_model_input(staged_path)
        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
        if not python.is_file():
            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
        execution = pointcloud_execution_environment(self.root)
        run_id = make_run_id("semantic-inference")
        raw_root = self.root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "model-inference-runs" / run_id
        processed_root = self.root / "shared" / "data" / "processed" / "05-3d-pointcloud" / "model-inference-runs" / run_id
@@ -1760,10 +1789,10 @@
        except ValueError as exc:
            raise ApiError("Selected model is outside the allowed training output directory.") from exc
        job_id = uuid4().hex
        job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "device": "cpu", "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)}
        job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "requestedDevice": "auto", "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)}
        with POINTCLOUD_INFERENCE_JOBS_LOCK:
            POINTCLOUD_INFERENCE_JOBS[job_id] = job
        thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output), daemon=True, name=f"pointcloud-inference-{job_id[:8]}")
        thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output, execution), daemon=True, name=f"pointcloud-inference-{job_id[:8]}")
        thread.start()
        return dict(job)