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>
130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
"""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"]
|