fix(desktop): ship the VC++ runtime the Windows voice wheels need
onnxruntime (wake word) and ctranslate2 (local speech-to-text) link against msvcp140.dll and msvcp140_1.dll, which the managed CPython does not ship. On Windows without the system VC++ Redistributable both imports failed with "DLL load failed: The specified module could not be found", so "Hey Hermes" and voice chat transcription never worked. - Bundle the Microsoft-signed VC143 CRT set (14.44.35211, matching the managed CPython's vcruntime140) as a Windows extra resource with a SHA-256 manifest. - Before every local backend start, copy missing or mismatching CRT files beside the managed python.exe (the process image directory, searched first), verifying digests; never overwrite the interpreter's own vcruntime and never block boot. Existing installs heal on their next launch. - Report an unloadable native voice wheel as a VC++ runtime problem in the wake-word requirements, local transcription and `hermes doctor`, instead of treating installed package metadata as proof the wheel loads. - Bump AITURK IDE to 1.0.0-beta.12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a1d831d4b
commit
233885d3a7
20 files changed
+684
-5
No files matched your search
@@ -0,0 +1,129 @@
|
||||
"""Windows VC++ runtime detection for the native voice wheels."""
|
||||
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
import tools.transcription_tools as tt
|
||||
import tools.wake_word as ww
|
||||
from tools import windows_native_runtime as wnr
|
||||
|
||||
_NATIVE_LOAD_ERROR = wnr.native_load_error
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_probe_cache():
|
||||
_NATIVE_LOAD_ERROR.cache_clear()
|
||||
yield
|
||||
_NATIVE_LOAD_ERROR.cache_clear()
|
||||
|
||||
|
||||
def _fake_run(returncode, stderr, calls=None):
|
||||
def run(cmd, **kwargs):
|
||||
if calls is not None:
|
||||
calls.append((cmd, kwargs))
|
||||
return subprocess.CompletedProcess(cmd, returncode, stdout="", stderr=stderr)
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def test_non_windows_never_probes(monkeypatch):
|
||||
monkeypatch.setattr(wnr.sys, "platform", "linux")
|
||||
monkeypatch.setattr(wnr.subprocess, "run", pytest.fail)
|
||||
|
||||
assert _NATIVE_LOAD_ERROR("onnxruntime") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stderr",
|
||||
[
|
||||
"ImportError: DLL load failed while importing onnxruntime_pybind11_state: The specified module could not be found.",
|
||||
"ImportError: DLL load failed while importing onnxruntime_pybind11_state: Belirtilen modül bulunamadı.",
|
||||
"FileNotFoundError: Could not find module 'C:\\venv\\ctranslate2\\ctranslate2.dll' (or one of its dependencies).",
|
||||
],
|
||||
)
|
||||
def test_dll_load_failure_is_reported(monkeypatch, stderr):
|
||||
calls = []
|
||||
monkeypatch.setattr(wnr.sys, "platform", "win32")
|
||||
monkeypatch.setattr(wnr.subprocess, "run", _fake_run(1, "Traceback (most recent call last):\n" + stderr, calls))
|
||||
|
||||
assert _NATIVE_LOAD_ERROR("onnxruntime") == stderr
|
||||
assert calls[0][0][1:] == ["-c", "import onnxruntime"]
|
||||
assert calls[0][1]["timeout"] > 0
|
||||
|
||||
|
||||
def test_missing_package_is_not_a_runtime_failure(monkeypatch):
|
||||
monkeypatch.setattr(wnr.sys, "platform", "win32")
|
||||
monkeypatch.setattr(
|
||||
wnr.subprocess, "run", _fake_run(1, "ModuleNotFoundError: No module named 'onnxruntime'")
|
||||
)
|
||||
|
||||
assert _NATIVE_LOAD_ERROR("onnxruntime") is None
|
||||
|
||||
|
||||
def test_probe_result_is_cached_per_process(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(wnr.sys, "platform", "win32")
|
||||
monkeypatch.setattr(wnr.subprocess, "run", _fake_run(0, "", calls))
|
||||
|
||||
assert _NATIVE_LOAD_ERROR("ctranslate2") is None
|
||||
assert _NATIVE_LOAD_ERROR("ctranslate2") is None
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_inconclusive_probe_does_not_block(monkeypatch):
|
||||
def timeout(cmd, **kwargs):
|
||||
raise subprocess.TimeoutExpired(cmd, kwargs["timeout"])
|
||||
|
||||
monkeypatch.setattr(wnr.sys, "platform", "win32")
|
||||
monkeypatch.setattr(wnr.subprocess, "run", timeout)
|
||||
|
||||
assert _NATIVE_LOAD_ERROR("onnxruntime") is None
|
||||
|
||||
|
||||
def test_module_name_is_validated(monkeypatch):
|
||||
monkeypatch.setattr(wnr.sys, "platform", "win32")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
_NATIVE_LOAD_ERROR("os; import shutil")
|
||||
|
||||
|
||||
def test_wake_requirements_report_missing_vc_runtime(monkeypatch):
|
||||
monkeypatch.setattr(ww, "_stt_ready", lambda: True)
|
||||
monkeypatch.setattr(ww, "_tts_ready", lambda: True)
|
||||
monkeypatch.setattr(ww, "_audio_available", lambda: True)
|
||||
monkeypatch.setattr(ww, "_local_input_device_ready", lambda: True)
|
||||
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: True)
|
||||
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: True)
|
||||
monkeypatch.setattr(ww, "_native_runtime_error", lambda module: "ImportError: DLL load failed")
|
||||
|
||||
reqs = ww.check_wake_word_requirements({"provider": "openwakeword", "capture": "local"})
|
||||
|
||||
assert reqs["available"] is False
|
||||
assert reqs["native_runtime_error"] == "ImportError: DLL load failed"
|
||||
assert "msvcp140.dll" in reqs["hint"]
|
||||
|
||||
|
||||
def test_wake_requirements_unaffected_when_runtime_loads(monkeypatch):
|
||||
monkeypatch.setattr(ww, "_stt_ready", lambda: True)
|
||||
monkeypatch.setattr(ww, "_tts_ready", lambda: True)
|
||||
monkeypatch.setattr(ww, "_audio_available", lambda: True)
|
||||
monkeypatch.setattr(ww, "_local_input_device_ready", lambda: True)
|
||||
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: True)
|
||||
monkeypatch.setattr(ww, "_native_runtime_error", lambda module: None)
|
||||
|
||||
reqs = ww.check_wake_word_requirements({"provider": "openwakeword", "capture": "local"})
|
||||
|
||||
assert reqs["available"] is True
|
||||
assert reqs["native_runtime_error"] is None
|
||||
|
||||
|
||||
def test_local_transcription_reports_missing_vc_runtime(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(tt, "_HAS_FASTER_WHISPER", True)
|
||||
monkeypatch.setattr(wnr, "native_load_error", lambda module: "ImportError: DLL load failed")
|
||||
monkeypatch.setattr(tt, "_load_stt_config", pytest.fail)
|
||||
|
||||
result = tt._transcribe_local(str(tmp_path / "a.wav"), "base")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "msvcp140.dll" in result["error"]
|
||||
Reference in new issue
Block a user