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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raivis Dejus 2026-09-12 20:40:48 +03:00 • committed by GitHub
parent 221659720d
commit 7d1929c588
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3593 additions and 887 deletions

View file

@ -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

View file

@ -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")

View file

@ -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.

View file

@ -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
View 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
View 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")

View file

@ -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()

View file

@ -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:

View file

@ -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!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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 ""

View file

@ -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!"

View file

@ -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 !"

View file

@ -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!"

View file

@ -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 "最新の状態です!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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ę!"

View file

@ -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!"

View file

@ -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 "У вас актуальная версия!"

View file

@ -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 "У вас актуальна версія!"

View file

@ -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 "已经是最新版本"

View file

@ -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 "你是最新的!"

View file

@ -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:

View file

@ -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

View file

@ -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:

View 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

View file

@ -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
)

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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())

View 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)

View file

@ -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)

View file

@ -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:

View file

@ -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):

View file

@ -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.

View file

@ -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.

View file

@ -26,7 +26,6 @@ OutputDir={#OutputDir}
OutputBaseFilename={#AppName}-{#AppVersion}-windows
SetupIconFile={#AppIconPath}
UninstallDisplayIcon={app}\{#AppExeName}
DiskSpanning=yes
Compression=lzma
SolidCompression=yes
WizardStyle=modern

View file

@ -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

View file

@ -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>

View file

@ -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
View 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
View 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()

View file

@ -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(

View file

@ -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(

View file

@ -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=""),
),
],

View file

@ -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

View file

@ -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")

View 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()

View file

@ -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",
],
),

1228
uv.lock generated

File diff suppressed because it is too large Load diff