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
@@ -1942,6 +1942,14 @@ def _transcribe_local(
|
||||
if not _try_lazy_install_stt():
|
||||
return {"success": False, "transcript": "", "error": "faster-whisper not installed"}
|
||||
|
||||
# faster-whisper imports lazily, so an unloadable ctranslate2 (missing
|
||||
# VC++ runtime on Windows) would otherwise surface as an opaque DLL error.
|
||||
from tools.windows_native_runtime import native_load_error, voice_runtime_error_message
|
||||
|
||||
native_error = native_load_error("ctranslate2")
|
||||
if native_error:
|
||||
return {"success": False, "transcript": "", "error": voice_runtime_error_message(native_error)}
|
||||
|
||||
try:
|
||||
local_cfg = _load_stt_config().get("local") or {}
|
||||
# Reset the idle timer BEFORE loading/transcribing so the idle-unload
|
||||
|
||||
+16
-1
@@ -896,6 +896,12 @@ def _tts_ready() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _native_runtime_error(module: str) -> Optional[str]:
|
||||
from tools.windows_native_runtime import native_load_error
|
||||
|
||||
return native_load_error(module)
|
||||
|
||||
|
||||
def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Report whether wake-word detection can run, with a remediation hint."""
|
||||
cfg = cfg if cfg is not None else load_wake_word_config()
|
||||
@@ -910,6 +916,10 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s
|
||||
feature = "wake.openwakeword"
|
||||
deps_ok = lazy_deps.is_available(feature)
|
||||
lazy_ok = lazy_deps._allow_lazy_installs()
|
||||
# Installed metadata does not prove the native wheel loads: on Windows
|
||||
# without the VC++ runtime onnxruntime is "installed" yet every import
|
||||
# fails. Report that instead of a lazy install that cannot help.
|
||||
native_error = _native_runtime_error("onnxruntime") if deps_ok and feature == "wake.openwakeword" else None
|
||||
# The audio probe imports sounddevice + numpy — two of the very packages
|
||||
# the lazy installer would fetch — so it can only be trusted once the
|
||||
# feature's deps are installed. On a fresh install (deps missing, lazy
|
||||
@@ -937,6 +947,10 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s
|
||||
if provider == "porcupine" and not (os.getenv("PORCUPINE_ACCESS_KEY") or "").strip():
|
||||
key_ok = False
|
||||
hint = "Set PORCUPINE_ACCESS_KEY (free key at https://console.picovoice.ai)."
|
||||
elif native_error:
|
||||
from tools.windows_native_runtime import voice_runtime_error_message
|
||||
|
||||
hint = voice_runtime_error_message(native_error)
|
||||
elif not deps_ok and not lazy_ok:
|
||||
hint = lazy_deps.feature_install_command(feature) or ""
|
||||
elif not tflite_ok:
|
||||
@@ -969,7 +983,7 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s
|
||||
)
|
||||
|
||||
return {
|
||||
"available": key_ok and stt_ok and tts_ok and tflite_ok and mic_ok,
|
||||
"available": key_ok and stt_ok and tts_ok and tflite_ok and mic_ok and native_error is None,
|
||||
"provider": provider,
|
||||
"deps_available": deps_ok,
|
||||
"audio_available": audio_ok,
|
||||
@@ -979,6 +993,7 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s
|
||||
"stt_available": stt_ok,
|
||||
"tts_available": tts_ok,
|
||||
"phrase": wake_phrase(cfg),
|
||||
"native_runtime_error": native_error,
|
||||
"hint": hint,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Detect native voice wheels that are installed but cannot load on Windows.
|
||||
|
||||
Package metadata (what ``tools.lazy_deps`` checks) reports onnxruntime and
|
||||
ctranslate2 as installed even when their DLLs cannot load because the
|
||||
Microsoft Visual C++ runtime (msvcp140.dll) is missing. A lazy reinstall cannot
|
||||
fix that, so callers need to tell the two cases apart. Importing the module in
|
||||
a child interpreter does so without risking the calling process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
_MODULE_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$")
|
||||
|
||||
# Windows reports the same loader failure in the user's UI language.
|
||||
_DLL_LOAD_MARKERS = (
|
||||
"dll load failed",
|
||||
"could not find module",
|
||||
"specified module could not be found",
|
||||
"belirtilen modül bulunamadı",
|
||||
"[winerror 126]",
|
||||
)
|
||||
|
||||
VC_RUNTIME_HINT = (
|
||||
"Ses bileşeni yüklenemedi: Microsoft Visual C++ çalışma zamanı (msvcp140.dll) eksik. "
|
||||
"AITURK IDE'yi kapatıp yeniden açın; IDE eksik dosyaları otomatik olarak geri yükler."
|
||||
)
|
||||
|
||||
|
||||
def looks_like_dll_load_failure(message: str) -> bool:
|
||||
text = (message or "").lower()
|
||||
return any(marker in text for marker in _DLL_LOAD_MARKERS)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def native_load_error(module: str) -> Optional[str]:
|
||||
"""Return the DLL load error for *module* on Windows, otherwise None.
|
||||
|
||||
None also covers "not installed" and inconclusive probes: this helper only
|
||||
answers "installed but its native libraries cannot load". Cached per
|
||||
process because the runtime cannot change under a running backend (the
|
||||
desktop restores it before the backend starts).
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return None
|
||||
if not _MODULE_RE.match(module):
|
||||
raise ValueError(f"invalid module name: {module!r}")
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", f"import {module}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=120,
|
||||
env=dict(os.environ, PYTHONIOENCODING="utf-8"),
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
||||
stderr = proc.stderr or ""
|
||||
if proc.returncode == 0 or not looks_like_dll_load_failure(stderr):
|
||||
return None
|
||||
lines = [line.strip() for line in stderr.splitlines() if line.strip()]
|
||||
return lines[-1] if lines else f"import {module} failed"
|
||||
|
||||
|
||||
def voice_runtime_error_message(error: str) -> str:
|
||||
return f"{VC_RUNTIME_HINT} ({error})"
|
||||
Reference in new issue
Block a user