diff --git a/README.md b/README.md index 60a10d7..9d335a8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ docker pull ghcr.io/tatertotterson/microwakeword:latest Tagged releases also publish matching immutable image tags: ```bash -docker pull ghcr.io/tatertotterson/microwakeword:v15 +docker pull ghcr.io/tatertotterson/microwakeword:v16 ``` The release tag must match `VERSION`. Update `WHATS_NEW.md` before tagging; the Docker workflow prepends it to GitHub's automatically generated release notes. @@ -32,7 +32,7 @@ Python 3.13 TensorFlow build for `sm_120`: ```bash docker pull ghcr.io/tatertotterson/microwakeword:blackwell -docker pull ghcr.io/tatertotterson/microwakeword:v15-blackwell +docker pull ghcr.io/tatertotterson/microwakeword:v16-blackwell ``` Use the Blackwell image only for RTX 50-series cards. It includes the @@ -53,9 +53,9 @@ docker run -d \ ghcr.io/tatertotterson/microwakeword:latest ``` -Use a version tag such as `ghcr.io/tatertotterson/microwakeword:v15` when you want to pin a known release instead of tracking `latest`. +Use a version tag such as `ghcr.io/tatertotterson/microwakeword:v16` when you want to pin a known release instead of tracking `latest`. For RTX 50-series cards, use `ghcr.io/tatertotterson/microwakeword:blackwell` -or a pinned tag such as `ghcr.io/tatertotterson/microwakeword:v15-blackwell` +or a pinned tag such as `ghcr.io/tatertotterson/microwakeword:v16-blackwell` in the same `docker run` command. The flags: diff --git a/VERSION b/VERSION index 60d3b2f..b6a7d89 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15 +16 diff --git a/WHATS_NEW.md b/WHATS_NEW.md index c373c8c..bbd30f0 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,3 +1,2 @@ -- Added secure Tater linking: enter the short-lived code from Tater Voice Settings instead of giving the trainer a general API token. -- Automatic and manual publishing now tell Tater which trained wake word is active, and Tater applies it globally to every connected satellite. -- Added clear linked, unlinked, and pairing-success states to the Auto Training interface. +- Fixed a startup crash caused by newer pip-installed NVIDIA cuBLAS and cuDNN namespace packages not providing a module file path. +- CUDA library discovery now works across both the standard NVIDIA and Blackwell images and safely allows startup when the optional libraries are unavailable. diff --git a/run.sh b/run.sh index 321a782..53ef07f 100644 --- a/run.sh +++ b/run.sh @@ -107,19 +107,33 @@ fi # Faster Whisper/CTranslate2 loads these CUDA libraries before Python starts. # They live in the persistent UI venv so both Docker image variants can use GPU STT. WHISPER_CUDA_LIBRARY_PATH="$("${PY}" - <<'PY' -import os +from importlib.util import find_spec +from pathlib import Path -try: - import nvidia.cublas.lib - import nvidia.cudnn.lib -except ImportError: - print("") -else: - print( - os.path.dirname(nvidia.cublas.lib.__file__) - + ":" - + os.path.dirname(nvidia.cudnn.lib.__file__) - ) + +def package_directory(name): + try: + spec = find_spec(name) + except (ImportError, AttributeError, ValueError): + return "" + if spec is None: + return "" + + for location in spec.submodule_search_locations or (): + if location: + return str(Path(location).resolve()) + + origin = spec.origin + if origin and origin not in {"built-in", "frozen"}: + return str(Path(origin).resolve().parent) + return "" + + +paths = [ + package_directory("nvidia.cublas.lib"), + package_directory("nvidia.cudnn.lib"), +] +print(":".join(dict.fromkeys(path for path in paths if path))) PY )" if [[ -n "${WHISPER_CUDA_LIBRARY_PATH}" ]]; then diff --git a/tests/test_run_sh.py b/tests/test_run_sh.py new file mode 100644 index 0000000..aafd83f --- /dev/null +++ b/tests/test_run_sh.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import os +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +RUN_SH = REPO_ROOT / "run.sh" + + +def _cuda_path_probe() -> str: + source = RUN_SH.read_text(encoding="utf-8") + match = re.search( + r'WHISPER_CUDA_LIBRARY_PATH="\$\("\$\{PY\}" - <<\'PY\'\n(?P.*?)\nPY\n\)"', + source, + flags=re.DOTALL, + ) + if match is None: + raise AssertionError("Could not locate the CUDA library path probe in run.sh") + return match.group("probe") + + +class RunShCudaLibraryPathTests(unittest.TestCase): + def _run_probe(self, python_path: Path) -> subprocess.CompletedProcess[str]: + env = dict(os.environ) + env["PYTHONPATH"] = str(python_path) + return subprocess.run( + [sys.executable, "-S", "-"], + input=_cuda_path_probe(), + text=True, + capture_output=True, + check=False, + env=env, + ) + + def test_namespace_cuda_packages_do_not_require_module_file(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + cublas_lib = root / "nvidia" / "cublas" / "lib" + cudnn_lib = root / "nvidia" / "cudnn" / "lib" + cublas_lib.mkdir(parents=True) + cudnn_lib.mkdir(parents=True) + + result = self._run_probe(root) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.strip().split(":"), + [str(cublas_lib.resolve()), str(cudnn_lib.resolve())], + ) + + def test_missing_cuda_packages_return_an_empty_path(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + result = self._run_probe(Path(temp_dir)) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "") + + +if __name__ == "__main__": + unittest.main()