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:
yilsemandClaude Opus 5 committed 2026-09-15 19:46:39 +03:00
1 parent 9a1d831d4b
commit 233885d3a7
20 files changed
+684 -5

No files matched your search

+129
View File
@@ -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"]