Refactor HVideoTool to exclusively use YOLO for detection and DeepMosaics for restoration: removed classic CV and composite detectors, updated configuration and UI accordingly. Enhanced documentation in README and CLAUDE.md to reflect these changes, including new batch processing capabilities and device diagnostics.

This commit is contained in:
Leonid Pershin
2026-06-07 06:16:05 +03:00
parent cc518cc3e6
commit 9c471ca701
17 changed files with 798 additions and 676 deletions
+191 -48
View File
@@ -1,18 +1,66 @@
"""Probe the PyTorch / CUDA situation, so the UI can show a device badge.
"""Diagnose the PyTorch / CUDA situation so the UI can explain *why* it's on CPU.
Pure (no Qt). ``gather()`` imports torch (slow / heavy) — call it off the GUI
thread. The rest are tiny formatters the UI uses to explain *why* it's on CPU and
how to enable the GPU.
Pure (no Qt). ``gather()`` is the heavy part — it imports torch and shells out to
``nvidia-smi`` — so call it off the GUI thread. ``analyze()`` is fast formatting on
the gathered dict and figures out the most likely cause + concrete fix.
"""
from __future__ import annotations
# pip index for the CUDA build (matches the README).
CUDA_WHEEL_INDEX = "https://download.pytorch.org/whl/cu121"
import re
import shutil
import subprocess
import sys
# pip indexes for the CUDA builds (see README). cu121 needs a driver with CUDA >= 12.1.
CUDA_WHEELS = {
"cu121": "https://download.pytorch.org/whl/cu121",
"cu118": "https://download.pytorch.org/whl/cu118",
}
_NO_WINDOW = 0x08000000 if sys.platform == "win32" else 0 # CREATE_NO_WINDOW
def _run_nvidia_smi() -> dict:
"""Probe the NVIDIA driver/GPU via nvidia-smi. Never raises."""
out: dict = {"found": False, "gpus": [], "driver_version": None, "cuda_driver": None}
exe = shutil.which("nvidia-smi")
if not exe and sys.platform == "win32":
candidate = r"C:\Windows\System32\nvidia-smi.exe"
exe = candidate if shutil.os.path.isfile(candidate) else None
if not exe:
return out
# GPU names + driver version (robust CSV form).
try:
r = subprocess.run(
[exe, "--query-gpu=name,driver_version", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,
)
if r.returncode == 0:
out["found"] = True
for line in r.stdout.strip().splitlines():
parts = [p.strip() for p in line.split(",")]
if parts and parts[0]:
out["gpus"].append(parts[0])
if len(parts) > 1 and parts[1]:
out["driver_version"] = parts[1]
except Exception: # noqa: BLE001 - any failure => "not found"
return out
# Max CUDA version the driver supports (only in the plain header).
try:
r2 = subprocess.run(
[exe], capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,
)
m = re.search(r"CUDA Version:\s*([\d.]+)", r2.stdout)
if m:
out["cuda_driver"] = m.group(1)
except Exception: # noqa: BLE001
pass
return out
def gather() -> dict:
"""Collect torch/CUDA facts. Never raises — missing torch is a valid result."""
"""Collect torch + NVIDIA facts. Never raises — missing torch/GPU are valid."""
info: dict = {
"installed": False,
"version": None, # torch.__version__ (e.g. "2.12.0+cpu")
@@ -20,27 +68,38 @@ def gather() -> dict:
"cuda_available": False,
"device_name": None, # the active GPU's name, if any
"import_error": None,
# filled by nvidia-smi:
"nvidia_smi": False,
"gpus": [],
"driver_version": None,
"cuda_driver": None,
}
try:
import torch
except Exception as exc: # noqa: BLE001 - report any import failure, not just ImportError
except Exception as exc: # noqa: BLE001 - report any import failure
info["import_error"] = str(exc)
return info
info["installed"] = True
info["version"] = getattr(torch, "__version__", None)
try:
info["built_cuda"] = torch.version.cuda
except Exception: # noqa: BLE001
info["built_cuda"] = None
try:
info["cuda_available"] = bool(torch.cuda.is_available())
except Exception: # noqa: BLE001
info["cuda_available"] = False
if info["cuda_available"]:
else:
info["installed"] = True
info["version"] = getattr(torch, "__version__", None)
try:
info["device_name"] = torch.cuda.get_device_name(0)
info["built_cuda"] = torch.version.cuda
except Exception: # noqa: BLE001
info["device_name"] = None
info["built_cuda"] = None
try:
info["cuda_available"] = bool(torch.cuda.is_available())
except Exception: # noqa: BLE001
info["cuda_available"] = False
if info["cuda_available"]:
try:
info["device_name"] = torch.cuda.get_device_name(0)
except Exception: # noqa: BLE001
info["device_name"] = None
smi = _run_nvidia_smi()
info["nvidia_smi"] = smi["found"]
info["gpus"] = smi["gpus"]
info["driver_version"] = smi["driver_version"]
info["cuda_driver"] = smi["cuda_driver"]
return info
@@ -48,33 +107,117 @@ def device_label(info: dict) -> str:
return "CUDA" if info.get("cuda_available") else "CPU"
def reason(info: dict) -> str:
"""One-sentence human explanation of the current device choice."""
if not info.get("installed"):
return ("PyTorch не установлен — детектор YOLO и восстановление DeepMosaics "
"работают на CPU (классический детектор torch не требует).")
if info.get("cuda_available"):
name = info.get("device_name") or "GPU"
return f"PyTorch использует CUDA: {name}. Вычисления идут на видеокарте."
version = info.get("version") or "?"
built = info.get("built_cuda")
if not built:
return (f"Установлена CPU-сборка PyTorch ({version}) — без поддержки CUDA, "
"поэтому вычисления идут на процессоре (медленно).")
return (f"PyTorch собран с CUDA {built} ({version}), но GPU недоступен: нет "
"NVIDIA-видеокарты, не установлен/устарел драйвер, либо версия CUDA "
"несовместима с драйвером.")
def _ver_tuple(v: str | None) -> tuple[int, ...]:
try:
return tuple(int(x) for x in str(v).split(".")[:2])
except (ValueError, AttributeError):
return ()
def install_hint() -> str:
"""Steps to enable the GPU (shown when running on CPU)."""
def recommend_channel(info: dict) -> str:
"""Pick the pip CUDA wheel index that matches the driver (cu118 for older)."""
cd = _ver_tuple(info.get("cuda_driver"))
if cd and cd < (12, 1):
return "cu118"
return "cu121"
def install_command(channel: str = "cu121") -> str:
"""The pip commands to (re)install the chosen CUDA build.
Targets the **running interpreter** (``sys.executable -m pip``) so the command
hits the same venv that runs the app — not whatever ``pip`` is on PATH. (A common
trap: running bare ``pip`` in a global shell while torch lives in the project venv.)
"""
index = CUDA_WHEELS.get(channel, CUDA_WHEELS["cu121"])
py = sys.executable or "python"
q = f'"{py}"' if " " in py else py
return (
"Как включить GPU (NVIDIA):\n"
"1. Нужна видеокарта NVIDIA и свежий драйвер (проверка в консоли: nvidia-smi).\n"
"2. Переустановите PyTorch со сборкой CUDA:\n\n"
" pip uninstall -y torch torchvision\n"
f" pip install torch torchvision --index-url {CUDA_WHEEL_INDEX}\n\n"
"3. Перезапустите приложение.\n\n"
"Классический детектор работает и без CUDA. На CPU детекция и расцензуривание "
"просто медленнее."
f"{q} -m pip uninstall -y torch torchvision\n"
f"{q} -m pip install torch torchvision --index-url {index}"
)
def analyze(info: dict) -> dict:
"""Turn the raw facts into {summary, details[list], steps, command}."""
installed = info.get("installed")
cuda = info.get("cuda_available")
built = info.get("built_cuda")
gpus = info.get("gpus") or []
smi = info.get("nvidia_smi")
driver = info.get("driver_version")
cuda_driver = info.get("cuda_driver")
channel = recommend_channel(info)
command = install_command(channel)
if not installed:
build_str = "не установлен"
elif built:
build_str = f"CUDA {built}"
else:
build_str = "CPU-only (+cpu)"
gpu_str = (
", ".join(gpus) if gpus
else ("не обнаружена" if smi or info.get("nvidia_smi") is False else "nvidia-smi не найден")
)
if not gpus and not smi:
gpu_str = "nvidia-smi не найден (нет драйвера NVIDIA?)"
details = [
f"PyTorch: {info.get('version') or 'не установлен'}",
f"Сборка PyTorch: {build_str}",
f"CUDA доступна в PyTorch: {'да' if cuda else 'нет'}",
f"Видеокарта (nvidia-smi): {gpu_str}",
f"Драйвер NVIDIA: {driver or ''}",
f"Макс. CUDA драйвера: {cuda_driver or ''}",
f"Интерпретатор (venv): {sys.executable}",
]
if info.get("import_error"):
details.append(f"Ошибка импорта torch: {info['import_error']}")
if not installed:
summary = "PyTorch не установлен — детекция YOLO и DeepMosaics идут на CPU."
steps = ("Установите PyTorch (CUDA-сборку, если есть NVIDIA-видеокарта):\n\n"
+ command + "\n\nДля YOLO также: pip install -e \".[yolo]\"")
elif cuda:
gpu = info.get("device_name") or (gpus[0] if gpus else "GPU")
summary = f"Всё в порядке: PyTorch использует CUDA. Активный GPU: {gpu}."
steps = "GPU уже задействован — ничего делать не нужно."
elif not built: # CPU-only torch build — the usual case
if gpus:
summary = (
"Главная причина: установлена CPU-сборка PyTorch (+cpu) — она физически "
f"не умеет в CUDA. Видеокарта ({gpus[0]}) и драйвер {driver or '?'} на месте, "
"поэтому достаточно переустановить PyTorch со сборкой CUDA."
)
steps = ("Переустановите PyTorch под CUDA, затем перезапустите приложение:\n\n"
+ command)
else:
summary = (
"Установлена CPU-сборка PyTorch (+cpu), и видеокарта NVIDIA не обнаружена "
"(nvidia-smi не отвечает). Либо нет NVIDIA GPU, либо не установлен драйвер."
)
steps = ("1. Проверьте видеокарту и драйвер: в консоли выполните nvidia-smi\n"
"2. Если NVIDIA GPU есть — переустановите PyTorch под CUDA:\n\n"
+ command +
"\n\nБез NVIDIA GPU всё работает на CPU — просто медленнее.")
else: # CUDA-enabled torch build, but CUDA still not available
if not gpus:
summary = (
f"PyTorch собран под CUDA {built}, но видеокарта/драйвер NVIDIA не найдены. "
"Скорее всего не установлен драйвер NVIDIA или нет GPU."
)
steps = "Установите свежий драйвер NVIDIA и перезапустите. Проверка: nvidia-smi."
elif cuda_driver and _ver_tuple(cuda_driver) < _ver_tuple(built):
summary = (
f"Драйвер поддерживает CUDA {cuda_driver}, а PyTorch собран под CUDA {built}"
"версия драйвера слишком старая."
)
steps = ("Вариант A — обновите драйвер NVIDIA (рекомендуется).\n"
"Вариант B — поставьте PyTorch под CUDA вашего драйвера:\n\n" + command)
else:
summary = (
f"PyTorch собран под CUDA {built}, GPU ({gpus[0]}) есть, но CUDA недоступна — "
"вероятен конфликт версий или повреждённая установка."
)
steps = "Переустановите PyTorch под CUDA:\n\n" + command
return {"summary": summary, "details": details, "steps": steps, "command": command}