mirror of
https://github.com/chidiwilliams/buzz.git
synced 2026-10-19 11:04:21 +00:00
Unbundle cuda (#1439)
Some checks failed
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / build_wheels (macos-latest) (push) Has been cancelled
CI / build_wheels (ubuntu-latest) (push) Has been cancelled
CI / build_wheels (windows-latest) (push) Has been cancelled
GitHub Pages / Deploy (push) Has been cancelled
Snapcraft / build (push) Has been cancelled
CI / publish_pypi (push) Has been cancelled
CI / release (macos-latest) (push) Has been cancelled
CI / release (windows-latest) (push) Has been cancelled
Snapcraft / upload-edge (push) Has been cancelled
Snapcraft / upload-stable (push) Has been cancelled
Some checks failed
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / build_wheels (macos-latest) (push) Has been cancelled
CI / build_wheels (ubuntu-latest) (push) Has been cancelled
CI / build_wheels (windows-latest) (push) Has been cancelled
GitHub Pages / Deploy (push) Has been cancelled
Snapcraft / build (push) Has been cancelled
CI / publish_pypi (push) Has been cancelled
CI / release (macos-latest) (push) Has been cancelled
CI / release (windows-latest) (push) Has been cancelled
Snapcraft / upload-edge (push) Has been cancelled
Snapcraft / upload-stable (push) Has been cancelled
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
221659720d
commit
7d1929c588
53 changed files with 3593 additions and 887 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
|
@ -200,11 +200,6 @@ jobs:
|
|||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
uv pip install msvc-runtime
|
||||
uv pip install -U torch==2.8.0+cu129 torchaudio==2.8.0+cu129 --index-url https://download.pytorch.org/whl/cu129
|
||||
uv pip install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia-cuda-runtime-cu12==12.9.79 --extra-index-url https://pypi.nvidia.com
|
||||
|
||||
uv cache clean
|
||||
uv run pip cache purge
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
|
|
@ -288,7 +283,6 @@ jobs:
|
|||
name: Buzz-${{ runner.os }}-${{ runner.arch }}
|
||||
path: |
|
||||
dist/Buzz*-windows.exe
|
||||
dist/Buzz*-windows-*.bin
|
||||
dist/Buzz*-mac.dmg
|
||||
dist/Buzz*.AppImage
|
||||
|
||||
|
|
@ -402,6 +396,5 @@ jobs:
|
|||
files: |
|
||||
Buzz*-unix.tar.gz
|
||||
Buzz*.exe
|
||||
Buzz*.bin
|
||||
Buzz*.dmg
|
||||
Buzz*.AppImage
|
||||
26
Buzz.spec
26
Buzz.spec
|
|
@ -105,6 +105,32 @@ else:
|
|||
|
||||
binaries.append(("buzz/whisper_cpp/*", "buzz/whisper_cpp"))
|
||||
|
||||
# Bundle a standalone Python interpreter for runtime pip installs (e.g. CUDA).
|
||||
# We copy python.exe, DLLs, and the stdlib from the uv-managed base interpreter.
|
||||
# The version is derived from the build interpreter so it tracks requires-python.
|
||||
if platform.system() == "Windows":
|
||||
import sys as _sys
|
||||
_base = _sys.base_prefix # e.g. .../uv/python/cpython-3.13.12-windows-x86_64-none
|
||||
_py_dll = f"python{_sys.version_info.major}{_sys.version_info.minor}.dll"
|
||||
_py_dest = "python"
|
||||
if os.path.isfile(os.path.join(_base, "python.exe")):
|
||||
binaries.append((os.path.join(_base, "python.exe"), _py_dest))
|
||||
binaries.append((os.path.join(_base, "python3.dll"), _py_dest))
|
||||
binaries.append((os.path.join(_base, _py_dll), _py_dest))
|
||||
for _vcrt in ("vcruntime140.dll", "vcruntime140_1.dll"):
|
||||
_vcrt_path = os.path.join(_base, _vcrt)
|
||||
if os.path.isfile(_vcrt_path):
|
||||
binaries.append((_vcrt_path, _py_dest))
|
||||
# Bundle DLLs directory (C extensions like _ssl, _socket, etc.)
|
||||
_dlls_dir = os.path.join(_base, "DLLs")
|
||||
if os.path.isdir(_dlls_dir):
|
||||
for _f in os.listdir(_dlls_dir):
|
||||
binaries.append((os.path.join(_dlls_dir, _f), os.path.join(_py_dest, "DLLs")))
|
||||
# Bundle standard library
|
||||
datas.append((os.path.join(_base, "Lib"), os.path.join(_py_dest, "Lib")))
|
||||
else:
|
||||
print(f"WARNING: Could not find bundleable Python at {_base}")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
datas += [("dll_backup", "dll_backup")]
|
||||
datas += collect_data_files("msvc-runtime")
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ pip3 install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia
|
|||
|
||||
For info on how to get latest development version with latest features and bug fixes see [FAQ](https://chidiwilliams.github.io/buzz/docs/faq#9-where-can-i-get-latest-development-version).
|
||||
|
||||
### Nvidia CUDA GPU acceleration
|
||||
|
||||
If you are on Buzz version `1.4.6` or later and have a Nvidia GPU, go to `Help -> About Buzz` and install CUDA Acceleration.
|
||||
|
||||
### Support Buzz
|
||||
|
||||
You can help the Buzz by starring 🌟 the repo and sharing it with your friends.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,27 @@ mkdir -p "$APPDIR/usr/bin" \
|
|||
# Copy entire PyInstaller output into usr/bin/
|
||||
cp -a "$PROJECT_DIR/dist/Buzz/." "$APPDIR/usr/bin/"
|
||||
|
||||
# Strip the executable-stack flag from bundled shared objects. uv's
|
||||
# python-build-standalone marks libpython PT_GNU_STACK=RWE, and kernels from
|
||||
# Linux 7.0 (Ubuntu 26.04) on refuse that at dlopen time, so the app dies with
|
||||
# "cannot enable executable stack as shared object requires: Invalid argument".
|
||||
# Patch the AppDir copy, never the shared uv Python cache.
|
||||
echo "==> Clearing executable-stack flags..."
|
||||
uv run --no-project "$SCRIPT_DIR/clear-execstack.py" "$APPDIR/usr/bin"
|
||||
|
||||
# Bundle uv: the in-app CUDA installer needs it to build the private CUDA venv.
|
||||
# The AppImage ships no standalone interpreter, and the host's python3 is
|
||||
# whatever the distro installed — on Ubuntu 26.04 that is 3.14, which has no
|
||||
# torch wheels. uv downloads a CPython matching the one Buzz is frozen with.
|
||||
echo "==> Bundling uv..."
|
||||
UV_BIN="$(command -v uv)"
|
||||
if [ -z "$UV_BIN" ]; then
|
||||
echo "ERROR: uv not found in PATH; it is required for in-app CUDA installation." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$(readlink -f "$UV_BIN")" "$APPDIR/usr/bin/uv"
|
||||
chmod +x "$APPDIR/usr/bin/uv"
|
||||
|
||||
# ── Step 3: Desktop integration ─────────────────────────────────────────────
|
||||
# Desktop file — Exec must be just the binary name for AppImage spec
|
||||
cat > "$APPDIR/Buzz.desktop" << 'EOF'
|
||||
|
|
@ -82,6 +103,11 @@ cat > "$APPDIR/AppRun" << 'APPRUN'
|
|||
SELF="$(readlink -f "$0")"
|
||||
APPDIR="$(dirname "$SELF")"
|
||||
|
||||
# The type-2 runtime already exports this; re-exporting keeps it correct for
|
||||
# an extracted AppDir too, so buzz/cuda_manager.py's is_appimage() can tell
|
||||
# that Buzz itself is the running bundle.
|
||||
export APPDIR
|
||||
|
||||
export PATH="$APPDIR/usr/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$APPDIR/usr/bin:${LD_LIBRARY_PATH:-}"
|
||||
export QT_MEDIA_BACKEND=ffmpeg
|
||||
|
|
@ -111,10 +137,26 @@ if [ ! -f "$RUNTIME" ]; then
|
|||
"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-${ARCH}"
|
||||
fi
|
||||
|
||||
# Use APPIMAGETOOL_EXTRACT_AND_RUN when FUSE is unavailable (CI, containers)
|
||||
# Fall back to extract-and-run when FUSE is unavailable (CI, containers) or
|
||||
# unusable. Presence of `fusermount` is not enough: distros that ship only FUSE 3
|
||||
# (Ubuntu 25.10+, where /usr/bin/fusermount is a symlink to fusermount3) still
|
||||
# fail to mount appimagetool's FUSE 2 runtime. Probe it instead of guessing.
|
||||
#
|
||||
# The variable is APPIMAGE_EXTRACT_AND_RUN: it is read by the AppImage type-2
|
||||
# runtime, which is what appimagetool (itself an AppImage) boots through. There
|
||||
# is no APPIMAGETOOL_-prefixed variant -- setting one is silently ignored and
|
||||
# the build dies with "Cannot mount AppImage, please check your FUSE setup."
|
||||
EXTRA_ARGS=(--runtime-file "$RUNTIME" --no-appstream)
|
||||
if [ "${CI:-}" = "true" ] || ! command -v fusermount &>/dev/null; then
|
||||
export APPIMAGETOOL_EXTRACT_AND_RUN=1
|
||||
if [ "${CI:-}" = "true" ] || ! "$APPIMAGETOOL" --version >/dev/null 2>&1; then
|
||||
echo "==> FUSE unusable for appimagetool; falling back to extract-and-run"
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
fi
|
||||
|
||||
# If it still cannot start, neither mounting nor extraction works -- fail here
|
||||
# with a clear message instead of midway through packaging.
|
||||
if ! "$APPIMAGETOOL" --version >/dev/null 2>&1; then
|
||||
echo "ERROR: cannot run $APPIMAGETOOL (neither FUSE mount nor extract-and-run works)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate AppStream metadata ourselves in offline mode. appimagetool's internal
|
||||
|
|
|
|||
87
appimage/clear-execstack.py
Executable file
87
appimage/clear-execstack.py
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Clear the executable-stack flag from bundled ELF shared objects.
|
||||
|
||||
uv's python-build-standalone ships libpython marked PT_GNU_STACK = RWE, and
|
||||
PyInstaller copies it verbatim. Recent kernels (Ubuntu 26.04 / Linux 7.0)
|
||||
refuse to make the stack executable at dlopen time, so loading the library
|
||||
dies with:
|
||||
|
||||
cannot enable executable stack as shared object requires: Invalid argument
|
||||
|
||||
Nothing in CPython actually needs an executable stack -- the flag is a build
|
||||
artifact -- so clearing PF_X on the PT_GNU_STACK header is the accepted fix.
|
||||
This is the same edit `execstack -c` / `patchelf --clear-execstack` perform,
|
||||
done here so the build needs no extra tooling.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
PT_GNU_STACK = 0x6474E551
|
||||
PF_X = 0x1
|
||||
|
||||
|
||||
def clear_execstack(path):
|
||||
"""Return True if the file was modified."""
|
||||
with open(path, "rb") as fh:
|
||||
header = fh.read(64)
|
||||
|
||||
# ELF64, little-endian only -- the only shape we ship on Linux.
|
||||
if len(header) < 64 or header[:4] != b"\x7fELF":
|
||||
return False
|
||||
if header[4] != 2 or header[5] != 1:
|
||||
return False
|
||||
|
||||
e_phoff = struct.unpack_from("<Q", header, 0x20)[0]
|
||||
e_phentsize = struct.unpack_from("<H", header, 0x36)[0]
|
||||
e_phnum = struct.unpack_from("<H", header, 0x38)[0]
|
||||
if not e_phoff or e_phentsize < 8:
|
||||
return False
|
||||
|
||||
with open(path, "r+b") as fh:
|
||||
for i in range(e_phnum):
|
||||
entry = e_phoff + i * e_phentsize
|
||||
fh.seek(entry)
|
||||
chunk = fh.read(8)
|
||||
if len(chunk) < 8:
|
||||
break
|
||||
p_type, p_flags = struct.unpack("<II", chunk)
|
||||
if p_type != PT_GNU_STACK or not (p_flags & PF_X):
|
||||
continue
|
||||
fh.seek(entry + 4)
|
||||
fh.write(struct.pack("<I", p_flags & ~PF_X))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print("usage: clear-execstack.py <dir-or-file>...", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
targets = []
|
||||
for arg in argv[1:]:
|
||||
if os.path.isdir(arg):
|
||||
for root, _dirs, files in os.walk(arg):
|
||||
targets.extend(os.path.join(root, f) for f in files)
|
||||
else:
|
||||
targets.append(arg)
|
||||
|
||||
patched = 0
|
||||
for target in targets:
|
||||
if os.path.islink(target) or not os.path.isfile(target):
|
||||
continue
|
||||
try:
|
||||
if clear_execstack(target):
|
||||
print(f" cleared execstack: {target}")
|
||||
patched += 1
|
||||
except OSError as exc:
|
||||
print(f" warning: {target}: {exc}", file=sys.stderr)
|
||||
|
||||
print(f" {patched} file(s) patched")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
491
buzz/cuda_manager.py
Normal file
491
buzz/cuda_manager.py
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
"""
|
||||
Utilities for checking and installing CUDA support at runtime.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pinned versions matching uv.lock for the cu129 build of PyTorch.
|
||||
# All packages are served from the PyTorch wheel index; pip selects the
|
||||
# correct platform wheel automatically (Linux-only packages have no
|
||||
# Windows wheel and are silently skipped by pip on Windows).
|
||||
CUDA_INDEX_URL = "https://download.pytorch.org/whl/cu129"
|
||||
CUDA_TORCH_PACKAGES = [
|
||||
"torch==2.8.0+cu129",
|
||||
"torchaudio==2.8.0+cu129",
|
||||
]
|
||||
|
||||
# NVIDIA runtime libraries — sourced from the official NVIDIA PyPI index.
|
||||
# Versions are pinned to those resolved in uv.lock to prevent accidental upgrades.
|
||||
# Packages that have wheels for both Linux and Windows (verified via uv.lock).
|
||||
CUDA_NVIDIA_PACKAGES_COMMON = [
|
||||
"nvidia-cublas-cu12==12.9.1.4",
|
||||
"nvidia-cudnn-cu12==9.10.2.21",
|
||||
]
|
||||
|
||||
# Packages that only have Linux (manylinux) wheels in uv.lock.
|
||||
CUDA_NVIDIA_PACKAGES_LINUX = [
|
||||
"nvidia-cuda-cupti-cu12==12.9.79",
|
||||
"nvidia-cuda-nvrtc-cu12==12.9.86",
|
||||
"nvidia-cuda-runtime-cu12==12.9.79",
|
||||
"nvidia-cufft-cu12==11.4.1.4",
|
||||
"nvidia-cufile-cu12==1.14.1.1",
|
||||
"nvidia-curand-cu12==10.3.10.19",
|
||||
"nvidia-cusolver-cu12==11.7.5.82",
|
||||
"nvidia-cusparse-cu12==12.5.10.65",
|
||||
"nvidia-cusparselt-cu12==0.7.1",
|
||||
"nvidia-nccl-cu12==2.27.3",
|
||||
"nvidia-nvjitlink-cu12==12.9.86",
|
||||
"nvidia-nvtx-cu12==12.9.79",
|
||||
]
|
||||
|
||||
|
||||
# The name of Buzz's own snap, as declared in snap/snapcraft.yaml.
|
||||
SNAP_NAME = "buzz"
|
||||
|
||||
# Name of the private virtual environment holding the CUDA packages.
|
||||
CUDA_ENV_DIR_NAME = "cuda_env"
|
||||
|
||||
# Name used before the switch to a private venv. Still cleaned up so an
|
||||
# upgrade does not leave several gigabytes of orphaned wheels behind.
|
||||
LEGACY_CUDA_DIR_NAME = "cuda_packages"
|
||||
|
||||
|
||||
def is_snap() -> bool:
|
||||
"""Returns True if running inside Buzz's own Snap package.
|
||||
|
||||
Testing for SNAP alone is not enough: any snap-packaged tool that launches
|
||||
Buzz (e.g. a snap-installed uv used during development) exports SNAP* into
|
||||
the environment, which would send this multi-gigabyte CUDA install into an
|
||||
unrelated snap's user data.
|
||||
"""
|
||||
return os.environ.get("SNAP_NAME") == SNAP_NAME
|
||||
|
||||
|
||||
def is_flatpak() -> bool:
|
||||
"""Returns True if running inside a Flatpak sandbox."""
|
||||
return "FLATPAK_ID" in os.environ
|
||||
|
||||
|
||||
def is_appimage() -> bool:
|
||||
"""Returns True if this Buzz is running from its own AppImage.
|
||||
|
||||
The type-2 runtime exports APPIMAGE (the .AppImage path) and APPDIR (the
|
||||
mounted bundle). Testing APPIMAGE alone is not enough, for the same reason
|
||||
as in is_snap(): any AppImage-packaged tool that launches Buzz leaks those
|
||||
into the environment. Requiring the running executable to live inside
|
||||
APPDIR ties the answer to *this* bundle.
|
||||
"""
|
||||
appdir = os.environ.get("APPDIR")
|
||||
if not appdir or "APPIMAGE" not in os.environ:
|
||||
return False
|
||||
try:
|
||||
return Path(sys.executable).resolve().is_relative_to(Path(appdir).resolve())
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def should_offer_cuda_prompt() -> bool:
|
||||
"""Returns True on platforms where in-app CUDA installation is supported."""
|
||||
|
||||
return sys.platform in ("win32", "linux")
|
||||
|
||||
|
||||
def is_cuda_torch_installed() -> bool:
|
||||
"""Returns True if torch with CUDA support is available."""
|
||||
try:
|
||||
import torch
|
||||
cuda_available = torch.cuda.is_available()
|
||||
logger.info(
|
||||
"CUDA check: torch version=%s, cuda_built=%s, cuda_available=%s, cuda_version=%s",
|
||||
torch.__version__,
|
||||
torch.version.cuda,
|
||||
cuda_available,
|
||||
torch.version.cuda if cuda_available else "N/A",
|
||||
)
|
||||
if not cuda_available and torch.version.cuda:
|
||||
# CUDA was compiled in but is not available at runtime — likely a DLL loading issue
|
||||
logger.warning(
|
||||
"CUDA check: torch was built with CUDA %s but cuda is not available. "
|
||||
"This usually means CUDA DLLs failed to load. torch.cuda.is_available() returned False.",
|
||||
torch.version.cuda,
|
||||
)
|
||||
return cuda_available
|
||||
except ImportError:
|
||||
logger.info("CUDA check: torch is not installed")
|
||||
return False
|
||||
|
||||
|
||||
def is_nvidia_gpu_present() -> bool:
|
||||
"""Returns True if an NVIDIA GPU is detected.
|
||||
|
||||
Tries nvidia-smi first, then falls back to /proc/driver/nvidia/version
|
||||
which is accessible inside Snap and Flatpak sandboxes without executing
|
||||
an external binary.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nvidia-smi"],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
**_subprocess_hide_window_kwargs(),
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
pass
|
||||
|
||||
# Fallback: kernel driver version file — present when NVIDIA driver is loaded
|
||||
return Path("/proc/driver/nvidia/version").exists()
|
||||
|
||||
|
||||
def get_cuda_root_dir() -> Path:
|
||||
"""Return the writable Buzz-owned directory that holds the CUDA install.
|
||||
|
||||
Snap and Flatpak have their own per-sandbox data directories. Everywhere
|
||||
else this is Buzz's platform data directory (on Windows a subdirectory of
|
||||
%LOCALAPPDATA%\\Buzz, which the uninstaller offers to delete), so the
|
||||
multi-gigabyte CUDA install never lands in a shared site-packages and goes
|
||||
away with the app.
|
||||
"""
|
||||
if is_snap():
|
||||
snap_user_data = os.environ.get("SNAP_USER_DATA")
|
||||
if snap_user_data:
|
||||
return Path(snap_user_data)
|
||||
return Path.home() / ".local" / "share" / "buzz"
|
||||
if is_flatpak():
|
||||
xdg_data = os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share"))
|
||||
return Path(xdg_data) / "buzz"
|
||||
|
||||
from platformdirs import user_data_dir
|
||||
|
||||
return Path(user_data_dir("Buzz"))
|
||||
|
||||
|
||||
def get_cuda_env_dir() -> Path:
|
||||
"""Return the path of the private virtualenv holding the CUDA packages."""
|
||||
return get_cuda_root_dir() / CUDA_ENV_DIR_NAME
|
||||
|
||||
|
||||
def get_cuda_env_site_packages(env_dir: Path | None = None) -> Path | None:
|
||||
"""Return the site-packages directory inside the CUDA venv, if it exists.
|
||||
|
||||
The Linux layout embeds the Python version (lib/python3.13/site-packages),
|
||||
so it is globbed rather than derived: after a Buzz upgrade to a different
|
||||
Python the directory is still found, and the ABI check in buzz/cuda_setup.py
|
||||
is what decides whether it may be used.
|
||||
"""
|
||||
env_dir = env_dir if env_dir is not None else get_cuda_env_dir()
|
||||
|
||||
if sys.platform == "win32":
|
||||
site_packages = env_dir / "Lib" / "site-packages"
|
||||
return site_packages if site_packages.is_dir() else None
|
||||
|
||||
candidates = sorted(env_dir.glob("lib/python3.*/site-packages"))
|
||||
return candidates[-1] if candidates else None
|
||||
|
||||
|
||||
def _find_stale_cuda_dirs(env_dir: Path) -> list[Path]:
|
||||
"""Return existing CUDA install dirs that a fresh install should replace.
|
||||
|
||||
Includes the current env (a previous or half-finished install), the legacy
|
||||
--target directory it replaced, and, under Snap, the same directories in
|
||||
other revisions: snapd copies $SNAP_USER_DATA forward on every refresh, so
|
||||
each revision keeps its own multi-gigabyte copy, and one built for an older
|
||||
Python is unusable after an upgrade.
|
||||
"""
|
||||
roots = [env_dir.parent]
|
||||
|
||||
if is_snap():
|
||||
snap_user_data = os.environ.get("SNAP_USER_DATA")
|
||||
if snap_user_data:
|
||||
revisions_root = Path(snap_user_data).parent
|
||||
try:
|
||||
revisions = sorted(revisions_root.iterdir())
|
||||
except OSError as exc:
|
||||
logger.warning("Could not list snap revisions in %s: %s", revisions_root, exc)
|
||||
revisions = []
|
||||
for revision in revisions:
|
||||
# 'current' is a symlink to the active revision — skip it so we
|
||||
# never delete the same directory twice via two names.
|
||||
if revision.is_symlink() or not revision.is_dir():
|
||||
continue
|
||||
if revision not in roots:
|
||||
roots.append(revision)
|
||||
|
||||
stale: list[Path] = []
|
||||
for root in roots:
|
||||
for name in (CUDA_ENV_DIR_NAME, LEGACY_CUDA_DIR_NAME):
|
||||
candidate = root / name
|
||||
if candidate.is_dir() and candidate not in stale:
|
||||
stale.append(candidate)
|
||||
return stale
|
||||
|
||||
|
||||
def _cleanup_old_cuda_packages(env_dir: Path, report=None) -> None:
|
||||
"""Delete previously installed CUDA packages before a fresh install.
|
||||
|
||||
Installing over an existing environment leaves files from the old install
|
||||
behind, which is how an ABI-incompatible torch survives a Python upgrade
|
||||
and shadows the bundled one (see buzz/cuda_setup.py).
|
||||
"""
|
||||
for stale in _find_stale_cuda_dirs(env_dir):
|
||||
message = f"Removing previous CUDA packages in {stale}..."
|
||||
logger.info(message)
|
||||
if report:
|
||||
report(message)
|
||||
try:
|
||||
shutil.rmtree(stale)
|
||||
except OSError as exc:
|
||||
# Not fatal: the install below recreates what it can, and cuda_setup
|
||||
# skips the directory if what remains is incompatible.
|
||||
logger.warning("Could not remove %s: %s", stale, exc)
|
||||
if report:
|
||||
report(f"Warning: could not remove {stale}: {exc}")
|
||||
|
||||
|
||||
def install_cuda(progress_callback=None):
|
||||
"""
|
||||
Install CUDA-enabled torch and nvidia libraries into a private venv.
|
||||
|
||||
Nothing is written to the user's global or user site-packages: everything
|
||||
lands in get_cuda_env_dir(), which Buzz puts on sys.path at startup and
|
||||
removes when GPU support is reinstalled.
|
||||
|
||||
Args:
|
||||
progress_callback: Optional callable(str) called with status messages.
|
||||
"""
|
||||
def report(msg):
|
||||
logger.info(msg)
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
|
||||
env_dir = get_cuda_env_dir()
|
||||
_cleanup_old_cuda_packages(env_dir, report)
|
||||
|
||||
report(f"Creating environment for CUDA packages in {env_dir}...")
|
||||
install_cmd = _create_cuda_env(env_dir, progress_callback=report)
|
||||
|
||||
nvidia_packages = CUDA_NVIDIA_PACKAGES_COMMON + (
|
||||
CUDA_NVIDIA_PACKAGES_LINUX if sys.platform != "win32" else []
|
||||
)
|
||||
report("Installing NVIDIA CUDA libraries...")
|
||||
_run_install(
|
||||
install_cmd,
|
||||
nvidia_packages,
|
||||
extra_args=["--index-url", CUDA_INDEX_URL],
|
||||
progress_callback=report,
|
||||
)
|
||||
|
||||
report("Installing CUDA-enabled PyTorch...")
|
||||
_run_install(
|
||||
install_cmd,
|
||||
CUDA_TORCH_PACKAGES,
|
||||
extra_args=["--index-url", CUDA_INDEX_URL, "--no-deps"],
|
||||
progress_callback=report,
|
||||
)
|
||||
|
||||
report("CUDA installation complete. Please restart Buzz to enable GPU acceleration.")
|
||||
|
||||
|
||||
def get_python_version() -> str:
|
||||
"""Return the major.minor version the CUDA wheels must be built for."""
|
||||
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
|
||||
def _interpreter_version_matches(python: str) -> bool:
|
||||
"""Return True if `python` reports the same major.minor as the running one.
|
||||
|
||||
The CUDA wheels are ABI-specific and Buzz imports them into its own
|
||||
process, so an interpreter of any other version is useless here — torch
|
||||
publishes no wheel for it (a newer Python), or the extension modules it
|
||||
does install cannot be loaded by the app.
|
||||
"""
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
[python, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
**_subprocess_hide_window_kwargs(),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
logger.warning("Could not probe the version of %s: %s", python, exc)
|
||||
return False
|
||||
|
||||
if probe.returncode != 0:
|
||||
return False
|
||||
return probe.stdout.strip() == get_python_version()
|
||||
|
||||
|
||||
def _get_base_python() -> str | None:
|
||||
"""Return a real Python interpreter that can create the CUDA venv.
|
||||
|
||||
In a frozen PyInstaller bundle sys.executable is the app binary and cannot
|
||||
run -m venv, so the interpreter shipped alongside the app is used instead.
|
||||
Its version always matches the one Buzz was frozen with, which matters
|
||||
because the CUDA wheels are ABI-specific.
|
||||
|
||||
Returns None when no interpreter of the right version is available — the
|
||||
AppImage bundle ships no separate interpreter, and the host's `python3` is
|
||||
whatever the distro installed (Ubuntu 26.04 ships 3.14, which has no torch
|
||||
wheels at all). uv can download a matching one in that case; see
|
||||
_create_cuda_env.
|
||||
"""
|
||||
if getattr(sys, "frozen", False):
|
||||
# PyInstaller extracts bundled data to sys._MEIPASS (_internal dir)
|
||||
internal_dir = Path(getattr(sys, "_MEIPASS", Path(sys.executable).parent))
|
||||
python_name = "python.exe" if sys.platform == "win32" else "python3"
|
||||
bundled_python = internal_dir / "python" / python_name
|
||||
if bundled_python.is_file():
|
||||
return str(bundled_python)
|
||||
# Fallback: look in PATH, but only for an interpreter of our version.
|
||||
version = get_python_version()
|
||||
for candidate in (f"python{version}", "python3", "python"):
|
||||
python = shutil.which(candidate)
|
||||
if python and _interpreter_version_matches(python):
|
||||
return python
|
||||
logger.info(
|
||||
"No Python %s interpreter found in PATH for the CUDA environment", version
|
||||
)
|
||||
return None
|
||||
|
||||
# Inside a venv, base_executable is the interpreter the venv was built from;
|
||||
# deriving the new venv from it avoids chaining venvs.
|
||||
return getattr(sys, "_base_executable", None) or sys.executable
|
||||
|
||||
|
||||
def _find_uv() -> str | None:
|
||||
"""Return the path of a usable uv binary, or None.
|
||||
|
||||
The snap ships uv (stage-snaps in snap/snapcraft.yaml) and its Python has
|
||||
neither pip nor ensurepip, so uv is the only way to create the venv there.
|
||||
"""
|
||||
candidates = []
|
||||
snap_dir = os.environ.get("SNAP")
|
||||
if snap_dir:
|
||||
candidates.append(str(Path(snap_dir) / "bin" / "uv"))
|
||||
found = shutil.which("uv")
|
||||
if found:
|
||||
candidates.append(found)
|
||||
|
||||
for candidate in candidates:
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
[candidate, "--version"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
**_subprocess_hide_window_kwargs(),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
continue
|
||||
if probe.returncode == 0:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _venv_python(env_dir: Path) -> Path:
|
||||
"""Return the interpreter path inside a venv."""
|
||||
if sys.platform == "win32":
|
||||
return env_dir / "Scripts" / "python.exe"
|
||||
return env_dir / "bin" / "python"
|
||||
|
||||
|
||||
def _create_cuda_env(env_dir: Path, progress_callback=None) -> list[str]:
|
||||
"""Create the private CUDA venv and return the install command prefix.
|
||||
|
||||
Two ways in, because no single one works everywhere:
|
||||
- uv, when available (the snap's Python has no pip and ensurepip is pruned
|
||||
from the snap payload, so `-m venv` cannot bootstrap one there).
|
||||
- the stdlib venv module, which bootstraps pip into the new environment.
|
||||
The bundled Windows interpreter ships the full stdlib, so this works even
|
||||
though the app directory under Program Files is not writable.
|
||||
"""
|
||||
env_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
python = _get_base_python()
|
||||
hide_kwargs = _subprocess_hide_window_kwargs()
|
||||
|
||||
uv = _find_uv()
|
||||
if uv:
|
||||
# With no matching interpreter on the machine, ask uv for the version
|
||||
# instead of a path: it downloads a managed CPython of exactly that
|
||||
# version, which is what the cu129 wheels are built for.
|
||||
python_arg = python or get_python_version()
|
||||
logger.info("Creating CUDA venv with uv (%s) from %s", uv, python_arg)
|
||||
_run_command(
|
||||
[uv, "venv", "--python", python_arg, str(env_dir)],
|
||||
progress_callback=progress_callback,
|
||||
error_message="Could not create the environment for CUDA packages",
|
||||
)
|
||||
# --no-cache: the wheels are several gigabytes and are never reused,
|
||||
# so a second copy in the uv cache is pure waste.
|
||||
return [
|
||||
uv, "pip", "install", "--no-cache",
|
||||
"--python", str(_venv_python(env_dir)),
|
||||
]
|
||||
|
||||
if python is None:
|
||||
raise RuntimeError(
|
||||
f"Could not find a Python {get_python_version()} interpreter to create "
|
||||
"the environment for CUDA packages. Install Python "
|
||||
f"{get_python_version()} (or uv) and try again."
|
||||
)
|
||||
|
||||
logger.info("Creating CUDA venv with %s -m venv", python)
|
||||
result = subprocess.run(
|
||||
[python, "-m", "venv", str(env_dir)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
**hide_kwargs,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
"Could not create the environment for CUDA packages in "
|
||||
f"{env_dir}: {(result.stderr or result.stdout or '').strip()}"
|
||||
)
|
||||
return [str(_venv_python(env_dir)), "-m", "pip", "install", "--no-cache-dir"]
|
||||
|
||||
|
||||
def _subprocess_hide_window_kwargs() -> dict[str, Any]:
|
||||
"""Return kwargs to hide the console window on Windows."""
|
||||
if sys.platform == "win32":
|
||||
si = subprocess.STARTUPINFO()
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
si.wShowWindow = subprocess.SW_HIDE
|
||||
return {"startupinfo": si, "creationflags": subprocess.CREATE_NO_WINDOW}
|
||||
return {}
|
||||
|
||||
|
||||
def _run_command(cmd, progress_callback=None, error_message="Command failed"):
|
||||
"""Run a command, streaming its output to progress_callback."""
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
**_subprocess_hide_window_kwargs(),
|
||||
)
|
||||
for line in process.stdout:
|
||||
line = line.rstrip()
|
||||
if line and progress_callback:
|
||||
progress_callback(line)
|
||||
|
||||
process.wait()
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(f"{error_message} (exit code {process.returncode})")
|
||||
|
||||
|
||||
def _run_install(install_cmd, packages, extra_args=None, progress_callback=None):
|
||||
cmd = list(install_cmd) + list(packages)
|
||||
if extra_args:
|
||||
cmd += extra_args
|
||||
|
||||
_run_command(cmd, progress_callback=progress_callback, error_message="pip install failed")
|
||||
|
|
@ -6,7 +6,8 @@ It handles locating and loading CUDA libraries (cuDNN, cuBLAS, etc.) from the nv
|
|||
pip packages.
|
||||
|
||||
On Windows: Uses os.add_dll_directory() to add library paths
|
||||
On Linux: Uses ctypes to preload libraries (LD_LIBRARY_PATH is read at process start)
|
||||
On Linux: Re-execs the process with LD_LIBRARY_PATH set when a CUDA venv is present,
|
||||
then preloads libraries with RTLD_GLOBAL for any remaining gaps.
|
||||
On macOS: No action needed (CUDA not supported)
|
||||
"""
|
||||
|
||||
|
|
@ -21,15 +22,93 @@ from pathlib import Path
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_cuda_target_dir() -> Path | None:
|
||||
"""Return the site-packages of the CUDA venv installed by cuda_manager.
|
||||
|
||||
Returns None when GPU support has never been installed. The lookup lives in
|
||||
buzz/cuda_manager.py so the installer and the loader can never disagree on
|
||||
where the packages went; it imports nothing that pulls in torch.
|
||||
"""
|
||||
from buzz.cuda_manager import get_cuda_env_site_packages
|
||||
|
||||
try:
|
||||
return get_cuda_env_site_packages()
|
||||
except OSError as exc:
|
||||
logger.warning("Could not look up the CUDA environment: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _is_abi_compatible(cuda_target: Path) -> bool:
|
||||
"""Check that a torch in cuda_target was built for the running Python.
|
||||
|
||||
The CUDA packages live in a venv outside the app that survives app
|
||||
upgrades. If Buzz later runs on a different Python version, the torch there
|
||||
is built against the wrong C ABI and would shadow the bundled one, failing
|
||||
the import with an undefined symbol error. Detect that and skip the dir.
|
||||
"""
|
||||
torch_dir = cuda_target / "torch"
|
||||
if not torch_dir.exists():
|
||||
# Nothing that could shadow the bundled torch.
|
||||
return True
|
||||
|
||||
ext_modules = list(torch_dir.glob("_C.cpython-*.so")) + list(
|
||||
torch_dir.glob("_C.cp*-win*.pyd")
|
||||
)
|
||||
if not ext_modules:
|
||||
# Can't tell (unexpected layout); leave the dir alone.
|
||||
return True
|
||||
|
||||
expected = f"cpython-{sys.version_info.major}{sys.version_info.minor}"
|
||||
expected_win = f"cp{sys.version_info.major}{sys.version_info.minor}"
|
||||
for module in ext_modules:
|
||||
if expected in module.name or expected_win in module.name:
|
||||
return True
|
||||
|
||||
logger.warning(
|
||||
"Ignoring CUDA packages in %s: torch there was built for a different "
|
||||
"Python version (found %s, running %s). Reinstall CUDA support from "
|
||||
"Help - About Buzz to use GPU acceleration.",
|
||||
cuda_target,
|
||||
", ".join(sorted(m.name for m in ext_modules)),
|
||||
expected,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _get_site_packages_dirs() -> list[Path]:
|
||||
"""Return all site-packages directories, including user site-packages."""
|
||||
site_packages_dirs = []
|
||||
import site
|
||||
|
||||
# GPU support installed from within Buzz lives in its own venv
|
||||
cuda_target = _get_cuda_target_dir()
|
||||
if cuda_target and cuda_target.exists() and _is_abi_compatible(cuda_target):
|
||||
if str(cuda_target) not in sys.path:
|
||||
sys.path.insert(0, str(cuda_target))
|
||||
site_packages_dirs.append(cuda_target)
|
||||
logger.info("CUDA environment site-packages: %s", cuda_target)
|
||||
|
||||
user_site = site.getusersitepackages()
|
||||
if user_site:
|
||||
user_site_path = Path(user_site)
|
||||
site_packages_dirs.append(user_site_path)
|
||||
logger.info("User site-packages: %s (exists=%s)", user_site_path, user_site_path.exists())
|
||||
# Ensure user site-packages is on sys.path so torch can be imported
|
||||
if str(user_site_path) not in sys.path and user_site_path.exists():
|
||||
sys.path.insert(0, str(user_site_path))
|
||||
logger.info("Added user site-packages to sys.path: %s", user_site_path)
|
||||
for path in sys.path:
|
||||
if "site-packages" in path:
|
||||
site_packages_dirs.append(Path(path))
|
||||
|
||||
return site_packages_dirs
|
||||
|
||||
|
||||
def _get_nvidia_package_lib_dirs() -> list[Path]:
|
||||
"""Find all nvidia package library directories in site-packages."""
|
||||
lib_dirs = []
|
||||
|
||||
# Find site-packages directories
|
||||
site_packages_dirs = []
|
||||
for path in sys.path:
|
||||
if "site-packages" in path:
|
||||
site_packages_dirs.append(Path(path))
|
||||
site_packages_dirs = _get_site_packages_dirs()
|
||||
|
||||
# Also check relative to the current module for frozen apps
|
||||
if getattr(sys, "frozen", False):
|
||||
|
|
@ -47,7 +126,7 @@ def _get_nvidia_package_lib_dirs() -> list[Path]:
|
|||
if bin_subdir.exists():
|
||||
lib_dirs.append(bin_subdir)
|
||||
|
||||
# Check each site-packages for nvidia packages
|
||||
# Check each site-packages for nvidia packages AND torch/lib
|
||||
for sp_dir in site_packages_dirs:
|
||||
nvidia_dir = sp_dir / "nvidia"
|
||||
if nvidia_dir.exists():
|
||||
|
|
@ -60,6 +139,10 @@ def _get_nvidia_package_lib_dirs() -> list[Path]:
|
|||
bin_subdir = pkg_dir / "bin"
|
||||
if bin_subdir.exists():
|
||||
lib_dirs.append(bin_subdir)
|
||||
# torch/lib contains torch_cuda and other CUDA-related DLLs
|
||||
torch_lib_dir = sp_dir / "torch" / "lib"
|
||||
if torch_lib_dir.exists():
|
||||
lib_dirs.append(torch_lib_dir)
|
||||
|
||||
return lib_dirs
|
||||
|
||||
|
|
@ -67,48 +150,149 @@ def _get_nvidia_package_lib_dirs() -> list[Path]:
|
|||
def _setup_windows_dll_directories():
|
||||
"""Add nvidia library directories to Windows DLL search path."""
|
||||
lib_dirs = _get_nvidia_package_lib_dirs()
|
||||
if not lib_dirs:
|
||||
logger.warning("CUDA setup: no nvidia/torch library directories found")
|
||||
for lib_dir in lib_dirs:
|
||||
try:
|
||||
os.add_dll_directory(str(lib_dir))
|
||||
logger.info("CUDA setup: added DLL directory: %s", lib_dir)
|
||||
except (OSError, AttributeError) as e:
|
||||
pass
|
||||
logger.warning("CUDA setup: failed to add DLL directory %s: %s", lib_dir, e)
|
||||
|
||||
|
||||
def _preload_linux_libraries():
|
||||
"""Preload CUDA libraries on Linux using ctypes.
|
||||
def _collect_cuda_lib_dirs(cuda_target: Path) -> list[str]:
|
||||
"""Return all library directories under the CUDA venv as strings."""
|
||||
lib_dirs: list[str] = []
|
||||
|
||||
On Linux, LD_LIBRARY_PATH is only read at process start, so we need to
|
||||
manually load the libraries using ctypes before torch tries to load them.
|
||||
torch_lib = cuda_target / "torch" / "lib"
|
||||
if torch_lib.exists():
|
||||
lib_dirs.append(str(torch_lib))
|
||||
|
||||
nvidia_dir = cuda_target / "nvidia"
|
||||
if nvidia_dir.exists():
|
||||
for pkg_dir in sorted(nvidia_dir.iterdir()):
|
||||
if pkg_dir.is_dir():
|
||||
lib_subdir = pkg_dir / "lib"
|
||||
if lib_subdir.exists():
|
||||
lib_dirs.append(str(lib_subdir))
|
||||
|
||||
return lib_dirs
|
||||
|
||||
|
||||
def _collect_site_packages_cuda_lib_dirs() -> list[str]:
|
||||
"""Return all nvidia/torch lib dirs found in any site-packages directory."""
|
||||
lib_dirs: list[str] = []
|
||||
for sp_dir in _get_site_packages_dirs():
|
||||
torch_lib = sp_dir / "torch" / "lib"
|
||||
if torch_lib.exists():
|
||||
lib_dirs.append(str(torch_lib))
|
||||
nvidia_dir = sp_dir / "nvidia"
|
||||
if nvidia_dir.exists():
|
||||
for pkg_dir in sorted(nvidia_dir.iterdir()):
|
||||
if pkg_dir.is_dir():
|
||||
lib_subdir = pkg_dir / "lib"
|
||||
if lib_subdir.exists():
|
||||
lib_dirs.append(str(lib_subdir))
|
||||
return lib_dirs
|
||||
|
||||
|
||||
def _setup_linux_cuda():
|
||||
"""Set up CUDA libraries on Linux.
|
||||
|
||||
LD_LIBRARY_PATH is read by the dynamic linker only at process start, so
|
||||
preloading individual .so files with ctypes is fragile: if *any* library
|
||||
in the dependency chain (e.g. libcusparseLt.so.0 → libcuda.so.1) is not
|
||||
yet in the search path, the load silently fails, and later imports of torch
|
||||
or torchaudio crash with "cannot open shared object file".
|
||||
|
||||
The reliable fix is to re-exec the current process with LD_LIBRARY_PATH
|
||||
extended to include all cuda lib dirs. After re-exec the dynamic linker
|
||||
finds every CUDA library via the standard search path for the entire
|
||||
lifetime of the process.
|
||||
|
||||
A sentinel env var prevents infinite re-exec loops.
|
||||
"""
|
||||
# Ensure cuda_packages (snap/flatpak) is on sys.path so Python can import torch
|
||||
_get_site_packages_dirs()
|
||||
|
||||
if os.environ.get("BUZZ_CUDA_SETUP_DONE"):
|
||||
logger.info("CUDA setup: already done (sentinel set), proceeding")
|
||||
return
|
||||
|
||||
# Never re-exec inside a multiprocessing worker — it would destroy the IPC pipe.
|
||||
# The parent process already set LD_LIBRARY_PATH before spawning workers.
|
||||
import multiprocessing
|
||||
if multiprocessing.parent_process() is not None:
|
||||
logger.debug("CUDA setup: skipping re-exec in worker subprocess")
|
||||
return
|
||||
|
||||
# Collect lib dirs from snap/flatpak cuda_packages OR regular site-packages
|
||||
cuda_target = _get_cuda_target_dir()
|
||||
if (
|
||||
cuda_target is not None
|
||||
and cuda_target.exists()
|
||||
and _is_abi_compatible(cuda_target)
|
||||
):
|
||||
extra_dirs = _collect_cuda_lib_dirs(cuda_target)
|
||||
else:
|
||||
extra_dirs = _collect_site_packages_cuda_lib_dirs()
|
||||
|
||||
if not extra_dirs:
|
||||
logger.debug("CUDA setup: no nvidia/torch lib dirs found, skipping")
|
||||
return
|
||||
|
||||
current_ld = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
new_ld = ":".join(extra_dirs)
|
||||
if current_ld:
|
||||
new_ld = new_ld + ":" + current_ld
|
||||
os.environ["LD_LIBRARY_PATH"] = new_ld
|
||||
os.environ["BUZZ_CUDA_SETUP_DONE"] = "1"
|
||||
|
||||
# Re-exec the process so the dynamic linker picks up the new LD_LIBRARY_PATH.
|
||||
# We read the original argv from /proc/self/cmdline to preserve the exact
|
||||
# invocation (works for both `python -m buzz` and frozen binaries).
|
||||
try:
|
||||
with open("/proc/self/cmdline", "rb") as f:
|
||||
raw_args = [a.decode("utf-8", errors="replace") for a in f.read().split(b"\x00") if a]
|
||||
executable = raw_args[0]
|
||||
logger.info("CUDA setup: re-execing %s with LD_LIBRARY_PATH=%s", executable, new_ld)
|
||||
os.execv(executable, raw_args)
|
||||
except Exception as e:
|
||||
# Re-exec failed (e.g. AppArmor restriction) — fall back to ctypes preloading
|
||||
logger.warning("CUDA setup: re-exec failed (%s), falling back to ctypes preload", e)
|
||||
_preload_linux_libraries_fallback()
|
||||
|
||||
|
||||
def _preload_linux_libraries_fallback():
|
||||
"""Fallback: preload CUDA .so files with RTLD_GLOBAL when re-exec is not possible."""
|
||||
lib_dirs = _get_nvidia_package_lib_dirs()
|
||||
|
||||
# Libraries to skip - NVBLAS requires special configuration and causes issues
|
||||
skip_patterns = ["libnvblas"]
|
||||
|
||||
loaded_libs = set()
|
||||
|
||||
candidates = []
|
||||
for lib_dir in lib_dirs:
|
||||
if not lib_dir.exists():
|
||||
continue
|
||||
|
||||
# Find all .so files in the directory
|
||||
for lib_file in sorted(lib_dir.glob("*.so*")):
|
||||
if lib_file.name in loaded_libs:
|
||||
continue
|
||||
if lib_file.is_symlink() and not lib_file.exists():
|
||||
continue
|
||||
|
||||
# Skip problematic libraries
|
||||
if any(pattern in lib_file.name for pattern in skip_patterns):
|
||||
if any(p in lib_file.name for p in skip_patterns):
|
||||
continue
|
||||
candidates.append(lib_file)
|
||||
|
||||
loaded: set[str] = set()
|
||||
for pass_num in range(5):
|
||||
newly = 0
|
||||
for lib_file in candidates:
|
||||
if lib_file.name in loaded:
|
||||
continue
|
||||
try:
|
||||
# Use RTLD_GLOBAL so symbols are available to other libraries
|
||||
ctypes.CDLL(str(lib_file), mode=ctypes.RTLD_GLOBAL)
|
||||
loaded_libs.add(lib_file.name)
|
||||
loaded.add(lib_file.name)
|
||||
newly += 1
|
||||
except OSError as e:
|
||||
# Some libraries may have missing dependencies, that's ok
|
||||
pass
|
||||
logger.debug("Preload pass %d: skipping %s: %s", pass_num + 1, lib_file.name, e)
|
||||
logger.info("Preload pass %d: +%d libs (%d total)", pass_num + 1, newly, len(loaded))
|
||||
if newly == 0:
|
||||
break
|
||||
|
||||
|
||||
def setup_cuda_libraries():
|
||||
|
|
@ -118,13 +302,16 @@ def setup_cuda_libraries():
|
|||
or CUDA-dependent libraries are imported.
|
||||
"""
|
||||
system = platform.system()
|
||||
logger.info("CUDA setup: platform=%s, frozen=%s", system, getattr(sys, "frozen", False))
|
||||
|
||||
if system == "Windows":
|
||||
_setup_windows_dll_directories()
|
||||
elif system == "Linux":
|
||||
_preload_linux_libraries()
|
||||
_setup_linux_cuda()
|
||||
# macOS doesn't have CUDA support, so nothing to do
|
||||
|
||||
logger.info("CUDA setup: complete")
|
||||
|
||||
|
||||
# Auto-run setup when this module is imported
|
||||
setup_cuda_libraries()
|
||||
|
|
|
|||
|
|
@ -664,7 +664,10 @@ class FileTranscriberQueueWorker(QObject):
|
|||
|
||||
@pyqtSlot(tuple)
|
||||
def on_task_progress(self, progress: Tuple[int, int]):
|
||||
if self.current.task is not None:
|
||||
if (
|
||||
self.current.task is not None
|
||||
and self.current.task.uid not in self.canceled_tasks
|
||||
):
|
||||
self.task_progress.emit(self.current.task, progress[0] / progress[1])
|
||||
|
||||
def on_task_download_progress(self, fraction_downloaded: float):
|
||||
|
|
@ -673,7 +676,14 @@ class FileTranscriberQueueWorker(QObject):
|
|||
|
||||
@pyqtSlot(list)
|
||||
def on_task_completed(self, segments: List[Segment]):
|
||||
if self.current.task is not None:
|
||||
# A transcriber that raced past cancellation still emits ``completed``.
|
||||
# Dropping it here keeps a canceled task from being overwritten as
|
||||
# completed in the UI and the database.
|
||||
if self.current.task is not None and self.current.task.uid in self.canceled_tasks:
|
||||
logging.debug(
|
||||
"Ignoring completion of canceled task %s", self.current.task.uid
|
||||
)
|
||||
elif self.current.task is not None:
|
||||
self.task_completed.emit(self.current.task, segments)
|
||||
|
||||
if self.speech_path is not None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: buzz\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Éric Duarte <contacto@ericdq.com>\n"
|
||||
"Language-Team: Catalan <jmas@softcatala.org>\n"
|
||||
|
|
@ -777,6 +777,51 @@ msgstr ""
|
|||
"trobat.No s'ha pogut reiniciar la transcripció: no s'ha trobat el "
|
||||
"treballador del transcriptor."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Acceleració GPU Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Voleu instal·lar l'acceleració per a GPU Nvidia?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"L'acceleració GPU per a Nvidia permet a Buzz transcriure àudio "
|
||||
"significativament més ràpid mitjançant CUDA. Per a altres marques de GPU, "
|
||||
"aquesta acceleració no tindrà cap efecte. CUDA no és necessari per executar "
|
||||
"whisper.cpp.\n"
|
||||
"\n"
|
||||
"Això descarregarà i instal·larà PyTorch amb suport CUDA (~8 GB). Buzz s'ha "
|
||||
"de reiniciar després de la instal·lació."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Instal·lar suport GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Ara no"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "Instal·lació completada! Reinicieu Buzz per activar l'acceleració GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Tanca"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "La instal·lació ha fallat: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Enregistrament en directe"
|
||||
|
|
@ -1001,6 +1046,10 @@ msgstr "Comprova si hi ha actualitzacions"
|
|||
msgid "Show logs"
|
||||
msgstr "Mostra els registres"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Instal·lar acceleració CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Estàs al dia!"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Ole Guldberg2 <xalt7x.service@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -771,6 +771,51 @@ msgstr ""
|
|||
"fundet.Kunne ikke genstarte transkription: transkriptionsprocessen blev ikke "
|
||||
"fundet."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU-acceleration"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Installer Nvidia GPU-acceleration?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Installation af GPU-acceleration til Nvidia GPU'er giver Buzz mulighed for "
|
||||
"at transskribere lyd betydeligt hurtigere ved hjælp af CUDA. For andre GPU-"
|
||||
"mærker vil denne acceleration ikke have nogen effekt. CUDA er ikke nødvendig "
|
||||
"for at køre whisper.cpp.\n"
|
||||
"\n"
|
||||
"Dette vil downloade og installere PyTorch med CUDA-understøttelse (~8 GB). "
|
||||
"Buzz skal genstartes efter installation."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Installer GPU-understøttelse"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Ikke nu"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "Installation fuldført! Genstart Buzz for at aktivere GPU-acceleration."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Luk"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Installation mislykkedes: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Live optagelse"
|
||||
|
|
@ -995,6 +1040,10 @@ msgstr "Tjek for opdateringer"
|
|||
msgid "Show logs"
|
||||
msgstr "Vis logfiler"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Installer CUDA-acceleration"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Du er opdateret!"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -781,6 +781,53 @@ msgstr ""
|
|||
"gefunden.Transkription konnte nicht neu gestartet werden: Transkriptions-"
|
||||
"Worker nicht gefunden."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU-Beschleunigung"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Nvidia-GPU-Beschleunigung installieren?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Die Installation der GPU-Beschleunigung für Nvidia GPUs ermöglicht es Buzz, "
|
||||
"Audio mithilfe von CUDA deutlich schneller zu transkribieren. Für andere GPU-"
|
||||
"Marken hat diese Beschleunigung keine Auswirkung. CUDA wird für whisper.cpp "
|
||||
"nicht benötigt.\n"
|
||||
"\n"
|
||||
"Dies lädt PyTorch mit CUDA-Unterstützung herunter und installiert es (~8 "
|
||||
"GB). Buzz muss nach der Installation neu gestartet werden."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "GPU-Unterstützung installieren"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Nicht jetzt"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Installation abgeschlossen! Starte Buzz neu, um die GPU-Beschleunigung zu "
|
||||
"aktivieren."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Installation fehlgeschlagen: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Live-Aufnahme"
|
||||
|
|
@ -1005,6 +1052,10 @@ msgstr "Nach Updates suchen"
|
|||
msgid "Show logs"
|
||||
msgstr "Protokolle anzeigen"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "CUDA-Beschleunigung installieren"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Sie sind auf dem Laufenden!"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -739,6 +739,44 @@ msgstr ""
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr ""
|
||||
|
|
@ -957,6 +995,10 @@ msgstr ""
|
|||
msgid "Show logs"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr ""
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Éric Duarte <contacto@ericdq.com>\n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -805,6 +805,51 @@ msgstr ""
|
|||
"encontrado.No se pudo reiniciar la transcripción: no se encontró el "
|
||||
"trabajador del transcriptor."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Aceleración GPU de Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "¿Instalar la aceleración para GPU Nvidia?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Instalar la aceleración GPU para GPUs Nvidia permite a Buzz transcribir "
|
||||
"audio significativamente más rápido usando CUDA. Para otras marcas de GPU "
|
||||
"esta aceleración no tendrá efecto. CUDA no es necesario para ejecutar "
|
||||
"whisper.cpp.\n"
|
||||
"\n"
|
||||
"Esto descargará e instalará PyTorch con soporte CUDA (~8 GB). Buzz debe "
|
||||
"reiniciarse después de la instalación."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Instalar soporte GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Ahora no"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "¡Instalación completa! Reinicia Buzz para activar la aceleración GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "La instalación falló: {}"
|
||||
|
||||
# automatic translation
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
|
|
@ -1036,6 +1081,10 @@ msgstr "Buscar actualizaciones"
|
|||
msgid "Show logs"
|
||||
msgstr "Mostrar registros"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Instalar aceleración CUDA"
|
||||
|
||||
# automatic translation
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: cherkaba\n"
|
||||
"Language-Team: French <kde-francophone@kde.org>\n"
|
||||
|
|
@ -785,6 +785,52 @@ msgstr ""
|
|||
"Impossible de redémarrer la transcription : worker du transcripteur "
|
||||
"introuvable."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Accélération GPU Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Installer l'accélération GPU Nvidia ?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"L'installation de l'accélération GPU pour les GPU Nvidia permet à Buzz de "
|
||||
"transcrire l'audio beaucoup plus rapidement grâce à CUDA. Pour les GPU "
|
||||
"d'autres marques, cette accélération n'aura aucun effet. CUDA n'est pas "
|
||||
"nécessaire pour utiliser whisper.cpp.\n"
|
||||
"\n"
|
||||
"Cela téléchargera et installera PyTorch avec la prise en charge de CUDA (~8 "
|
||||
"Go). Buzz doit être redémarré après l'installation."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Installer la prise en charge GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Pas maintenant"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Installation terminée ! Redémarrez Buzz pour activer l'accélération GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Échec de l'installation : {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Enregistrement en direct"
|
||||
|
|
@ -1009,6 +1055,10 @@ msgstr "Vérifier les mises à jour"
|
|||
msgid "Show logs"
|
||||
msgstr "Afficher les journaux"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Installer l'accélération CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Vous êtes à jour !"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: buzz\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Language-Team: (Italiano) Albano Battistella <albanobattistella@gmail.com>\n"
|
||||
"Language: it_IT\n"
|
||||
|
|
@ -776,6 +776,52 @@ msgstr ""
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr "Impossibile riavviare la trascrizione: trascrittore non trovato."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Accelerazione GPU Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Installare l'accelerazione GPU Nvidia?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"L'installazione dell'accelerazione GPU per GPU Nvidia consente a Buzz di "
|
||||
"trascrivere l'audio in modo significativamente più veloce tramite CUDA. Per "
|
||||
"altre marche di GPU questa accelerazione non avrà alcun effetto. CUDA non è "
|
||||
"necessario per eseguire whisper.cpp.\n"
|
||||
"\n"
|
||||
"Verranno scaricati e installati PyTorch con supporto CUDA (~8 GB). Buzz deve "
|
||||
"essere riavviato dopo l'installazione."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Installa supporto GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Non ora"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Installazione completata! Riavvia Buzz per abilitare l'accelerazione GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Chiudi"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Installazione fallita: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Registrazione in diretta"
|
||||
|
|
@ -1000,6 +1046,10 @@ msgstr "Controlla gli aggiornamenti"
|
|||
msgid "Show logs"
|
||||
msgstr "Mostra log"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Installa accelerazione CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Il programma è aggiornato!"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: nunawa <71294849+nunawa@users.noreply.github.com>\n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -764,6 +764,53 @@ msgstr ""
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr "文字起こしを再開できませんでした: 文字起こしワーカーが見つかりません。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU アクセラレーション"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Nvidia GPU アクセラレーションをインストールしますか?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Nvidia GPU 向けの GPU アクセラレーションをインストールすると、CUDA を使用し"
|
||||
"て Buzz が音声をはるかに高速に文字起こしできるようになります。他の GPU ブラン"
|
||||
"ドではこのアクセラレーションは効果がありません。CUDA は whisper.cpp の実行に"
|
||||
"は必要ありません。\n"
|
||||
"\n"
|
||||
"CUDA サポート付きの PyTorch がダウンロード・インストールされます(約 8 GB)。"
|
||||
"インストール後、Buzz を再起動する必要があります。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "GPU サポートをインストール"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "後で"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"インストール完了!GPU アクセラレーションを有効にするには Buzz を再起動してく"
|
||||
"ださい。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "閉じる"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "インストールに失敗しました: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "ライブ録音"
|
||||
|
|
@ -988,6 +1035,10 @@ msgstr "アップデートを確認する"
|
|||
msgid "Show logs"
|
||||
msgstr "ログを表示"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "CUDA アクセラレーションをインストール"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "最新の状態です!"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -769,6 +769,51 @@ msgstr ""
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr "Neizdevās sākt atpazīšanu: Kļūda lietotnē, pārstartējiet."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU paātrinājums"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Instalēt Nvidia GPU paātrināšanu"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"GPU paātrinājuma instalēšana Nvidia GPU kartēm ļauj Buzz ievērojami ātrāk "
|
||||
"atpazīt audio, izmantojot CUDA. Citu GPU ražotāju kartēm šim paātrinājumam "
|
||||
"nebūs nekādas ietekmes. CUDA nav nepieciešams, lai darbinātu whisper.cpp.\n"
|
||||
"\n"
|
||||
"Tiks lejupielādēts un instalēts PyTorch ar CUDA atbalstu (~8 GB). Pēc "
|
||||
"instalēšanas Buzz ir jārestartē."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Instalēt GPU atbalstu"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Ne tagad"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Instalēšana pabeigta! Restartējiet Buzz, lai aktivizētu GPU paātrinājumu."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Aizvērt"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Instalēšana neizdevās: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Dzīvā ierakstīšana"
|
||||
|
|
@ -993,6 +1038,10 @@ msgstr "Pārbaudīt atjauninājumus"
|
|||
msgid "Show logs"
|
||||
msgstr "Parādīt sistēmas žurnālu"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Instalēt CUDA paātrinājumu"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Jums ir jaunākā versija!"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Heimen Stoffels <vistausss@fastmail.com>\n"
|
||||
"Language-Team: none\n"
|
||||
|
|
@ -781,6 +781,52 @@ msgstr ""
|
|||
"gevonden.Transcriptie kon niet opnieuw worden gestart: transcriptieproces "
|
||||
"niet gevonden."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU-versnelling"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Nvidia GPU-versnelling installeren?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Het installeren van GPU-versnelling voor Nvidia GPU's stelt Buzz in staat om "
|
||||
"audio aanzienlijk sneller te transcriberen met CUDA. Voor andere GPU-merken "
|
||||
"heeft deze versnelling geen effect. CUDA is niet nodig om whisper.cpp uit te "
|
||||
"voeren.\n"
|
||||
"\n"
|
||||
"Dit downloadt en installeert PyTorch met CUDA-ondersteuning (~8 GB). Buzz "
|
||||
"moet na de installatie opnieuw worden gestart."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "GPU-ondersteuning installeren"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Niet nu"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Installatie voltooid! Herstart Buzz om GPU-versnelling in te schakelen."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Sluiten"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Installatie mislukt: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Live-opname"
|
||||
|
|
@ -1003,6 +1049,10 @@ msgstr "Controleren op updates"
|
|||
msgid "Show logs"
|
||||
msgstr "Logboeken tonen"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "CUDA-versnelling installeren"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "De software is actueel!"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -773,6 +773,52 @@ msgstr ""
|
|||
"transkrypcji.Nie można uruchomić ponownie transkrypcji: nie znaleziono "
|
||||
"procesu transkrypcji."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Akceleracja GPU Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Zainstalować akcelerację GPU Nvidia?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Instalacja akceleracji GPU dla kart Nvidia pozwala Buzz znacznie szybciej "
|
||||
"transkrybować dźwięk przy użyciu CUDA. Dla innych marek GPU ta akceleracja "
|
||||
"nie będzie miała żadnego efektu. CUDA nie jest potrzebna do uruchomienia "
|
||||
"whisper.cpp.\n"
|
||||
"\n"
|
||||
"Spowoduje to pobranie i zainstalowanie PyTorch z obsługą CUDA (~8 GB). Po "
|
||||
"instalacji Buzz musi zostać ponownie uruchomiony."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Zainstaluj obsługę GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Nie teraz"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Instalacja zakończona! Uruchom ponownie Buzz, aby włączyć akcelerację GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Zamknij"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Instalacja nie powiodła się: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Nagrywanie na żywo"
|
||||
|
|
@ -997,6 +1043,10 @@ msgstr "Sprawdź aktualizacje"
|
|||
msgid "Show logs"
|
||||
msgstr "Pokaż logi"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Zainstaluj akcelerację CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Posiadasz najnowszą wersję!"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Buzz\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Paulo Schopf <pschopf@gmail.com>\n"
|
||||
"Language-Team: none\n"
|
||||
|
|
@ -774,6 +774,51 @@ msgstr ""
|
|||
"encontrado.Não foi possível reiniciar a transcrição: trabalhador de "
|
||||
"transcrição não encontrado."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Aceleração GPU Nvidia"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Instalar a aceleração por GPU Nvidia?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Instalar a aceleração GPU para GPUs Nvidia permite ao Buzz transcrever áudio "
|
||||
"significativamente mais rápido usando CUDA. Para outras marcas de GPU, essa "
|
||||
"aceleração não terá efeito. O CUDA não é necessário para executar o "
|
||||
"whisper.cpp.\n"
|
||||
"\n"
|
||||
"Isso irá baixar e instalar o PyTorch com suporte a CUDA (~8 GB). O Buzz deve "
|
||||
"ser reiniciado após a instalação."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Instalar suporte a GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Agora não"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "Instalação concluída! Reinicie o Buzz para ativar a aceleração GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Fechar"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Falha na instalação: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Gravação ao Vivo"
|
||||
|
|
@ -998,6 +1043,10 @@ msgstr "Verificar atualizações"
|
|||
msgid "Show logs"
|
||||
msgstr "Mostrar logs"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Instalar aceleração CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "Você está atualizado!"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -774,6 +774,51 @@ msgstr ""
|
|||
"найден.Не удалось перезапустить транскрипцию: обработчик транскрипции не "
|
||||
"найден."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Ускорение Nvidia GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Установить ускорение Nvidia GPU?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Установка ускорения GPU для видеокарт Nvidia позволяет Buzz значительно "
|
||||
"быстрее транскрибировать аудио с помощью CUDA. Для видеокарт других "
|
||||
"производителей это ускорение не даст эффекта. CUDA не требуется для работы "
|
||||
"whisper.cpp.\n"
|
||||
"\n"
|
||||
"Будет загружен и установлен PyTorch с поддержкой CUDA (~8 ГБ). После "
|
||||
"установки Buzz необходимо перезапустить."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Установить поддержку GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Не сейчас"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "Установка завершена! Перезапустите Buzz, чтобы включить ускорение GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Ошибка установки: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Живая запись"
|
||||
|
|
@ -998,6 +1043,10 @@ msgstr "Проверить обновления"
|
|||
msgid "Show logs"
|
||||
msgstr "Показать журналы"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Установить ускорение CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "У вас актуальная версия!"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: Yevhen Popok <xalt7x.service@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
|
|
@ -771,6 +771,53 @@ msgstr ""
|
|||
"знайдено.Не вдалося перезапустити транскрипцію: обробник транскрипції не "
|
||||
"знайдено."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Апаратне прискорення Nvidia GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "Встановити прискорення Nvidia GPU?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"Встановлення апаратного прискорення GPU для відеокарт Nvidia дозволяє Buzz "
|
||||
"значно швидше транскрибувати аудіо за допомогою CUDA. Для відеокарт інших "
|
||||
"виробників це прискорення не матиме жодного ефекту. CUDA не потрібна для "
|
||||
"роботи whisper.cpp.\n"
|
||||
"\n"
|
||||
"Це завантажить і встановить PyTorch з підтримкою CUDA (~8 ГБ). Після "
|
||||
"встановлення необхідно перезапустити Buzz."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "Встановити підтримку GPU"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "Не зараз"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr ""
|
||||
"Встановлення завершено! Перезапустіть Buzz, щоб увімкнути апаратне "
|
||||
"прискорення GPU."
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "Закрити"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "Помилка встановлення: {}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "Живий запис"
|
||||
|
|
@ -995,6 +1042,10 @@ msgstr "Перевірити оновлення"
|
|||
msgid "Show logs"
|
||||
msgstr "Показати журнали"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "Встановити прискорення CUDA"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "У вас актуальна версія!"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: lamb\n"
|
||||
|
|
@ -755,6 +755,48 @@ msgstr "无法重新开始识别:模型不可用且无法下载。"
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr "无法重新开始识别:未找到识别工作进程。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU 加速"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "是否安装 Nvidia GPU 加速?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"为 Nvidia GPU 安装 GPU 加速后,Buzz 可以使用 CUDA 显著加快音频转录速度。对于"
|
||||
"其他品牌的 GPU,此加速将不起作用。运行 whisper.cpp 不需要 CUDA。\n"
|
||||
"\n"
|
||||
"这将下载并安装支持 CUDA 的 PyTorch(约 8 GB)。安装完成后必须重启 Buzz。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "安装 GPU 支持"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "暂不安装"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "安装完成!请重启 Buzz 以启用 GPU 加速。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "安装失败:{}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "实时录制"
|
||||
|
|
@ -975,6 +1017,10 @@ msgstr "检查更新"
|
|||
msgid "Show logs"
|
||||
msgstr "显示日志"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "安装 CUDA 加速"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "已经是最新版本"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-12 09:03+0300\n"
|
||||
"POT-Creation-Date: 2026-09-12 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:58+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Lamb\n"
|
||||
|
|
@ -756,6 +756,48 @@ msgstr "無法重新開始轉錄:模型不可用且無法下載。"
|
|||
msgid "Could not restart transcription: transcriber worker not found."
|
||||
msgstr "無法重新開始轉錄:找不到轉錄工作器。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Nvidia GPU Acceleration"
|
||||
msgstr "Nvidia GPU 加速"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install Nvidia GPU Acceleration?"
|
||||
msgstr "是否安裝 Nvidia GPU 加速?"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid ""
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz to transcribe audio "
|
||||
"significantly faster using CUDA. For other GPU brands this acceleration will "
|
||||
"have no effect. CUDA is not needed to run whisper.cpp.\n"
|
||||
"\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). Buzz must "
|
||||
"be restarted after installation."
|
||||
msgstr ""
|
||||
"為 Nvidia GPU 安裝 GPU 加速後,Buzz 可以使用 CUDA 大幅提升音訊轉錄速度。對於"
|
||||
"其他品牌的 GPU,此加速將不起作用。執行 whisper.cpp 不需要 CUDA。\n"
|
||||
"\n"
|
||||
"這將下載並安裝支援 CUDA 的 PyTorch(約 8 GB)。安裝完成後必須重新啟動 Buzz。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Install GPU Support"
|
||||
msgstr "安裝 GPU 支援"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Not Now"
|
||||
msgstr "暫不安裝"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation complete! Restart Buzz to enable GPU acceleration."
|
||||
msgstr "安裝完成!請重新啟動 Buzz 以啟用 GPU 加速。"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py buzz/widgets/about_dialog.py
|
||||
msgid "Close"
|
||||
msgstr "關閉"
|
||||
|
||||
#: buzz/widgets/cuda_installer_widget.py
|
||||
msgid "Installation failed: {}"
|
||||
msgstr "安裝失敗:{}"
|
||||
|
||||
#: buzz/widgets/recording_transcriber_widget.py
|
||||
msgid "Live Recording"
|
||||
msgstr "現場錄製"
|
||||
|
|
@ -976,6 +1018,10 @@ msgstr "檢查更新"
|
|||
msgid "Show logs"
|
||||
msgstr "顯示日誌"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "Install CUDA Acceleration"
|
||||
msgstr "安裝 CUDA 加速"
|
||||
|
||||
#: buzz/widgets/about_dialog.py
|
||||
msgid "You're up to date!"
|
||||
msgstr "你是最新的!"
|
||||
|
|
|
|||
|
|
@ -176,12 +176,20 @@ def get_expected_whisper_model_size(size: WhisperModelSize) -> Optional[int]:
|
|||
return WHISPER_MODEL_SIZES.get(size, None)
|
||||
|
||||
class ModelType(enum.Enum):
|
||||
WHISPER = "Whisper"
|
||||
WHISPER_CPP = "Whisper.cpp"
|
||||
HUGGING_FACE = "Hugging Face"
|
||||
FASTER_WHISPER = "Faster Whisper"
|
||||
WHISPER = "OpenAI Whisper"
|
||||
OPEN_AI_WHISPER_API = "OpenAI Whisper API"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
# Migrate old stored values to new display names
|
||||
legacy_map = {
|
||||
"Whisper": cls.WHISPER,
|
||||
}
|
||||
return legacy_map.get(value)
|
||||
|
||||
@property
|
||||
def supports_initial_prompt(self):
|
||||
return self in (
|
||||
|
|
@ -434,7 +442,7 @@ class TranscriptionModel:
|
|||
def __str__(self):
|
||||
match self.model_type:
|
||||
case ModelType.WHISPER:
|
||||
return f"Whisper ({self.whisper_model_size})"
|
||||
return f"OpenAI Whisper ({self.whisper_model_size})"
|
||||
case ModelType.WHISPER_CPP:
|
||||
return f"Whisper.cpp ({self.whisper_model_size})"
|
||||
case ModelType.HUGGING_FACE:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ Shared helpers for installing Python packages at runtime via pip.
|
|||
This module centralises the logic needed to run ``pip`` reliably across the
|
||||
environments Buzz ships in: PyInstaller frozen bundles, Snap/Flatpak sandboxes,
|
||||
uv/virtualenv installs and plain system Python. It is used by the plugin system
|
||||
to install plugin-declared dependencies, and is intended to be the single source
|
||||
of truth for runtime pip handling (the CUDA installer on the ``unbundle-cuda``
|
||||
branch contains the original copy of this logic and should be unified with this
|
||||
module when that branch merges).
|
||||
to install plugin-declared dependencies.
|
||||
|
||||
Note: the CUDA installer (``buzz/cuda_manager.py``) no longer shares this code.
|
||||
It installs into a private venv it creates itself, because neither pip nor
|
||||
ensurepip exists in the snap's Python, which ``get_pip_cmd`` below cannot
|
||||
recover from — plugin dependency installs hit the same wall there and would
|
||||
benefit from the same treatment.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ class Settings:
|
|||
|
||||
PLUGINS = "plugins"
|
||||
|
||||
CUDA_PROMPT_SHOWN = "cuda/prompt-shown"
|
||||
CUDA_INSTALL_DECLINED = "cuda/install-declined"
|
||||
|
||||
def get_user_identifier(self) -> str:
|
||||
user_id = self.value(self.Key.USER_IDENTIFIER, "")
|
||||
if not user_id:
|
||||
|
|
|
|||
17
buzz/transcriber/cuda_device.py
Normal file
17
buzz/transcriber/cuda_device.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import logging
|
||||
|
||||
|
||||
def cuda_works() -> bool:
|
||||
"""Return True only if CUDA is available AND a kernel actually executes without error."""
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
return False
|
||||
if not torch.cuda.is_available() or not torch.version.cuda:
|
||||
return False
|
||||
try:
|
||||
torch.zeros(1, device="cuda")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.debug("CUDA smoke test failed, falling back to CPU: %s", e)
|
||||
return False
|
||||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
import platform
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import wave
|
||||
import time
|
||||
import tempfile
|
||||
|
|
@ -16,6 +17,7 @@ from buzz import cuda_setup # noqa: F401
|
|||
|
||||
import torch
|
||||
import numpy as np
|
||||
from buzz.transcriber.cuda_device import cuda_works
|
||||
import sounddevice
|
||||
from sounddevice import PortAudioError
|
||||
from openai import OpenAI
|
||||
|
|
@ -78,6 +80,8 @@ class RecordingTranscriber(QObject):
|
|||
)
|
||||
self.process = None
|
||||
self._stderr_lines: list[bytes] = []
|
||||
self.use_cuda = False
|
||||
self._whisper_server_port: int = 3003
|
||||
|
||||
def start(self):
|
||||
self.is_running = True
|
||||
|
|
@ -165,7 +169,8 @@ class RecordingTranscriber(QObject):
|
|||
model_path = self.model_path
|
||||
|
||||
force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
|
||||
use_cuda = torch.cuda.is_available() and force_cpu == "false"
|
||||
use_cuda = cuda_works() and force_cpu == "false"
|
||||
self.use_cuda = use_cuda
|
||||
|
||||
if torch.cuda.is_available():
|
||||
logging.debug(f"CUDA version detected: {torch.version.cuda}")
|
||||
|
|
@ -191,12 +196,11 @@ class RecordingTranscriber(QObject):
|
|||
model_root_dir = os.getenv("BUZZ_MODEL_ROOT", model_root_dir)
|
||||
|
||||
device = "auto"
|
||||
if torch.cuda.is_available() and torch.version.cuda < "12":
|
||||
logging.debug("Unsupported CUDA version (<12), using CPU")
|
||||
if not cuda_works():
|
||||
logging.debug("CUDA not available or not functional, using CPU")
|
||||
device = "cpu"
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
logging.debug("CUDA is not available, using CPU")
|
||||
elif torch.version.cuda < "12":
|
||||
logging.debug("Unsupported CUDA version (<12), using CPU")
|
||||
device = "cpu"
|
||||
|
||||
if force_cpu != "false":
|
||||
|
|
@ -261,7 +265,7 @@ class RecordingTranscriber(QObject):
|
|||
initial_prompt=initial_prompt,
|
||||
temperature=DEFAULT_WHISPER_TEMPERATURE,
|
||||
no_speech_threshold=0.4,
|
||||
fp16=False,
|
||||
fp16=self.use_cuda,
|
||||
)
|
||||
|
||||
def _transcribe_faster_whisper(self, samples, model, initial_prompt):
|
||||
|
|
@ -467,10 +471,15 @@ class RecordingTranscriber(QObject):
|
|||
if not os.path.exists(server_path):
|
||||
server_path = os.path.join(APP_BASE_DIR, "buzz", "whisper_cpp", server_executable)
|
||||
|
||||
# Pick a free port to avoid conflicts when multiple tests or instances run concurrently
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as _s:
|
||||
_s.bind(('127.0.0.1', 0))
|
||||
self._whisper_server_port = _s.getsockname()[1]
|
||||
|
||||
cmd = [
|
||||
server_path,
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "3003",
|
||||
"--port", str(self._whisper_server_port),
|
||||
"--inference-path", "/audio/transcriptions",
|
||||
"--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8) // 2)),
|
||||
"--model", self.model_path,
|
||||
|
|
@ -549,7 +558,7 @@ class RecordingTranscriber(QObject):
|
|||
|
||||
self.openai_client = OpenAI(
|
||||
api_key="not-used",
|
||||
base_url="http://127.0.0.1:3003",
|
||||
base_url=f"http://127.0.0.1:{self._whisper_server_port}",
|
||||
timeout=30.0,
|
||||
max_retries=0
|
||||
)
|
||||
|
|
|
|||
|
|
@ -196,8 +196,8 @@ class WhisperCpp:
|
|||
return cmd
|
||||
|
||||
@staticmethod
|
||||
def _run_whisper(cmd) -> int:
|
||||
"""Run whisper-cli subprocess and return the return code."""
|
||||
def _run_whisper(cmd) -> tuple:
|
||||
"""Run whisper-cli subprocess, returning (return code, tail of stderr)."""
|
||||
if sys.platform == "win32":
|
||||
si = subprocess.STARTUPINFO()
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
|
@ -223,11 +223,14 @@ class WhisperCpp:
|
|||
errors="replace",
|
||||
)
|
||||
|
||||
stderr_tail = []
|
||||
for line in iter(process.stderr.readline, ''):
|
||||
sys.stderr.write(line)
|
||||
stderr_tail.append(line)
|
||||
del stderr_tail[:-20]
|
||||
|
||||
process.wait()
|
||||
return process.returncode
|
||||
return process.returncode, "".join(stderr_tail)
|
||||
|
||||
@staticmethod
|
||||
def _read_json_output(file_to_process: str) -> dict:
|
||||
|
|
@ -380,11 +383,14 @@ class WhisperCpp:
|
|||
vad_enabled = os.path.exists(vad_model_path)
|
||||
|
||||
cmd = WhisperCpp._build_command(task, file_to_process, language, vad_enabled)
|
||||
return_code = WhisperCpp._run_whisper(cmd)
|
||||
return_code, stderr_tail = WhisperCpp._run_whisper(cmd)
|
||||
|
||||
if return_code != 0:
|
||||
WhisperCpp._cleanup_files(temp_file, None)
|
||||
raise Exception(f"whisper-cli failed with return code {return_code}")
|
||||
message = f"whisper-cli failed with return code {return_code}"
|
||||
if stderr_tail.strip():
|
||||
message = f"{message}\n{stderr_tail.strip()}"
|
||||
raise Exception(message)
|
||||
|
||||
try:
|
||||
result = WhisperCpp._read_json_output(file_to_process)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ from buzz import cuda_setup # noqa: F401
|
|||
|
||||
import torch
|
||||
import platform
|
||||
|
||||
from buzz.transcriber.cuda_device import cuda_works
|
||||
import subprocess
|
||||
from platformdirs import user_cache_dir
|
||||
from multiprocessing.connection import Connection
|
||||
from threading import Thread
|
||||
from threading import Lock, Thread
|
||||
from typing import Optional, List
|
||||
|
||||
import tqdm
|
||||
|
|
@ -116,6 +118,12 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
self.recv_pipe = None
|
||||
self.send_pipe = None
|
||||
self.error_message = None
|
||||
# Guards the ``stopped`` / ``started_process`` pair. Without it, a
|
||||
# stop() landing while ``current_process.start()`` is still running
|
||||
# (slow on Windows, which spawns a fresh interpreter) sees
|
||||
# ``started_process`` still False, terminates nothing, and the
|
||||
# transcription runs to completion despite being canceled.
|
||||
self.process_lock = Lock()
|
||||
|
||||
def transcribe(self) -> List[Segment]:
|
||||
time_started = datetime.datetime.now()
|
||||
|
|
@ -131,10 +139,20 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
self.current_process = multiprocessing.Process(
|
||||
target=self.transcribe_whisper, args=(self.send_pipe, self.transcription_task)
|
||||
)
|
||||
if not self.stopped:
|
||||
with self.process_lock:
|
||||
canceled_before_start = self.stopped
|
||||
if not canceled_before_start:
|
||||
self.current_process.start()
|
||||
self.started_process = True
|
||||
|
||||
if canceled_before_start:
|
||||
for pipe in (self.send_pipe, self.recv_pipe):
|
||||
try:
|
||||
pipe.close()
|
||||
except OSError:
|
||||
pass
|
||||
raise Exception("Transcription was canceled")
|
||||
|
||||
self.read_line_thread = Thread(target=self.read_line, args=(self.recv_pipe,))
|
||||
self.read_line_thread.start()
|
||||
|
||||
|
|
@ -326,12 +344,11 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
||||
|
||||
device = "auto"
|
||||
if torch.cuda.is_available() and torch.version.cuda < "12":
|
||||
logging.debug("Unsupported CUDA version (<12), using CPU")
|
||||
if not cuda_works():
|
||||
logging.debug("CUDA not available or not functional, using CPU")
|
||||
device = "cpu"
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
logging.debug("CUDA is not available, using CPU")
|
||||
elif torch.version.cuda < "12":
|
||||
logging.debug("Unsupported CUDA version (<12), using CPU")
|
||||
device = "cpu"
|
||||
|
||||
if force_cpu != "false":
|
||||
|
|
@ -397,7 +414,7 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
if force_cpu != "false":
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
||||
|
||||
use_cuda = torch.cuda.is_available() and force_cpu == "false"
|
||||
use_cuda = cuda_works() and force_cpu == "false"
|
||||
|
||||
device = "cuda" if use_cuda else "cpu"
|
||||
|
||||
|
|
@ -423,7 +440,7 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
temperature=DEFAULT_WHISPER_TEMPERATURE,
|
||||
initial_prompt=task.transcription_options.initial_prompt,
|
||||
no_speech_threshold=0.4,
|
||||
fp16=False,
|
||||
fp16=use_cuda,
|
||||
)
|
||||
return [
|
||||
Segment(
|
||||
|
|
@ -443,7 +460,7 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
temperature=task.transcription_options.temperature,
|
||||
initial_prompt=task.transcription_options.initial_prompt,
|
||||
verbose=False,
|
||||
fp16=False,
|
||||
fp16=use_cuda,
|
||||
)
|
||||
segments = result.get("segments")
|
||||
return [
|
||||
|
|
@ -457,9 +474,13 @@ class WhisperFileTranscriber(FileTranscriber):
|
|||
]
|
||||
|
||||
def stop(self):
|
||||
with self.process_lock:
|
||||
self.stopped = True
|
||||
# Read under the lock so a start() in progress has finished (and
|
||||
# published the pid) before we decide there is nothing to kill.
|
||||
started_process = self.started_process
|
||||
|
||||
if self.started_process:
|
||||
if started_process:
|
||||
# Kill the whisper-cli subprocess the worker spawned first. The
|
||||
# worker's own terminate() below does not reach it, so it would
|
||||
# otherwise keep running (orphaned) after Stop / app close.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from buzz import cuda_setup # noqa: F401
|
|||
|
||||
import torch
|
||||
import requests
|
||||
from buzz.transcriber.cuda_device import cuda_works
|
||||
from typing import Union
|
||||
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline, BitsAndBytesConfig
|
||||
from transformers.pipelines import AutomaticSpeechRecognitionPipeline
|
||||
|
|
@ -289,7 +290,7 @@ class TransformersTranscriber:
|
|||
):
|
||||
"""Transcribe using Whisper model."""
|
||||
force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
|
||||
use_cuda = torch.cuda.is_available() and force_cpu == "false"
|
||||
use_cuda = cuda_works() and force_cpu == "false"
|
||||
device = "cuda" if use_cuda else "cpu"
|
||||
torch_dtype = torch.float16 if use_cuda else torch.float32
|
||||
|
||||
|
|
@ -793,7 +794,7 @@ class TransformersTranscriber:
|
|||
from transformers import Wav2Vec2ForCTC, AutoProcessor as MMSAutoProcessor
|
||||
|
||||
force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
|
||||
use_cuda = torch.cuda.is_available() and force_cpu == "false"
|
||||
use_cuda = cuda_works() and force_cpu == "false"
|
||||
device = "cuda" if use_cuda else "cpu"
|
||||
|
||||
# Map language code to ISO 639-3 for MMS
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from buzz.__version__ import VERSION
|
|||
from buzz.widgets.icon import BUZZ_ICON_PATH, BUZZ_LARGE_ICON_PATH
|
||||
from buzz.locale import _
|
||||
from buzz.settings.settings import APP_NAME
|
||||
from buzz.widgets.cuda_installer_widget import CudaInstallerDialog
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
|
|
@ -84,9 +85,13 @@ class AboutDialog(QDialog):
|
|||
self.show_logs_button = QPushButton(_("Show logs"), self)
|
||||
self.show_logs_button.clicked.connect(self.on_click_show_logs)
|
||||
|
||||
self.cuda_installer_button = QPushButton(_("Install CUDA Acceleration"), self)
|
||||
self.cuda_installer_button.clicked.connect(self.on_click_cuda_installer)
|
||||
|
||||
button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton(QDialogButtonBox.StandardButton.Close), self
|
||||
)
|
||||
button_box.button(QDialogButtonBox.StandardButton.Close).setText(_("Close"))
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
|
|
@ -95,6 +100,7 @@ class AboutDialog(QDialog):
|
|||
layout.addWidget(version_label)
|
||||
layout.addWidget(self.check_updates_button)
|
||||
layout.addWidget(self.show_logs_button)
|
||||
layout.addWidget(self.cuda_installer_button)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
|
@ -109,6 +115,12 @@ class AboutDialog(QDialog):
|
|||
log_dir = user_log_dir(appname="Buzz")
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(log_dir))
|
||||
|
||||
def on_click_cuda_installer(self):
|
||||
dialog = CudaInstallerDialog(self)
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
|
||||
def on_latest_release_reply(self, reply: QNetworkReply):
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
response = json.loads(reply.readAll().data())
|
||||
|
|
|
|||
149
buzz/widgets/cuda_installer_widget.py
Normal file
149
buzz/widgets/cuda_installer_widget.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
Dialog for installing GPU (CUDA) acceleration at runtime.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6.QtCore import QRunnable, QThreadPool, Qt, pyqtSignal, QObject
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QProgressBar,
|
||||
QTextEdit,
|
||||
QWidget,
|
||||
QStyle,
|
||||
)
|
||||
|
||||
from buzz.locale import _
|
||||
from buzz.widgets.icon import BUZZ_ICON_PATH
|
||||
from PyQt6.QtGui import QIcon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _InstallSignals(QObject):
|
||||
progress = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
error = pyqtSignal(str)
|
||||
|
||||
|
||||
class _InstallWorker(QRunnable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.signals = _InstallSignals()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from buzz.cuda_manager import install_cuda
|
||||
install_cuda(progress_callback=self.signals.progress.emit)
|
||||
self.signals.finished.emit()
|
||||
except Exception as exc:
|
||||
logger.exception("CUDA installation failed")
|
||||
self.signals.error.emit(str(exc))
|
||||
|
||||
|
||||
class CudaInstallerDialog(QDialog):
|
||||
"""Dialog that offers to install CUDA GPU acceleration."""
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(_("Nvidia GPU Acceleration"))
|
||||
self.setWindowIcon(QIcon(BUZZ_ICON_PATH))
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(400)
|
||||
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(12)
|
||||
|
||||
header = QLabel(_("Install Nvidia GPU Acceleration?"))
|
||||
header.setStyleSheet("font-size: 15px; font-weight: bold;")
|
||||
layout.addWidget(header)
|
||||
|
||||
desc = QLabel(
|
||||
_(
|
||||
"Installing GPU acceleration for Nvidia GPUS allows Buzz "
|
||||
"to transcribe audio significantly faster using CUDA. "
|
||||
"For other GPU brands this acceleration will have no effect. "
|
||||
"CUDA is not needed to run whisper.cpp.\n\n"
|
||||
"This will download and install PyTorch with CUDA support (~8 GB). "
|
||||
"Buzz must be restarted after installation."
|
||||
)
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
layout.addWidget(desc)
|
||||
|
||||
self.log_view = QTextEdit()
|
||||
self.log_view.setReadOnly(True)
|
||||
self.log_view.setMinimumHeight(120)
|
||||
self.log_view.setVisible(False)
|
||||
layout.addWidget(self.log_view, 1) # stretch factor so it expands with window
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 0) # indeterminate
|
||||
self.progress_bar.setVisible(False)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
self.status_label = QLabel("")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.install_button = QPushButton(_("Install GPU Support"))
|
||||
self.install_button.setIcon(
|
||||
self.style().standardIcon(QStyle.StandardPixmap.SP_DialogApplyButton)
|
||||
)
|
||||
self.install_button.setDefault(True)
|
||||
self.install_button.setAutoDefault(True)
|
||||
self.install_button.clicked.connect(self._on_install_clicked)
|
||||
|
||||
self.decline_button = QPushButton(_("Not Now"))
|
||||
self.decline_button.setAutoDefault(False)
|
||||
self.decline_button.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.decline_button)
|
||||
button_layout.addWidget(self.install_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _on_install_clicked(self):
|
||||
self.install_button.setEnabled(False)
|
||||
self.decline_button.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.log_view.setVisible(True)
|
||||
self.status_label.setVisible(False)
|
||||
|
||||
worker = _InstallWorker()
|
||||
worker.signals.progress.connect(self._on_progress)
|
||||
worker.signals.finished.connect(self._on_finished)
|
||||
worker.signals.error.connect(self._on_error)
|
||||
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
def _on_progress(self, message: str):
|
||||
self.log_view.append(message)
|
||||
self.log_view.verticalScrollBar().setValue(
|
||||
self.log_view.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
def _on_finished(self):
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_label.setVisible(True)
|
||||
self.status_label.setText(
|
||||
_("Installation complete! Restart Buzz to enable GPU acceleration.")
|
||||
)
|
||||
self.install_button.setText(_("Close"))
|
||||
self.install_button.setIcon(QIcon())
|
||||
self.install_button.setEnabled(True)
|
||||
self.install_button.clicked.disconnect()
|
||||
self.install_button.clicked.connect(self.accept)
|
||||
|
||||
def _on_error(self, error: str):
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_label.setVisible(True)
|
||||
self.status_label.setText(_("Installation failed: {}").format(error))
|
||||
self.install_button.setEnabled(True)
|
||||
self.decline_button.setEnabled(True)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QPalette
|
||||
from PyQt6.QtCore import QSize
|
||||
from PyQt6.QtSvg import QSvgRenderer
|
||||
import os
|
||||
from buzz.assets import APP_BASE_DIR
|
||||
|
||||
class PresentationIcon:
|
||||
"Icons for presentation window controls"
|
||||
def __init__(self, parent, svg_path: str, color: str = None):
|
||||
self.parent = parent
|
||||
self.svg_path = svg_path
|
||||
self.color = color or self.get_default_color()
|
||||
|
||||
|
||||
def get_default_color(self) -> str:
|
||||
"""Get default icon color based on theme"""
|
||||
palette = self.parent.palette()
|
||||
is_dark = palette.window().color().black() > 127
|
||||
|
||||
return "#EEE" if is_dark else "#555"
|
||||
|
||||
def get_icon(self) -> QIcon:
|
||||
"""Load SVG icon and return as QIcon"""
|
||||
#Load from asset first
|
||||
full_path = os.path.join(APP_BASE_DIR, "assets", "icons", os.path.basename(self.svg_path))
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
pixmap = QPixmap(24, 24)
|
||||
pixmap.fill(self.color)
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
#Load SVG
|
||||
renderer = QSvgRenderer(full_path)
|
||||
pixmap = QPixmap(24, 24)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -198,6 +198,9 @@ class MainWindow(QMainWindow):
|
|||
#Initialize and run update checker
|
||||
self._init_update_checker()
|
||||
|
||||
#Offer CUDA installation on Windows if not done before
|
||||
self._maybe_show_cuda_prompt()
|
||||
|
||||
def on_preferences_changed(self, preferences: Preferences):
|
||||
self.preferences = preferences
|
||||
self.save_preferences(preferences)
|
||||
|
|
@ -637,6 +640,38 @@ class MainWindow(QMainWindow):
|
|||
self._update_info = update_info
|
||||
self.toolbar.set_update_available(True)
|
||||
|
||||
def _maybe_show_cuda_prompt(self):
|
||||
"""On first launch (Windows/Linux), offer CUDA installation if an NVIDIA GPU is present."""
|
||||
from buzz import cuda_manager
|
||||
is_nvidia_gpu_present = cuda_manager.is_nvidia_gpu_present()
|
||||
|
||||
logging.debug(f"Nvidia GPU detected: {is_nvidia_gpu_present}")
|
||||
|
||||
if not is_nvidia_gpu_present:
|
||||
return
|
||||
if not cuda_manager.should_offer_cuda_prompt():
|
||||
return
|
||||
# The packaged builds all ship CPU-only torch, so this only ever fires
|
||||
# for a pip or source install where the user set up CUDA themselves.
|
||||
# Offering them a second, shadowing torch would be worse than useless.
|
||||
if cuda_manager.is_cuda_torch_installed():
|
||||
logging.debug("CUDA torch already available; not offering the install")
|
||||
return
|
||||
if self.settings.value(Settings.Key.CUDA_PROMPT_SHOWN, False):
|
||||
return
|
||||
self.settings.set_value(Settings.Key.CUDA_PROMPT_SHOWN, True)
|
||||
|
||||
from PyQt6.QtCore import QTimer
|
||||
from buzz.widgets.cuda_installer_widget import CudaInstallerDialog
|
||||
|
||||
def _show():
|
||||
dialog = CudaInstallerDialog(self)
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
|
||||
QTimer.singleShot(500, _show)
|
||||
|
||||
def on_update_action_triggered(self):
|
||||
"""Called when user clicks the update action in toolbar."""
|
||||
if self._update_info is None:
|
||||
|
|
|
|||
|
|
@ -676,7 +676,7 @@ class RecordingTranscriberWidget(QWidget):
|
|||
|
||||
self.transcription_thread.started.connect(self.transcriber.start)
|
||||
self.transcription_thread.finished.connect(
|
||||
self.transcription_thread.deleteLater
|
||||
self._on_transcription_thread_finished
|
||||
)
|
||||
self.transcription_thread.finished.connect(
|
||||
lambda: setattr(self, 'transcription_thread', None)
|
||||
|
|
@ -1120,6 +1120,7 @@ class RecordingTranscriberWidget(QWidget):
|
|||
self.reset_record_button()
|
||||
self.set_recording_status_stopped()
|
||||
self.reset_recording_amplitude_listener()
|
||||
self.transcription_stopped.emit()
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"",
|
||||
|
|
@ -1166,7 +1167,7 @@ class RecordingTranscriberWidget(QWidget):
|
|||
super().closeEvent(event)
|
||||
return
|
||||
|
||||
if self.current_status == self.RecordingStatus.RECORDING:
|
||||
if self.current_status == self.RecordingStatus.RECORDING or self.transcription_thread is not None:
|
||||
# Defer the close until the transcription thread finishes to avoid
|
||||
# blocking the GUI thread with a synchronous wait.
|
||||
event.ignore()
|
||||
|
|
@ -1195,8 +1196,28 @@ class RecordingTranscriberWidget(QWidget):
|
|||
self._do_close()
|
||||
super().closeEvent(event)
|
||||
|
||||
def _on_close_transcriber_finished(self):
|
||||
def _cleanup_transcription_thread(self):
|
||||
"""Wait for the OS thread to exit, schedule deletion, and clear the reference.
|
||||
|
||||
QThread::finished is emitted before the OS thread fully exits, so we
|
||||
must call thread.wait() before dropping the last reference or calling
|
||||
deleteLater() — otherwise Qt destroys the C++ object while the thread
|
||||
is still in its final cleanup steps.
|
||||
"""
|
||||
thread = self.transcription_thread
|
||||
self.transcription_thread = None
|
||||
if thread is not None:
|
||||
try:
|
||||
thread.wait()
|
||||
thread.deleteLater()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _on_transcription_thread_finished(self):
|
||||
self._cleanup_transcription_thread()
|
||||
|
||||
def _on_close_transcriber_finished(self):
|
||||
self._cleanup_transcription_thread()
|
||||
self.close()
|
||||
|
||||
def _do_close(self):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ Speech recognition requires large amount of computation, so one option is to try
|
|||
|
||||
Buzz also supports using OpenAI API to do speech recognition on a remote server. To use this feature you need to set OpenAI API key in Preferences. See [Preferences](https://chidiwilliams.github.io/buzz/docs/preferences) section for more details.
|
||||
|
||||
If you are on Buzz version `1.4.6` or later and have a Nvidia GPU, go to `Help -> About Buzz` and install CUDA Acceleration.
|
||||
|
||||
### 3. How to record system audio?
|
||||
|
||||
To transcribe system audio you need to configure virtual audio device and connect output from the applications you want to transcribe to this virtual speaker. After that you can select it as source in the Buzz. See [Usage](https://chidiwilliams.github.io/buzz/docs/usage/live_recording) section for more details.
|
||||
|
|
|
|||
|
|
@ -51,3 +51,8 @@ On Linux install system dependencies you may be missing
|
|||
sudo apt-get install --no-install-recommends libyaml-dev libtbb-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 ffmpeg
|
||||
```
|
||||
On versions prior to Ubuntu 24.04 install `sudo apt-get install --no-install-recommends libegl1-mesa`
|
||||
|
||||
|
||||
## CUDA GPU Acceleration
|
||||
|
||||
Since version `1.4.6` Nvidia CUDA GPU acceleration is no longer included in base package of the buzz and has to be installed separately on Linux and Windows. You will get a prompt to do so if Nvidia GPU is detected, but you can also install CUDA support manually by going to `Help -> About Buzz -> Install CUDA Acceleration`. CUDA is not needed for `whisper.cpp` and if you do not have Nvidia GPU.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ OutputDir={#OutputDir}
|
|||
OutputBaseFilename={#AppName}-{#AppVersion}-windows
|
||||
SetupIconFile={#AppIconPath}
|
||||
UninstallDisplayIcon={app}\{#AppExeName}
|
||||
DiskSpanning=yes
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
|
|
|||
|
|
@ -30,15 +30,14 @@ dependencies = [
|
|||
"bitsandbytes>=0.45.0",
|
||||
"polib>=1.2.0,<2",
|
||||
"srt-equalizer>=0.1.10,<0.2",
|
||||
# For ARM macOS (arm64) - use latest CPU-only versions from PyPI
|
||||
"torch==2.8.0; sys_platform == 'darwin'",
|
||||
"torchaudio==2.8.0; sys_platform == 'darwin'",
|
||||
"ctranslate2>=4.6.2,<5; sys_platform == 'darwin'",
|
||||
# For Linux/Windows - use CUDA versions from the pytorch index
|
||||
# (routed via tool.uv.sources below to ensure CUDA 12.9 compatibility)
|
||||
"torch==2.8.0; sys_platform != 'darwin'",
|
||||
"torchaudio==2.8.0; sys_platform != 'darwin'",
|
||||
# For Linux/Windows - use CPU versions from pytorch index
|
||||
"ctranslate2>=4.6.2,<5; sys_platform != 'darwin'",
|
||||
"torch==2.8.0+cpu; sys_platform != 'darwin'",
|
||||
"torchaudio==2.8.0+cpu; sys_platform != 'darwin'",
|
||||
# For ARM Mac - use regular PyPI torch
|
||||
"torch==2.8.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
||||
"torchaudio==2.8.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
||||
# faster whisper need cudnn 9
|
||||
"nvidia-cudnn-cu12>=9,<10; sys_platform != 'darwin'",
|
||||
# CUDA runtime libraries are provided by torch dependencies, no need to specify explicitly
|
||||
|
|
@ -131,26 +130,15 @@ default-groups = [
|
|||
|
||||
[tool.uv.sources]
|
||||
torch = [
|
||||
{ index = "PyPI", marker = "sys_platform == 'darwin'" },
|
||||
{ index = "pytorch-cu129", marker = "sys_platform != 'darwin'" },
|
||||
{ index = "pytorch-cpu", marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
torchaudio = [
|
||||
{ index = "PyPI", marker = "sys_platform == 'darwin'" },
|
||||
{ index = "pytorch-cu129", marker = "sys_platform != 'darwin'" },
|
||||
{ index = "pytorch-cpu", marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "nvidia"
|
||||
url = "https://pypi.nvidia.com/"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cu129"
|
||||
url = "https://download.pytorch.org/whl/cu129"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "PyPI"
|
||||
url = "https://pypi.org/simple/"
|
||||
default = true
|
||||
name = "pytorch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
explicit = true
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
<li>Plugin system with plugins like AI summary generation and automated transcript resizing or noise filter</li>
|
||||
<li>Speaker identification improvements</li>
|
||||
<li>Appimage support for Linux</li>
|
||||
<li>Minor fixes to model loading and initial prompt processing</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ base: core24
|
|||
version: git
|
||||
title: Buzz
|
||||
summary: Buzz, offline audio transcription and translation
|
||||
website: https://buzzcaptions.com
|
||||
website: https://github.com/chidiwilliams/buzz
|
||||
source-code: https://github.com/chidiwilliams/buzz
|
||||
issues: https://github.com/chidiwilliams/buzz/issues
|
||||
contact: https://github.com/chidiwilliams
|
||||
|
|
|
|||
539
tests/cuda_manager_test.py
Normal file
539
tests/cuda_manager_test.py
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
|
||||
import pytest
|
||||
|
||||
from buzz.cuda_manager import (
|
||||
CUDA_INDEX_URL,
|
||||
CUDA_NVIDIA_PACKAGES_LINUX,
|
||||
get_cuda_env_dir,
|
||||
get_cuda_env_site_packages,
|
||||
get_cuda_root_dir,
|
||||
is_cuda_torch_installed,
|
||||
is_flatpak,
|
||||
is_nvidia_gpu_present,
|
||||
is_snap,
|
||||
should_offer_cuda_prompt,
|
||||
_cleanup_old_cuda_packages,
|
||||
_create_cuda_env,
|
||||
_find_stale_cuda_dirs,
|
||||
_find_uv,
|
||||
get_python_version,
|
||||
_get_base_python,
|
||||
_interpreter_version_matches,
|
||||
_run_install,
|
||||
_subprocess_hide_window_kwargs,
|
||||
_venv_python,
|
||||
install_cuda,
|
||||
)
|
||||
|
||||
|
||||
class TestIsSnap:
|
||||
def test_returns_true_when_snap_env_set(self, monkeypatch):
|
||||
monkeypatch.setenv("SNAP", "/snap/buzz/current")
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
assert is_snap() is True
|
||||
|
||||
def test_returns_false_when_snap_env_not_set(self, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
assert is_snap() is False
|
||||
|
||||
|
||||
def test_returns_false_for_an_unrelated_snap(self, monkeypatch):
|
||||
# A snap-packaged tool (e.g. snap-installed uv) launching Buzz exports
|
||||
# SNAP*, but its user data is not ours to install into.
|
||||
monkeypatch.setenv("SNAP", "/snap/astral-uv/1682")
|
||||
monkeypatch.setenv("SNAP_NAME", "astral-uv")
|
||||
assert is_snap() is False
|
||||
|
||||
|
||||
class TestFindStaleCudaDirs:
|
||||
def test_includes_the_env_when_it_exists(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
env_dir.mkdir()
|
||||
assert _find_stale_cuda_dirs(env_dir) == [env_dir]
|
||||
|
||||
def test_includes_the_legacy_target_dir(self, tmp_path, monkeypatch):
|
||||
# Installs made before the switch to a private venv used --target
|
||||
# cuda_packages; several gigabytes that nothing else would remove.
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
legacy = tmp_path / "cuda_packages"
|
||||
legacy.mkdir()
|
||||
assert _find_stale_cuda_dirs(tmp_path / "cuda_env") == [legacy]
|
||||
|
||||
def test_empty_when_nothing_installed(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
assert _find_stale_cuda_dirs(tmp_path / "cuda_env") == []
|
||||
|
||||
def test_includes_other_snap_revisions(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SNAP", "/snap/buzz/current")
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
current = tmp_path / "1682"
|
||||
old = tmp_path / "1662"
|
||||
for revision in (current, old):
|
||||
(revision / "cuda_env").mkdir(parents=True)
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(current))
|
||||
|
||||
stale = _find_stale_cuda_dirs(current / "cuda_env")
|
||||
|
||||
assert set(stale) == {current / "cuda_env", old / "cuda_env"}
|
||||
|
||||
def test_skips_the_current_symlink(self, tmp_path, monkeypatch):
|
||||
# ~/snap/<name>/current symlinks to the active revision; following it
|
||||
# would list the same directory twice under two names.
|
||||
monkeypatch.setenv("SNAP", "/snap/buzz/current")
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
revision = tmp_path / "1682"
|
||||
(revision / "cuda_env").mkdir(parents=True)
|
||||
(tmp_path / "current").symlink_to(revision)
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(revision))
|
||||
|
||||
assert _find_stale_cuda_dirs(revision / "cuda_env") == [revision / "cuda_env"]
|
||||
|
||||
|
||||
class TestCleanupOldCudaPackages:
|
||||
def test_removes_stale_directory(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
(env_dir / "torch").mkdir(parents=True)
|
||||
|
||||
_cleanup_old_cuda_packages(env_dir)
|
||||
|
||||
assert not env_dir.exists()
|
||||
|
||||
def test_reports_progress(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
env_dir.mkdir()
|
||||
messages = []
|
||||
|
||||
_cleanup_old_cuda_packages(env_dir, messages.append)
|
||||
|
||||
assert any("Removing previous CUDA packages" in m for m in messages)
|
||||
|
||||
def test_survives_removal_failure(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
env_dir.mkdir()
|
||||
|
||||
with patch("shutil.rmtree", side_effect=OSError("permission denied")):
|
||||
_cleanup_old_cuda_packages(env_dir) # must not raise
|
||||
|
||||
def test_install_cleans_before_installing(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz")
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
stale_marker = tmp_path / "buzz" / "cuda_env" / "torch" / "stale.txt"
|
||||
stale_marker.parent.mkdir(parents=True)
|
||||
stale_marker.write_text("old install")
|
||||
|
||||
with patch("buzz.cuda_manager._create_cuda_env", return_value=["pip", "install"]):
|
||||
with patch("buzz.cuda_manager._run_install") as run_install:
|
||||
install_cuda()
|
||||
|
||||
assert not stale_marker.exists()
|
||||
assert run_install.called
|
||||
|
||||
|
||||
class TestIsFlatpak:
|
||||
def test_returns_true_when_flatpak_env_set(self, monkeypatch):
|
||||
monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz")
|
||||
assert is_flatpak() is True
|
||||
|
||||
def test_returns_false_when_flatpak_env_not_set(self, monkeypatch):
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
assert is_flatpak() is False
|
||||
|
||||
|
||||
class TestShouldOfferCudaPrompt:
|
||||
def test_returns_true_on_windows(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
assert should_offer_cuda_prompt() is True
|
||||
|
||||
def test_returns_true_on_linux_regardless_of_packaging(self, monkeypatch):
|
||||
# The installer works from a private venv, so a bare pip or source
|
||||
# install is offered the same as a snap, Flatpak or AppImage.
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
monkeypatch.delenv("APPDIR", raising=False)
|
||||
monkeypatch.delenv("APPIMAGE", raising=False)
|
||||
assert should_offer_cuda_prompt() is True
|
||||
|
||||
def test_returns_false_on_macos(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "darwin")
|
||||
assert should_offer_cuda_prompt() is False
|
||||
|
||||
def test_returns_false_on_macos(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "darwin")
|
||||
assert should_offer_cuda_prompt() is False
|
||||
|
||||
|
||||
class TestIsCudaTorchInstalled:
|
||||
def test_returns_true_when_cuda_available(self):
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = True
|
||||
mock_torch.__version__ = "2.0.0+cu118"
|
||||
mock_torch.version.cuda = "11.8"
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
assert is_cuda_torch_installed() is True
|
||||
|
||||
def test_returns_false_when_cuda_not_available(self):
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
mock_torch.__version__ = "2.0.0"
|
||||
mock_torch.version.cuda = None
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
assert is_cuda_torch_installed() is False
|
||||
|
||||
def test_returns_false_when_torch_not_installed(self):
|
||||
with patch.dict("sys.modules", {"torch": None}):
|
||||
with patch("builtins.__import__", side_effect=ImportError):
|
||||
assert is_cuda_torch_installed() is False
|
||||
|
||||
def test_logs_warning_when_cuda_compiled_but_unavailable(self):
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
mock_torch.__version__ = "2.0.0+cu118"
|
||||
mock_torch.version.cuda = "11.8"
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
with patch("buzz.cuda_manager.logger") as mock_logger:
|
||||
is_cuda_torch_installed()
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
class TestIsNvidiaGpuPresent:
|
||||
def test_returns_true_when_nvidia_smi_succeeds(self):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
assert is_nvidia_gpu_present() is True
|
||||
|
||||
def test_falls_back_to_proc_file_when_nvidia_smi_missing(self, tmp_path):
|
||||
with patch("subprocess.run", side_effect=FileNotFoundError):
|
||||
with patch("buzz.cuda_manager.Path") as mock_path_cls:
|
||||
mock_path_cls.return_value.exists.return_value = True
|
||||
assert is_nvidia_gpu_present() is True
|
||||
|
||||
def test_returns_false_when_nvidia_smi_fails_and_no_proc_file(self):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=1)
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
assert is_nvidia_gpu_present() is False
|
||||
|
||||
def test_handles_timeout(self):
|
||||
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(["nvidia-smi"], 5)):
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
assert is_nvidia_gpu_present() is False
|
||||
|
||||
|
||||
class TestGetCudaEnvDir:
|
||||
def test_snap_uses_snap_user_data(self, monkeypatch, tmp_path):
|
||||
snap_dir = tmp_path / "snap_data"
|
||||
monkeypatch.setenv("SNAP", "/snap/buzz/current")
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(snap_dir))
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
assert get_cuda_env_dir() == snap_dir / "cuda_env"
|
||||
|
||||
def test_snap_falls_back_to_home_when_no_snap_user_data(self, monkeypatch):
|
||||
monkeypatch.setenv("SNAP", "/snap/buzz/current")
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
assert get_cuda_env_dir() == Path.home() / ".local" / "share" / "buzz" / "cuda_env"
|
||||
|
||||
def test_flatpak_uses_xdg_data_home(self, monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz")
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
assert get_cuda_env_dir() == tmp_path / "buzz" / "cuda_env"
|
||||
|
||||
def test_falls_back_to_the_buzz_data_dir(self, monkeypatch):
|
||||
# Never a shared site-packages: an uninstall has to be able to take the
|
||||
# multi-gigabyte install with it.
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
from platformdirs import user_data_dir
|
||||
|
||||
assert get_cuda_root_dir() == Path(user_data_dir("Buzz"))
|
||||
assert get_cuda_env_dir().name == "cuda_env"
|
||||
|
||||
|
||||
class TestGetCudaEnvSitePackages:
|
||||
def test_finds_posix_layout(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
site_packages = tmp_path / "lib" / "python3.13" / "site-packages"
|
||||
site_packages.mkdir(parents=True)
|
||||
assert get_cuda_env_site_packages(tmp_path) == site_packages
|
||||
|
||||
def test_finds_windows_layout(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
site_packages = tmp_path / "Lib" / "site-packages"
|
||||
site_packages.mkdir(parents=True)
|
||||
assert get_cuda_env_site_packages(tmp_path) == site_packages
|
||||
|
||||
def test_returns_none_when_not_installed(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
assert get_cuda_env_site_packages(tmp_path) is None
|
||||
|
||||
|
||||
class TestVenvPython:
|
||||
def test_windows_layout(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
assert _venv_python(tmp_path) == tmp_path / "Scripts" / "python.exe"
|
||||
|
||||
def test_posix_layout(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
assert _venv_python(tmp_path) == tmp_path / "bin" / "python"
|
||||
|
||||
|
||||
class TestSubprocessHideWindowKwargs:
|
||||
def test_returns_empty_on_linux(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
assert _subprocess_hide_window_kwargs() == {}
|
||||
|
||||
def test_returns_startupinfo_on_windows(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
# Only run on actual windows, otherwise mock the STARTUPINFO
|
||||
if sys.platform != "win32":
|
||||
mock_si = MagicMock()
|
||||
with patch("subprocess.STARTUPINFO", return_value=mock_si):
|
||||
with patch("subprocess.STARTF_USESHOWWINDOW", 1):
|
||||
with patch("subprocess.SW_HIDE", 0):
|
||||
with patch("subprocess.CREATE_NO_WINDOW", 0x08000000):
|
||||
result = _subprocess_hide_window_kwargs()
|
||||
assert "startupinfo" in result
|
||||
assert "creationflags" in result
|
||||
|
||||
|
||||
class TestGetBasePython:
|
||||
def test_returns_base_executable_when_not_frozen(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "frozen", False, raising=False)
|
||||
assert _get_base_python() == getattr(sys, "_base_executable", None) or sys.executable
|
||||
|
||||
def test_prefers_the_bundled_interpreter_when_frozen(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False)
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
bundled = tmp_path / "python" / "python3"
|
||||
bundled.parent.mkdir(parents=True)
|
||||
bundled.write_text("")
|
||||
|
||||
assert _get_base_python() == str(bundled)
|
||||
|
||||
def test_falls_back_to_path_when_frozen_without_bundle(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False)
|
||||
with patch("shutil.which", return_value="/usr/bin/python3"):
|
||||
with patch(
|
||||
"buzz.cuda_manager._interpreter_version_matches", return_value=True
|
||||
):
|
||||
assert _get_base_python() == "/usr/bin/python3"
|
||||
|
||||
def test_skips_a_path_interpreter_of_another_version(self, monkeypatch, tmp_path):
|
||||
# The AppImage bundles no separate interpreter, so PATH is all there is,
|
||||
# and the distro's python3 may be a version torch has no wheels for.
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False)
|
||||
with patch("shutil.which", return_value="/usr/bin/python3"):
|
||||
with patch(
|
||||
"buzz.cuda_manager._interpreter_version_matches", return_value=False
|
||||
):
|
||||
assert _get_base_python() is None
|
||||
|
||||
def test_returns_none_when_frozen_and_no_interpreter(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False)
|
||||
with patch("shutil.which", return_value=None):
|
||||
assert _get_base_python() is None
|
||||
|
||||
|
||||
class TestInterpreterVersionMatches:
|
||||
def test_true_for_the_running_version(self):
|
||||
assert _interpreter_version_matches(sys.executable) is True
|
||||
|
||||
def test_false_for_another_version(self):
|
||||
probe = MagicMock(returncode=0, stdout="3.14\n")
|
||||
with patch("subprocess.run", return_value=probe):
|
||||
assert _interpreter_version_matches("/usr/bin/python3") is False
|
||||
|
||||
def test_false_when_the_probe_cannot_run(self):
|
||||
with patch("subprocess.run", side_effect=OSError):
|
||||
assert _interpreter_version_matches("/usr/bin/python3") is False
|
||||
|
||||
|
||||
class TestFindUv:
|
||||
def test_prefers_the_snap_bundled_uv(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("SNAP", str(tmp_path))
|
||||
with patch("subprocess.run", return_value=MagicMock(returncode=0)):
|
||||
assert _find_uv() == str(tmp_path / "bin" / "uv")
|
||||
|
||||
def test_falls_back_to_path(self, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
with patch("shutil.which", return_value="/usr/bin/uv"):
|
||||
with patch("subprocess.run", return_value=MagicMock(returncode=0)):
|
||||
assert _find_uv() == "/usr/bin/uv"
|
||||
|
||||
def test_returns_none_when_uv_is_unusable(self, monkeypatch):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
with patch("shutil.which", return_value="/usr/bin/uv"):
|
||||
with patch("subprocess.run", side_effect=OSError):
|
||||
assert _find_uv() is None
|
||||
|
||||
|
||||
class TestCreateCudaEnv:
|
||||
def test_uses_uv_when_available(self, tmp_path):
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
with patch("buzz.cuda_manager._find_uv", return_value="/usr/bin/uv"):
|
||||
with patch("buzz.cuda_manager._get_base_python", return_value="/usr/bin/python3"):
|
||||
with patch("buzz.cuda_manager._run_command") as run_command:
|
||||
cmd = _create_cuda_env(env_dir)
|
||||
|
||||
assert run_command.call_args[0][0][:2] == ["/usr/bin/uv", "venv"]
|
||||
assert cmd[:3] == ["/usr/bin/uv", "pip", "install"]
|
||||
|
||||
def test_asks_uv_for_a_matching_python_when_none_is_installed(self, tmp_path):
|
||||
# AppImage on a host whose only python3 is too new for the torch wheels:
|
||||
# uv downloads a managed CPython of the version Buzz itself runs.
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
with patch("buzz.cuda_manager._find_uv", return_value="/usr/bin/uv"):
|
||||
with patch("buzz.cuda_manager._get_base_python", return_value=None):
|
||||
with patch("buzz.cuda_manager._run_command") as run_command:
|
||||
_create_cuda_env(env_dir)
|
||||
|
||||
assert run_command.call_args[0][0][:4] == [
|
||||
"/usr/bin/uv",
|
||||
"venv",
|
||||
"--python",
|
||||
get_python_version(),
|
||||
]
|
||||
|
||||
def test_raises_without_uv_or_a_matching_python(self, tmp_path):
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
with patch("buzz.cuda_manager._find_uv", return_value=None):
|
||||
with patch("buzz.cuda_manager._get_base_python", return_value=None):
|
||||
with pytest.raises(RuntimeError, match="Could not find a Python"):
|
||||
_create_cuda_env(env_dir)
|
||||
|
||||
def test_falls_back_to_the_venv_module(self, tmp_path):
|
||||
# The snap's Python has neither pip nor ensurepip, the Windows bundle
|
||||
# has both; only one of the two paths can work on a given platform.
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
with patch("buzz.cuda_manager._find_uv", return_value=None):
|
||||
with patch("buzz.cuda_manager._get_base_python", return_value="/usr/bin/python3"):
|
||||
with patch("subprocess.run", return_value=MagicMock(returncode=0)) as run:
|
||||
cmd = _create_cuda_env(env_dir)
|
||||
|
||||
assert run.call_args[0][0] == ["/usr/bin/python3", "-m", "venv", str(env_dir)]
|
||||
assert cmd[1:] == ["-m", "pip", "install", "--no-cache-dir"]
|
||||
|
||||
def test_raises_when_venv_creation_fails(self, tmp_path):
|
||||
env_dir = tmp_path / "cuda_env"
|
||||
failure = MagicMock(returncode=1, stderr="no ensurepip", stdout="")
|
||||
with patch("buzz.cuda_manager._find_uv", return_value=None):
|
||||
with patch("buzz.cuda_manager._get_base_python", return_value="/usr/bin/python3"):
|
||||
with patch("subprocess.run", return_value=failure):
|
||||
with pytest.raises(RuntimeError, match="Could not create the environment"):
|
||||
_create_cuda_env(env_dir)
|
||||
|
||||
|
||||
class TestRunInstall:
|
||||
def test_calls_the_installer_with_packages(self):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.stdout = iter(["Collecting torch\n", "Successfully installed\n"])
|
||||
mock_proc.returncode = 0
|
||||
mock_proc.wait.return_value = None
|
||||
|
||||
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
_run_install(
|
||||
[sys.executable, "-m", "pip", "install"],
|
||||
["torch==2.0.0"],
|
||||
extra_args=["--index-url", "https://example.com"],
|
||||
)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "torch==2.0.0" in cmd
|
||||
assert "--index-url" in cmd
|
||||
|
||||
def test_raises_on_nonzero_exit(self):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.stdout = iter([])
|
||||
mock_proc.returncode = 1
|
||||
mock_proc.wait.return_value = None
|
||||
|
||||
with patch("subprocess.Popen", return_value=mock_proc):
|
||||
with pytest.raises(RuntimeError, match="pip install failed"):
|
||||
_run_install([sys.executable, "-m", "pip", "install"], ["torch==2.0.0"])
|
||||
|
||||
def test_calls_progress_callback(self):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.stdout = iter(["line1\n", "line2\n"])
|
||||
mock_proc.returncode = 0
|
||||
mock_proc.wait.return_value = None
|
||||
|
||||
calls = []
|
||||
with patch("subprocess.Popen", return_value=mock_proc):
|
||||
_run_install([sys.executable, "-m", "pip", "install"], ["pkg"], progress_callback=calls.append)
|
||||
|
||||
assert "line1" in calls
|
||||
assert "line2" in calls
|
||||
|
||||
|
||||
class TestInstallCuda:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_env(self, monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("SNAP", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"buzz.cuda_manager.get_cuda_root_dir", lambda: tmp_path
|
||||
)
|
||||
|
||||
def test_installs_into_the_private_env(self):
|
||||
with patch("buzz.cuda_manager._create_cuda_env") as create_env:
|
||||
with patch("buzz.cuda_manager._run_install") as run_install:
|
||||
install_cuda()
|
||||
|
||||
assert create_env.called
|
||||
assert run_install.call_count == 2
|
||||
# No --user, no --break-system-packages: nothing outside the private env.
|
||||
for call_args in run_install.call_args_list:
|
||||
assert "--user" not in (call_args.kwargs.get("extra_args") or [])
|
||||
|
||||
def test_passes_progress_callback(self):
|
||||
messages = []
|
||||
with patch("buzz.cuda_manager._create_cuda_env"):
|
||||
with patch("buzz.cuda_manager._run_install"):
|
||||
install_cuda(progress_callback=messages.append)
|
||||
|
||||
assert any("NVIDIA" in m for m in messages)
|
||||
assert any("PyTorch" in m for m in messages)
|
||||
|
||||
def test_excludes_linux_only_packages_on_windows(self, monkeypatch):
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
captured = []
|
||||
|
||||
with patch("buzz.cuda_manager._create_cuda_env"):
|
||||
with patch(
|
||||
"buzz.cuda_manager._run_install",
|
||||
side_effect=lambda cmd, packages, **kwargs: captured.append(packages),
|
||||
):
|
||||
install_cuda()
|
||||
|
||||
nvidia_pkgs = captured[0]
|
||||
for pkg in CUDA_NVIDIA_PACKAGES_LINUX:
|
||||
assert pkg not in nvidia_pkgs
|
||||
301
tests/cuda_setup_test.py
Normal file
301
tests/cuda_setup_test.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def _make_env(root, version="3.13", platform_name="linux"):
|
||||
"""Create a venv-shaped site-packages tree under root and return it."""
|
||||
if platform_name == "win32":
|
||||
site_packages = root / "cuda_env" / "Lib" / "site-packages"
|
||||
else:
|
||||
site_packages = root / "cuda_env" / "lib" / f"python{version}" / "site-packages"
|
||||
site_packages.mkdir(parents=True)
|
||||
return site_packages
|
||||
|
||||
|
||||
class TestGetCudaTargetDir:
|
||||
def test_returns_snap_env_site_packages(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(tmp_path))
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
site_packages = _make_env(tmp_path)
|
||||
|
||||
from buzz.cuda_setup import _get_cuda_target_dir
|
||||
assert _get_cuda_target_dir() == site_packages
|
||||
|
||||
def test_ignores_an_unrelated_snap(self, monkeypatch, tmp_path):
|
||||
# A snap-packaged tool launching Buzz exports SNAP_USER_DATA pointing at
|
||||
# its own directory; must agree with is_snap() in buzz/cuda_manager.py.
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(tmp_path))
|
||||
monkeypatch.setenv("SNAP_NAME", "astral-uv")
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
_make_env(tmp_path)
|
||||
|
||||
from buzz.cuda_setup import _get_cuda_target_dir
|
||||
assert _get_cuda_target_dir() != tmp_path / "cuda_env"
|
||||
|
||||
def test_returns_flatpak_env_site_packages(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz")
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
site_packages = _make_env(tmp_path / "buzz")
|
||||
|
||||
from buzz.cuda_setup import _get_cuda_target_dir
|
||||
assert _get_cuda_target_dir() == site_packages
|
||||
|
||||
def test_returns_windows_env_site_packages(self, monkeypatch, tmp_path):
|
||||
# The Windows build installs into the same private venv, so nothing
|
||||
# lands in a site-packages shared with other Python installs.
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
site_packages = _make_env(tmp_path, platform_name="win32")
|
||||
monkeypatch.setattr("buzz.cuda_manager.get_cuda_root_dir", lambda: tmp_path)
|
||||
|
||||
from buzz.cuda_setup import _get_cuda_target_dir
|
||||
assert _get_cuda_target_dir() == site_packages
|
||||
|
||||
def test_returns_none_when_not_installed(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
monkeypatch.setattr("buzz.cuda_manager.get_cuda_root_dir", lambda: tmp_path)
|
||||
|
||||
from buzz.cuda_setup import _get_cuda_target_dir
|
||||
assert _get_cuda_target_dir() is None
|
||||
|
||||
|
||||
class TestGetSitePackagesDirs:
|
||||
def test_adds_cuda_target_to_sys_path(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
cuda_target = _make_env(tmp_path)
|
||||
monkeypatch.setenv("SNAP_USER_DATA", str(tmp_path))
|
||||
monkeypatch.setenv("SNAP_NAME", "buzz")
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
|
||||
from buzz.cuda_setup import _get_site_packages_dirs
|
||||
dirs = _get_site_packages_dirs()
|
||||
|
||||
assert cuda_target in dirs
|
||||
assert str(cuda_target) in sys.path
|
||||
|
||||
def test_returns_list_including_existing_site_packages(self, monkeypatch):
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
|
||||
from buzz.cuda_setup import _get_site_packages_dirs
|
||||
dirs = _get_site_packages_dirs()
|
||||
assert isinstance(dirs, list)
|
||||
|
||||
|
||||
class TestCollectCudaLibDirs:
|
||||
def test_includes_torch_lib(self, tmp_path):
|
||||
torch_lib = tmp_path / "torch" / "lib"
|
||||
torch_lib.mkdir(parents=True)
|
||||
|
||||
from buzz.cuda_setup import _collect_cuda_lib_dirs
|
||||
dirs = _collect_cuda_lib_dirs(tmp_path)
|
||||
assert str(torch_lib) in dirs
|
||||
|
||||
def test_includes_nvidia_package_libs(self, tmp_path):
|
||||
nvidia_cublas = tmp_path / "nvidia" / "cublas" / "lib"
|
||||
nvidia_cublas.mkdir(parents=True)
|
||||
|
||||
from buzz.cuda_setup import _collect_cuda_lib_dirs
|
||||
dirs = _collect_cuda_lib_dirs(tmp_path)
|
||||
assert str(nvidia_cublas) in dirs
|
||||
|
||||
def test_returns_empty_when_no_dirs_exist(self, tmp_path):
|
||||
from buzz.cuda_setup import _collect_cuda_lib_dirs
|
||||
dirs = _collect_cuda_lib_dirs(tmp_path)
|
||||
assert dirs == []
|
||||
|
||||
|
||||
class TestGetNvidiaPackageLibDirs:
|
||||
def test_finds_nvidia_lib_dirs(self, monkeypatch, tmp_path):
|
||||
sp = tmp_path / "site-packages"
|
||||
nvidia_lib = sp / "nvidia" / "cublas" / "lib"
|
||||
nvidia_lib.mkdir(parents=True)
|
||||
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
|
||||
with patch("buzz.cuda_setup._get_site_packages_dirs", return_value=[sp]):
|
||||
from buzz.cuda_setup import _get_nvidia_package_lib_dirs
|
||||
dirs = _get_nvidia_package_lib_dirs()
|
||||
|
||||
assert nvidia_lib in dirs
|
||||
|
||||
def test_finds_torch_lib_dir(self, monkeypatch, tmp_path):
|
||||
sp = tmp_path / "site-packages"
|
||||
torch_lib = sp / "torch" / "lib"
|
||||
torch_lib.mkdir(parents=True)
|
||||
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
|
||||
with patch("buzz.cuda_setup._get_site_packages_dirs", return_value=[sp]):
|
||||
from buzz.cuda_setup import _get_nvidia_package_lib_dirs
|
||||
dirs = _get_nvidia_package_lib_dirs()
|
||||
|
||||
assert torch_lib in dirs
|
||||
|
||||
|
||||
class TestSetupLinuxCuda:
|
||||
def test_skips_when_no_cuda_target(self, monkeypatch):
|
||||
monkeypatch.delenv("SNAP_USER_DATA", raising=False)
|
||||
monkeypatch.delenv("SNAP_NAME", raising=False)
|
||||
monkeypatch.delenv("FLATPAK_ID", raising=False)
|
||||
monkeypatch.delenv("BUZZ_CUDA_SETUP_DONE", raising=False)
|
||||
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=None):
|
||||
with patch("buzz.cuda_setup._collect_site_packages_cuda_lib_dirs", return_value=[]):
|
||||
with patch("multiprocessing.parent_process", return_value=None):
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda() # should not raise
|
||||
|
||||
def test_skips_when_cuda_target_does_not_exist(self, monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("BUZZ_CUDA_SETUP_DONE", raising=False)
|
||||
nonexistent = tmp_path / "nonexistent"
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=nonexistent):
|
||||
with patch("buzz.cuda_setup._collect_site_packages_cuda_lib_dirs", return_value=[]):
|
||||
with patch("multiprocessing.parent_process", return_value=None):
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda() # should not raise
|
||||
|
||||
def test_skips_when_no_torch_lib(self, monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("BUZZ_CUDA_SETUP_DONE", raising=False)
|
||||
cuda_target = tmp_path / "cuda_packages"
|
||||
cuda_target.mkdir()
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target):
|
||||
with patch("buzz.cuda_setup._collect_site_packages_cuda_lib_dirs", return_value=[]):
|
||||
with patch("multiprocessing.parent_process", return_value=None):
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda() # should not raise
|
||||
|
||||
def test_reexecs_when_sentinel_not_set(self, monkeypatch, tmp_path):
|
||||
cuda_target = tmp_path / "cuda_packages"
|
||||
torch_lib = cuda_target / "torch" / "lib"
|
||||
torch_lib.mkdir(parents=True)
|
||||
|
||||
monkeypatch.delenv("BUZZ_CUDA_SETUP_DONE", raising=False)
|
||||
monkeypatch.delenv("LD_LIBRARY_PATH", raising=False)
|
||||
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target):
|
||||
with patch("buzz.cuda_setup._collect_cuda_lib_dirs", return_value=[str(torch_lib)]):
|
||||
with patch("multiprocessing.parent_process", return_value=None):
|
||||
with patch("os.execv", side_effect=OSError("test")):
|
||||
with patch("buzz.cuda_setup._preload_linux_libraries_fallback") as mock_fallback:
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda()
|
||||
|
||||
mock_fallback.assert_called_once()
|
||||
|
||||
def test_no_reexec_when_sentinel_already_set(self, monkeypatch, tmp_path):
|
||||
cuda_target = tmp_path / "cuda_packages"
|
||||
torch_lib = cuda_target / "torch" / "lib"
|
||||
torch_lib.mkdir(parents=True)
|
||||
|
||||
monkeypatch.setenv("BUZZ_CUDA_SETUP_DONE", "1")
|
||||
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target):
|
||||
with patch("os.execv") as mock_execv:
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda()
|
||||
|
||||
mock_execv.assert_not_called()
|
||||
|
||||
def test_no_reexec_in_worker_subprocess(self, monkeypatch, tmp_path):
|
||||
cuda_target = tmp_path / "cuda_packages"
|
||||
torch_lib = cuda_target / "torch" / "lib"
|
||||
torch_lib.mkdir(parents=True)
|
||||
|
||||
monkeypatch.delenv("BUZZ_CUDA_SETUP_DONE", raising=False)
|
||||
|
||||
with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target):
|
||||
with patch("multiprocessing.parent_process", return_value=object()):
|
||||
with patch("os.execv") as mock_execv:
|
||||
from buzz.cuda_setup import _setup_linux_cuda
|
||||
_setup_linux_cuda()
|
||||
|
||||
mock_execv.assert_not_called()
|
||||
|
||||
|
||||
class TestSetupWindowsDllDirectories:
|
||||
def test_calls_add_dll_directory(self, tmp_path):
|
||||
lib_dir = tmp_path / "lib"
|
||||
lib_dir.mkdir()
|
||||
|
||||
with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]):
|
||||
with patch("buzz.cuda_setup.os") as mock_os:
|
||||
from buzz.cuda_setup import _setup_windows_dll_directories
|
||||
_setup_windows_dll_directories()
|
||||
|
||||
mock_os.add_dll_directory.assert_called_once_with(str(lib_dir))
|
||||
|
||||
def test_logs_warning_when_no_lib_dirs(self):
|
||||
with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[]):
|
||||
with patch("buzz.cuda_setup.logger") as mock_logger:
|
||||
from buzz.cuda_setup import _setup_windows_dll_directories
|
||||
_setup_windows_dll_directories()
|
||||
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
class TestSetupCudaLibraries:
|
||||
def test_calls_windows_setup_on_windows(self):
|
||||
with patch("platform.system", return_value="Windows"):
|
||||
with patch("buzz.cuda_setup._setup_windows_dll_directories") as mock_win:
|
||||
from buzz.cuda_setup import setup_cuda_libraries
|
||||
setup_cuda_libraries()
|
||||
mock_win.assert_called_once()
|
||||
|
||||
def test_calls_linux_setup_on_linux(self):
|
||||
with patch("platform.system", return_value="Linux"):
|
||||
with patch("buzz.cuda_setup._setup_linux_cuda") as mock_linux:
|
||||
from buzz.cuda_setup import setup_cuda_libraries
|
||||
setup_cuda_libraries()
|
||||
mock_linux.assert_called_once()
|
||||
|
||||
def test_does_nothing_on_macos(self):
|
||||
with patch("platform.system", return_value="Darwin"):
|
||||
with patch("buzz.cuda_setup._setup_windows_dll_directories") as mock_win:
|
||||
with patch("buzz.cuda_setup._setup_linux_cuda") as mock_linux:
|
||||
from buzz.cuda_setup import setup_cuda_libraries
|
||||
setup_cuda_libraries()
|
||||
mock_win.assert_not_called()
|
||||
mock_linux.assert_not_called()
|
||||
|
||||
|
||||
class TestPreloadLinuxLibrariesFallback:
|
||||
def test_loads_so_files(self, tmp_path):
|
||||
lib_dir = tmp_path / "lib"
|
||||
lib_dir.mkdir()
|
||||
so_file = lib_dir / "libfoo.so.1"
|
||||
so_file.touch()
|
||||
|
||||
with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]):
|
||||
with patch("ctypes.CDLL") as mock_cdll:
|
||||
from buzz.cuda_setup import _preload_linux_libraries_fallback
|
||||
_preload_linux_libraries_fallback()
|
||||
mock_cdll.assert_called()
|
||||
|
||||
def test_skips_libnvblas(self, tmp_path):
|
||||
lib_dir = tmp_path / "lib"
|
||||
lib_dir.mkdir()
|
||||
skip_file = lib_dir / "libnvblas.so.1"
|
||||
skip_file.touch()
|
||||
|
||||
with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]):
|
||||
with patch("ctypes.CDLL") as mock_cdll:
|
||||
from buzz.cuda_setup import _preload_linux_libraries_fallback
|
||||
_preload_linux_libraries_fallback()
|
||||
mock_cdll.assert_not_called()
|
||||
|
|
@ -206,7 +206,7 @@ class TestTranscriptionModel:
|
|||
model = TranscriptionModel(
|
||||
model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY
|
||||
)
|
||||
assert str(model) == "Whisper (Tiny)"
|
||||
assert str(model) == "OpenAI Whisper (Tiny)"
|
||||
|
||||
def test_str_whisper_cpp(self):
|
||||
model = TranscriptionModel(
|
||||
|
|
|
|||
|
|
@ -414,6 +414,7 @@ class TestStartLocalWhisperServer:
|
|||
# Mock a successful process
|
||||
mock_process = MagicMock()
|
||||
mock_process.poll.return_value = None # Process is running
|
||||
mock_process.stderr = iter([])
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
transcriber = RecordingTranscriber(
|
||||
|
|
@ -522,7 +523,7 @@ class TestStartLocalWhisperServer:
|
|||
# Mock a failed process
|
||||
mock_process = MagicMock()
|
||||
mock_process.poll.return_value = 1 # Process terminated with error
|
||||
mock_process.stderr.read.return_value = b"Error loading model"
|
||||
mock_process.stderr = iter([b"Error loading model"])
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
transcriber = RecordingTranscriber(
|
||||
|
|
|
|||
|
|
@ -167,13 +167,13 @@ class TestWhisperFileTranscriber:
|
|||
pytest.param(
|
||||
"/a/b/c.mp4",
|
||||
OutputFormat.SRT,
|
||||
"/a/b/c-translate--Whisper-tiny.srt",
|
||||
"/a/b/c-translate--OpenAI Whisper-tiny.srt",
|
||||
marks=pytest.mark.skipif(platform.system() == "Windows", reason=""),
|
||||
),
|
||||
pytest.param(
|
||||
"C:\\a\\b\\c.mp4",
|
||||
OutputFormat.SRT,
|
||||
"C:\\a\\b\\c-translate--Whisper-tiny.srt",
|
||||
"C:\\a\\b\\c-translate--OpenAI Whisper-tiny.srt",
|
||||
marks=pytest.mark.skipif(platform.system() != "Windows", reason=""),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -54,3 +54,30 @@ class TestAudioMeterWidget:
|
|||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
assert widget.height() == 56
|
||||
|
||||
def test_update_queue_size(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_queue_size(5)
|
||||
assert widget.queue_size == 5
|
||||
|
||||
def test_reset_amplitude_clears_queue_size(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_queue_size(3)
|
||||
widget.reset_amplitude()
|
||||
assert widget.queue_size == 0
|
||||
|
||||
def test_initial_queue_size_is_zero(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
assert widget.queue_size == 0
|
||||
|
||||
def test_paint_event_does_not_raise(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.show()
|
||||
widget.update_amplitude(0.5)
|
||||
widget.update_average_amplitude(0.1)
|
||||
widget.update_queue_size(2)
|
||||
widget.repaint() # triggers paintEvent
|
||||
|
|
|
|||
|
|
@ -1,20 +1,49 @@
|
|||
import gc
|
||||
import logging
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
from buzz.settings.settings import Settings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_amplitude_listener_stream():
|
||||
# RecordingAmplitudeListener creates a real sounddevice.InputStream that
|
||||
# fires C-level callbacks from a background thread. If the Qt object is
|
||||
# deleted (via deleteLater + processEvents) while the C thread is still
|
||||
# active it causes a segfault or "Exception ignored from cffi callback"
|
||||
# that contaminates later tests. Patch it out so no real audio hardware
|
||||
# stream is created during widget tests.
|
||||
mock_stream = MagicMock()
|
||||
mock_stream.samplerate = 16000
|
||||
with patch("buzz.recording.sounddevice.InputStream", return_value=mock_stream):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_get_password():
|
||||
with patch("buzz.widgets.recording_transcriber_widget.get_password", return_value=None):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_message_box():
|
||||
# Prevent QMessageBox dialogs from blocking tests (e.g. when a background
|
||||
# transcription thread emits an error after the test has already "passed")
|
||||
with patch("buzz.widgets.recording_transcriber_widget.QMessageBox"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def force_gc_between_tests():
|
||||
yield
|
||||
gc.collect()
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
# Flush pending deleteLater() and queued cross-thread signals.
|
||||
# We intentionally do NOT call gc.collect() here: PyQt6 signal/slot
|
||||
# infrastructure can leave Python tuples with corrupted type pointers in
|
||||
# CPython's cyclic-GC tracking list, causing a PyTuple_GET_SIZE assertion
|
||||
# failure inside gc.collect(). Python's reference-counting handles
|
||||
# non-cyclic objects; PyQt6's own lifecycle management handles Qt cycles.
|
||||
for _ in range(3):
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
|
|
|
|||
132
tests/widgets/cuda_installer_widget_test.py
Normal file
132
tests/widgets/cuda_installer_widget_test.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from buzz.widgets.cuda_installer_widget import CudaInstallerDialog, _InstallWorker
|
||||
|
||||
|
||||
class TestInstallWorker:
|
||||
def test_calls_install_cuda_and_emits_finished(self):
|
||||
worker = _InstallWorker()
|
||||
finished_mock = MagicMock()
|
||||
worker.signals.finished.connect(finished_mock)
|
||||
|
||||
with patch("buzz.cuda_manager.install_cuda") as mock_install:
|
||||
worker.run()
|
||||
|
||||
mock_install.assert_called_once()
|
||||
finished_mock.assert_called_once()
|
||||
|
||||
def test_emits_error_on_exception(self):
|
||||
worker = _InstallWorker()
|
||||
error_mock = MagicMock()
|
||||
worker.signals.error.connect(error_mock)
|
||||
|
||||
with patch("buzz.cuda_manager.install_cuda", side_effect=RuntimeError("fail")):
|
||||
worker.run()
|
||||
|
||||
error_mock.assert_called_once_with("fail")
|
||||
|
||||
def test_passes_progress_callback_to_install(self):
|
||||
worker = _InstallWorker()
|
||||
progress_mock = MagicMock()
|
||||
worker.signals.progress.connect(progress_mock)
|
||||
|
||||
def fake_install(progress_callback=None):
|
||||
if progress_callback:
|
||||
progress_callback("installing...")
|
||||
|
||||
with patch("buzz.cuda_manager.install_cuda", side_effect=fake_install):
|
||||
worker.run()
|
||||
|
||||
progress_mock.assert_called_once_with("installing...")
|
||||
|
||||
|
||||
class TestCudaInstallerDialog:
|
||||
def test_dialog_creates_with_correct_title(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
assert "GPU" in dialog.windowTitle() or "Nvidia" in dialog.windowTitle()
|
||||
|
||||
def test_install_button_exists(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
assert dialog.install_button is not None
|
||||
assert dialog.install_button.isEnabled()
|
||||
|
||||
def test_decline_button_exists(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
assert dialog.decline_button is not None
|
||||
|
||||
def test_progress_bar_hidden_initially(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
assert not dialog.progress_bar.isVisible()
|
||||
|
||||
def test_log_view_hidden_initially(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
assert not dialog.log_view.isVisible()
|
||||
|
||||
def test_install_click_shows_progress_bar(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.show()
|
||||
|
||||
with patch("buzz.widgets.cuda_installer_widget.QThreadPool"):
|
||||
dialog._on_install_clicked()
|
||||
|
||||
assert dialog.progress_bar.isVisible()
|
||||
assert dialog.log_view.isVisible()
|
||||
assert not dialog.install_button.isEnabled()
|
||||
assert not dialog.decline_button.isEnabled()
|
||||
|
||||
def test_on_progress_appends_to_log(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog._on_progress("step 1")
|
||||
assert "step 1" in dialog.log_view.toPlainText()
|
||||
|
||||
def test_on_finished_re_enables_install_button(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.install_button.setEnabled(False)
|
||||
dialog._on_finished()
|
||||
assert dialog.install_button.isEnabled()
|
||||
|
||||
def test_on_finished_hides_progress_bar(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.progress_bar.setVisible(True)
|
||||
dialog._on_finished()
|
||||
assert not dialog.progress_bar.isVisible()
|
||||
|
||||
def test_on_finished_shows_completion_message(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog._on_finished()
|
||||
assert "complete" in dialog.status_label.text().lower() or "restart" in dialog.status_label.text().lower()
|
||||
|
||||
def test_on_error_shows_error_message(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog._on_error("something went wrong")
|
||||
assert "something went wrong" in dialog.status_label.text()
|
||||
|
||||
def test_on_error_re_enables_buttons(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.install_button.setEnabled(False)
|
||||
dialog.decline_button.setEnabled(False)
|
||||
dialog._on_error("fail")
|
||||
assert dialog.install_button.isEnabled()
|
||||
assert dialog.decline_button.isEnabled()
|
||||
|
||||
def test_on_error_hides_progress_bar(self, qtbot: QtBot):
|
||||
dialog = CudaInstallerDialog()
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.progress_bar.setVisible(True)
|
||||
dialog._on_error("fail")
|
||||
assert not dialog.progress_bar.isVisible()
|
||||
|
|
@ -11,16 +11,16 @@ class TestModelTypeComboBox:
|
|||
[
|
||||
pytest.param(
|
||||
[
|
||||
"Whisper",
|
||||
"Whisper.cpp",
|
||||
"Hugging Face",
|
||||
"Faster Whisper",
|
||||
"OpenAI Whisper",
|
||||
"OpenAI Whisper API",
|
||||
# Faster Whisper is not available on macOS x86_64
|
||||
] if not (platform.system() == "Darwin" and platform.machine() == "x86_64") else [
|
||||
"Whisper",
|
||||
"Whisper.cpp",
|
||||
"Hugging Face",
|
||||
"OpenAI Whisper",
|
||||
"OpenAI Whisper API",
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue