From 4e5474405c802c3668b120b3f24531b49abb64c7 Mon Sep 17 00:00:00 2001 From: OLmatter Date: Sat, 13 Jun 2026 19:33:37 +0800 Subject: [PATCH] fix: fallback from broken GPU OCR in auto mode --- scripts/one_click_start.ps1 | 27 ++++++++++---- scripts/setup_backend.py | 14 +++++++ scripts/tools/backend_config.py | 66 ++++++++++++++++++++++++++++++--- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/scripts/one_click_start.ps1 b/scripts/one_click_start.ps1 index d1f6b40..3d318c2 100644 --- a/scripts/one_click_start.ps1 +++ b/scripts/one_click_start.ps1 @@ -33,10 +33,11 @@ function Has-NvidiaGpu { return $LASTEXITCODE -eq 0 } -$Selected = $Target -if ($Selected -eq "auto") { - $Selected = if (Has-NvidiaGpu) { "gpu" } else { "cpu" } +$InstallTarget = $Target +if ($InstallTarget -eq "auto") { + $InstallTarget = if (Has-NvidiaGpu) { "gpu" } else { "cpu" } } +$StartMode = if ($Target -eq "auto") { "auto" } else { $InstallTarget } $CpuPython = Join-Path $Root ".venv_paddle\Scripts\python.exe" $GpuPython = Join-Path $Root ".venv_paddle_gpu\Scripts\python.exe" @@ -44,7 +45,7 @@ $ImportCode = "import ultralytics, paddleocr, paddlex, cv2, PIL, numpy" $Ready = $false $SelectedPython = "" -if ($Selected -eq "gpu") { +if ($InstallTarget -eq "gpu") { $SelectedPython = $GpuPython $Ready = Test-PythonImports $GpuPython $ImportCode } else { @@ -53,8 +54,8 @@ if ($Selected -eq "gpu") { } if (-not $Ready) { - Write-Host "Backend environment is missing or incomplete. Installing $Selected environment..." - $argsList = @("-Target", $Selected) + Write-Host "Backend environment is missing or incomplete. Installing $InstallTarget environment..." + $argsList = @("-Target", $InstallTarget) if ($SelectedPython -and (Test-Path $SelectedPython)) { Write-Host "Existing backend environment failed import checks. Recreating it..." $argsList += "-Recreate" @@ -66,5 +67,15 @@ if (-not $Ready) { & powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\bootstrap_windows.ps1" @argsList } -Write-Host "Starting backend in $Selected mode on port $Port..." -& powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\start_backend.ps1" -Mode $Selected -Port $Port +if ($Target -eq "auto" -and $InstallTarget -eq "gpu" -and -not (Test-PythonImports $CpuPython $ImportCode)) { + Write-Host "CPU fallback environment is missing. Installing CPU environment for auto fallback..." + $fallbackArgs = @("-Target", "cpu") + foreach ($arg in $PipArg) { + $fallbackArgs += "-PipArg" + $fallbackArgs += $arg + } + & powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\bootstrap_windows.ps1" @fallbackArgs +} + +Write-Host "Starting backend in $StartMode mode on port $Port..." +& powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\start_backend.ps1" -Mode $StartMode -Port $Port diff --git a/scripts/setup_backend.py b/scripts/setup_backend.py index 797f7d8..a066ff4 100644 --- a/scripts/setup_backend.py +++ b/scripts/setup_backend.py @@ -74,6 +74,20 @@ def smoke_test(py: Path, mode: str) -> None: "print('cuda_count=', paddle.device.cuda.device_count() if paddle.is_compiled_with_cuda() else 0)" ) run([str(py), "-c", gpu_code]) + gpu_ocr_code = ( + "import os; " + f"os.environ['HOME'] = {str(ROOT / '.paddle_home_gpu')!r}; " + f"os.environ['USERPROFILE'] = {str(ROOT / '.paddle_home_gpu')!r}; " + f"os.environ['PADDLE_HOME'] = {str(ROOT / '.paddle_home_gpu' / '.cache' / 'paddle')!r}; " + f"os.environ['PADDLE_PDX_CACHE_HOME'] = {str(ROOT / '.paddlex_cache_gpu')!r}; " + "os.environ.setdefault('PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK', 'True'); " + "from paddleocr import TextRecognition; " + "r = TextRecognition(model_name='PP-OCRv5_server_rec', device='gpu:0', engine='paddle_dynamic'); " + "close = getattr(r, 'close', None); " + "close() if callable(close) else None; " + "print('gpu ocr ok')" + ) + run([str(py), "-c", gpu_ocr_code]) def check_assets() -> None: diff --git a/scripts/tools/backend_config.py b/scripts/tools/backend_config.py index 3b0085b..5789b05 100644 --- a/scripts/tools/backend_config.py +++ b/scripts/tools/backend_config.py @@ -74,7 +74,21 @@ def _venv_python(name: str) -> Path: return ROOT / name / "bin" / "python" -def detect_gpu(timeout: float = 8.0) -> tuple[bool, str]: +def _gpu_probe_env() -> dict[str, str]: + env = os.environ.copy() + paddle_home = ROOT / ".paddle_home_gpu" + paddlex_cache = ROOT / ".paddlex_cache_gpu" + env["HOME"] = str(paddle_home) + env["USERPROFILE"] = str(paddle_home) + env["PADDLE_HOME"] = str(paddle_home / ".cache" / "paddle") + env["PADDLE_PDX_CACHE_HOME"] = str(paddlex_cache) + env.setdefault("PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK", "True") + env.setdefault("PYTHONUTF8", "1") + env.setdefault("PYTHONIOENCODING", "utf-8") + return env + + +def detect_gpu(timeout: float = 45.0) -> tuple[bool, str]: if parse_bool(os.environ.get("CNCAPTCHA_SKIP_GPU_DETECT"), False): return False, "skipped by CNCAPTCHA_SKIP_GPU_DETECT" @@ -82,6 +96,8 @@ def detect_gpu(timeout: float = 8.0) -> tuple[bool, str]: if not gpu_python.exists(): return False, f"missing {gpu_python}" + env = _gpu_probe_env() + probe = ( "import paddle; " "ok = paddle.is_compiled_with_cuda(); " @@ -92,20 +108,60 @@ def detect_gpu(timeout: float = 8.0) -> tuple[bool, str]: proc = subprocess.run( [str(gpu_python), "-c", probe], cwd=str(ROOT), + env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=timeout, + timeout=min(timeout, 10.0), check=False, ) except Exception as exc: return False, f"probe failed: {exc}" output = (proc.stdout or "").strip() - if proc.returncode == 0 and output.startswith("ok"): + if not (proc.returncode == 0 and output.startswith("ok")): + reason = output or (proc.stderr or "").strip().splitlines()[-1:] or [f"returncode={proc.returncode}"] + return False, str(reason[0]) + + if not parse_bool(os.environ.get("CNCAPTCHA_STRICT_GPU_DETECT"), True): return True, output - reason = output or (proc.stderr or "").strip().splitlines()[-1:] or [f"returncode={proc.returncode}"] - return False, str(reason[0]) + + model_name = os.environ.get("CNCAPTCHA_GPU_OCR_MODEL", "PP-OCRv5_server_rec") + device = os.environ.get("CNCAPTCHA_GPU_OCR_DEVICE", "gpu:0") + engine = os.environ.get("CNCAPTCHA_GPU_OCR_ENGINE", "paddle_dynamic") + ocr_probe = ( + "from paddleocr import TextRecognition; " + f"r = TextRecognition(model_name={model_name!r}, device={device!r}, engine={engine!r}); " + "close = getattr(r, 'close', None); " + "close() if callable(close) else None; " + "print('ocr_ok')" + ) + try: + ocr_proc = subprocess.run( + [str(gpu_python), "-c", ocr_probe], + cwd=str(ROOT), + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired: + return False, f"GPU OCR probe timeout after {timeout:.0f}s" + except Exception as exc: + return False, f"GPU OCR probe failed: {exc}" + + if ocr_proc.returncode == 0 and "ocr_ok" in (ocr_proc.stdout or ""): + return True, output + "; ocr_ok" + + reason_lines = [ + line.strip() + for line in ((ocr_proc.stderr or "") + "\n" + (ocr_proc.stdout or "")).splitlines() + if line.strip() + ] + reason = reason_lines[-1] if reason_lines else f"returncode={ocr_proc.returncode}" + return False, f"GPU OCR unavailable: {reason}" def resolve_backend_config(source: str = "env") -> BackendConfig: