Merge fix/ide-windows-vcruntime: AITURK IDE 1.0.0-beta.12 VC++ runtime fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5 committed 2026-09-15 20:01:19 +03:00
commit 4d813c90ae
21 files changed
+700 -9

No files matched your search

@@ -0,0 +1,51 @@
{
"schemaVersion": 1,
"product": "Microsoft Visual C++ 2015-2022 Redistributable (VC143 CRT)",
"version": "14.44.35211.0",
"architecture": "x64",
"publisher": "Microsoft Corporation (Authenticode doğrulandı)",
"files": [
{
"name": "msvcp140.dll",
"size": 557728,
"sha256": "0f885b509a685d2bbfa652fed26b5fb31d88fbdab0a978c641d1c7b8aa460aa9"
},
{
"name": "msvcp140_1.dll",
"size": 35952,
"sha256": "bfad5aef4c63a669e3c140655cdfdf395b6c979b400a447bd5dcb65ed8826c3d"
},
{
"name": "msvcp140_2.dll",
"size": 280200,
"sha256": "3ea06f0ee098b4823cb79599df3780e7f23cce52c19aac31d2a0d47efe33a5e9"
},
{
"name": "msvcp140_atomic_wait.dll",
"size": 50304,
"sha256": "640b2aefced484d0368eea5bdd06addd0658a3a70a49256e560d6923b404a479"
},
{
"name": "msvcp140_codecvt_ids.dll",
"size": 31872,
"sha256": "f2069a52880ec885ee7f0511186100eb7fada0411a2b4948fafea7735b878a18"
},
{
"name": "concrt140.dll",
"size": 324208,
"sha256": "2405355f0a58067b258f8df33c327e3a3d716eaac5a3a5aebb757842d85bd376"
},
{
"name": "vcruntime140.dll",
"size": 124544,
"sha256": "d5e4d9a3e835fa679450145d6a7d94e36573a509317111904d9b3712c30d9066",
"ownedByPython": true
},
{
"name": "vcruntime140_1.dll",
"size": 49792,
"sha256": "1f2d41c4aa5db0bc33ebf7b66d72943a817d7ce6cbe880502a9403823633093f",
"ownedByPython": true
}
]
}
+17
View File
@@ -33,6 +33,7 @@ import {
} from 'electron'
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
import { describeVcRuntimeResult, ensureWindowsVcRuntime } from './windows-vc-runtime'
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
import { appIconCandidates, resolveAppIcon } from './app-icon'
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
@@ -5221,6 +5222,22 @@ async function ensureRuntime(backend) {
)
}
// Voice wheels (onnxruntime, ctranslate2) need msvcp140*.dll, which the
// managed CPython does not ship. Place the bundled Microsoft CRT beside the
// managed python.exe before the backend imports anything. Never blocks boot:
// only the voice features depend on it.
if (IS_WINDOWS) {
const vcRuntime = ensureWindowsVcRuntime({
platform: process.platform,
venvRoot: VENV_ROOT,
sourceDir: IS_PACKAGED
? path.join(process.resourcesPath, 'vc-runtime', 'x64')
: path.join(APP_ROOT, 'assets', 'windows', 'vc-runtime', 'x64')
})
rememberLog(`[vc-runtime] ${describeVcRuntimeResult(vcRuntime)}`)
}
backend.command = getVenvPython(VENV_ROOT)
backend.label = `Hermes at ${ACTIVE_HERMES_ROOT} (venv: ${VENV_ROOT})`
updateBootProgress({
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict'
import crypto from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, test } from 'vitest'
import { ensureWindowsVcRuntime, loadVcRuntimeManifest, readPythonHome } from './windows-vc-runtime'
const here = path.dirname(fileURLToPath(import.meta.url))
const roots: string[] = []
function tempRoot() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aiturk-vc-runtime-'))
roots.push(root)
return root
}
function sha256(data: Buffer) {
return crypto.createHash('sha256').update(data).digest('hex')
}
// A fake install: venv/pyvenv.cfg -> python home, plus a bundled source dir
// whose manifest describes the given files.
function fixture(files: Record<string, { data: string; ownedByPython?: boolean }>) {
const root = tempRoot()
const home = path.join(root, 'python home')
const venv = path.join(root, 'venv')
const source = path.join(root, 'bundle')
for (const dir of [home, venv, source]) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(path.join(home, 'python.exe'), 'exe')
fs.writeFileSync(path.join(venv, 'pyvenv.cfg'), `home = ${home}\r\nversion_info = 3.11\r\n`)
const entries = Object.entries(files).map(([name, { data, ownedByPython }]) => {
const bytes = Buffer.from(data)
fs.writeFileSync(path.join(source, name), bytes)
return { name, size: bytes.length, sha256: sha256(bytes), ...(ownedByPython ? { ownedByPython } : {}) }
})
fs.writeFileSync(
path.join(source, 'manifest.json'),
JSON.stringify({ schemaVersion: 1, version: 'test', architecture: 'x64', files: entries })
)
return { home, venv, source }
}
afterEach(() => {
for (const root of roots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true })
}
})
test('non-Windows platforms are left untouched', () => {
const { home, venv, source } = fixture({ 'msvcp140.dll': { data: 'crt' } })
assert.deepEqual(ensureWindowsVcRuntime({ platform: 'linux', venvRoot: venv, sourceDir: source }), {
status: 'skipped',
reason: 'not windows'
})
assert.equal(fs.existsSync(path.join(home, 'msvcp140.dll')), false)
})
test('readPythonHome parses pyvenv.cfg home with spaces', () => {
const { home, venv } = fixture({ 'msvcp140.dll': { data: 'crt' } })
assert.equal(readPythonHome(venv), home)
assert.equal(readPythonHome(path.join(venv, 'missing')), null)
})
test('missing CRT files are copied beside python.exe and verified', () => {
const { home, venv, source } = fixture({
'msvcp140.dll': { data: 'msvcp' },
'msvcp140_1.dll': { data: 'msvcp1' }
})
const result = ensureWindowsVcRuntime({ platform: 'win32', venvRoot: venv, sourceDir: source })
assert.deepEqual(result, { status: 'repaired', pythonHome: home, copied: ['msvcp140.dll', 'msvcp140_1.dll'] })
assert.equal(fs.readFileSync(path.join(home, 'msvcp140_1.dll'), 'utf8'), 'msvcp1')
assert.deepEqual(fs.readdirSync(home).filter(name => name.endsWith('.tmp')), [])
// Second launch: nothing to do.
assert.deepEqual(ensureWindowsVcRuntime({ platform: 'win32', venvRoot: venv, sourceDir: source }), {
status: 'ok',
pythonHome: home,
copied: []
})
})
test('a mismatching msvcp140 copy is replaced, a python-owned vcruntime is kept', () => {
const { home, venv, source } = fixture({
'msvcp140.dll': { data: 'good' },
'vcruntime140.dll': { data: 'bundled', ownedByPython: true }
})
fs.writeFileSync(path.join(home, 'msvcp140.dll'), 'stale')
fs.writeFileSync(path.join(home, 'vcruntime140.dll'), 'interpreter-owned')
const result = ensureWindowsVcRuntime({ platform: 'win32', venvRoot: venv, sourceDir: source })
assert.deepEqual(result, { status: 'repaired', pythonHome: home, copied: ['msvcp140.dll'] })
assert.equal(fs.readFileSync(path.join(home, 'msvcp140.dll'), 'utf8'), 'good')
assert.equal(fs.readFileSync(path.join(home, 'vcruntime140.dll'), 'utf8'), 'interpreter-owned')
})
test('a tampered bundled file is refused and nothing is written', () => {
const { home, venv, source } = fixture({ 'msvcp140.dll': { data: 'genuine' } })
fs.writeFileSync(path.join(source, 'msvcp140.dll'), 'tampered')
const result = ensureWindowsVcRuntime({ platform: 'win32', venvRoot: venv, sourceDir: source })
assert.equal(result.status, 'failed')
assert.match((result as { error: string }).error, /does not match its manifest digest/)
assert.equal(fs.existsSync(path.join(home, 'msvcp140.dll')), false)
})
test('an unresolvable python home fails without throwing', () => {
const { venv, source } = fixture({ 'msvcp140.dll': { data: 'crt' } })
fs.writeFileSync(path.join(venv, 'pyvenv.cfg'), 'home = C:\\does-not-exist\n')
assert.equal(ensureWindowsVcRuntime({ platform: 'win32', venvRoot: venv, sourceDir: source }).status, 'failed')
})
test('the shipped CRT set matches its manifest and covers the voice wheels', () => {
const sourceDir = path.join(here, '..', 'assets', 'windows', 'vc-runtime', 'x64')
const manifest = loadVcRuntimeManifest(sourceDir)
const names = manifest.files.map(file => file.name)
for (const required of ['msvcp140.dll', 'msvcp140_1.dll', 'vcruntime140.dll', 'vcruntime140_1.dll']) {
assert.ok(names.includes(required), `${required} must be bundled`)
}
for (const file of manifest.files) {
const bytes = fs.readFileSync(path.join(sourceDir, file.name))
assert.equal(bytes.length, file.size, file.name)
assert.equal(sha256(bytes), file.sha256, file.name)
}
})
test('the Windows package ships the CRT set as an extra resource', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8'))
const resources = pkg.build.win.extraResources
assert.ok(
resources.some(entry => entry.from === 'assets/windows/vc-runtime/x64' && entry.to === 'vc-runtime/x64'),
'win.extraResources must carry vc-runtime/x64'
)
})
test('ensureRuntime places the CRT before handing the venv python to the backend', () => {
const source = fs.readFileSync(path.join(here, 'main.ts'), 'utf8')
const start = source.indexOf('async function ensureRuntime(')
const body = source.slice(start, source.indexOf('\nfunction ', start + 1))
const ensureIdx = body.indexOf('ensureWindowsVcRuntime(')
const commandIdx = body.indexOf('backend.command = getVenvPython(VENV_ROOT)')
assert.notEqual(ensureIdx, -1, 'ensureRuntime must call ensureWindowsVcRuntime')
assert.ok(ensureIdx < commandIdx, 'CRT must be in place before the backend command is chosen')
})
+168
View File
@@ -0,0 +1,168 @@
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
// ── Windows VC++ runtime for the managed Python ─────────────────────────────
//
// Voice features load native wheels (onnxruntime for the wake word,
// ctranslate2 for local speech-to-text) that link against msvcp140.dll and
// msvcp140_1.dll. The managed CPython only ships vcruntime140*.dll, so on a
// Windows install without the system-wide VC++ Redistributable those imports
// fail with "DLL load failed / The specified module could not be found".
//
// The desktop package carries the Microsoft-signed VC143 CRT set and copies it
// next to the managed python.exe before every local backend start. That
// directory is the process image directory, which Windows searches first for
// extension-module dependencies, so a stale msvcp140.dll elsewhere on PATH can
// never win. Existing installs are healed on their next launch.
export interface VcRuntimeFile {
name: string
size: number
sha256: string
// Files the managed CPython already ships. Never overwrite an existing copy:
// the interpreter owns it and replacing a loaded DLL is not possible anyway.
ownedByPython?: boolean
}
export interface VcRuntimeManifest {
schemaVersion: 1
version: string
architecture: string
files: VcRuntimeFile[]
}
export type VcRuntimeResult =
| { status: 'skipped'; reason: string }
| { status: 'ok' | 'repaired'; pythonHome: string; copied: string[] }
| { status: 'failed'; pythonHome: string | null; copied: string[]; error: string }
const FILE_NAME_RE = /^[a-z0-9_]+\.dll$/i
const SHA256_RE = /^[0-9a-f]{64}$/
export function readPythonHome(venvRoot: string): string | null {
let text: string
try {
text = fs.readFileSync(path.join(venvRoot, 'pyvenv.cfg'), 'utf8')
} catch {
return null
}
for (const line of text.split(/\r?\n/)) {
const match = /^\s*home\s*=\s*(.+?)\s*$/i.exec(line)
if (match) {
return match[1]
}
}
return null
}
export function loadVcRuntimeManifest(sourceDir: string): VcRuntimeManifest {
const manifest = JSON.parse(fs.readFileSync(path.join(sourceDir, 'manifest.json'), 'utf8'))
if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.files) || manifest.files.length === 0) {
throw new Error('invalid VC++ runtime manifest')
}
for (const file of manifest.files) {
if (!FILE_NAME_RE.test(file?.name) || !SHA256_RE.test(file?.sha256) || !Number.isInteger(file?.size)) {
throw new Error(`invalid VC++ runtime manifest entry: ${JSON.stringify(file?.name)}`)
}
}
return manifest
}
function sha256File(filePath: string): string {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
}
function matches(filePath: string, file: VcRuntimeFile): boolean {
try {
return fs.statSync(filePath).size === file.size && sha256File(filePath) === file.sha256
} catch {
return false
}
}
function installFile(sourceDir: string, pythonHome: string, file: VcRuntimeFile) {
const source = path.join(sourceDir, file.name)
if (!matches(source, file)) {
throw new Error(`bundled ${file.name} does not match its manifest digest`)
}
const target = path.join(pythonHome, file.name)
const temp = `${target}.aiturk-${process.pid}.tmp`
try {
fs.copyFileSync(source, temp)
if (!matches(temp, file)) {
throw new Error(`copied ${file.name} failed digest verification`)
}
fs.renameSync(temp, target)
} finally {
fs.rmSync(temp, { force: true })
}
}
export function ensureWindowsVcRuntime({
platform,
venvRoot,
sourceDir
}: {
platform: NodeJS.Platform
venvRoot: string
sourceDir: string
}): VcRuntimeResult {
if (platform !== 'win32') {
return { status: 'skipped', reason: 'not windows' }
}
const pythonHome = readPythonHome(venvRoot)
if (!pythonHome || !fs.existsSync(path.join(pythonHome, 'python.exe'))) {
return { status: 'failed', pythonHome, copied: [], error: 'managed python home not found' }
}
const copied: string[] = []
try {
const manifest = loadVcRuntimeManifest(sourceDir)
for (const file of manifest.files) {
const target = path.join(pythonHome, file.name)
if (file.ownedByPython ? fs.existsSync(target) : matches(target, file)) {
continue
}
installFile(sourceDir, pythonHome, file)
copied.push(file.name)
}
} catch (error) {
return { status: 'failed', pythonHome, copied, error: error instanceof Error ? error.message : String(error) }
}
return { status: copied.length ? 'repaired' : 'ok', pythonHome, copied }
}
export function describeVcRuntimeResult(result: VcRuntimeResult): string {
if (result.status === 'skipped') {
return `skipped (${result.reason})`
}
if (result.status === 'failed') {
return `failed for ${result.pythonHome ?? 'unknown python home'}: ${result.error}` +
(result.copied.length ? ` (copied before failure: ${result.copied.join(', ')})` : '')
}
return result.status === 'repaired'
? `installed ${result.copied.join(', ')} into ${result.pythonHome}`
: `verified in ${result.pythonHome}`
}
+12 -2
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.11",
"version": "1.0.0-beta.12",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -290,7 +290,17 @@
"nsis",
"msi"
],
"signAndEditExecutable": false
"signAndEditExecutable": false,
"extraResources": [
{
"from": "assets/windows/vc-runtime/x64",
"to": "vc-runtime/x64",
"filter": [
"*.dll",
"manifest.json"
]
}
]
},
"linux": {
"category": "Development",
+16 -4
View File
@@ -10,9 +10,10 @@
// `electron` and `node-pty` are external (provided by the runtime / staged
// separately via stage-native-deps).
import { build } from 'esbuild'
import { createRequire } from 'node:module'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { mkdirSync, cpSync } from 'node:fs'
import { mkdirSync, cpSync, existsSync } from 'node:fs'
const here = dirname(fileURLToPath(import.meta.url))
const root = resolve(here, '..')
@@ -70,7 +71,18 @@ mkdirSync(resolve(distDir, 'mobile'), { recursive: true })
for (const name of ['transport.mjs', 'peer-identity.mjs', 'style.css', 'qr.mjs']) {
cpSync(resolve(root, '../mobile/src', name), resolve(distDir, 'mobile', name))
}
// A clean `npm ci` hoists the mobile vendor packages to the workspace root, so
// look them up along Node's resolution path from apps/mobile instead of
// assuming apps/mobile/node_modules exists.
const mobileRequire = createRequire(resolve(root, '../mobile/package.json'))
function mobilePackageDir(name) {
for (const base of mobileRequire.resolve.paths(name) ?? []) {
const dir = resolve(base, name)
if (existsSync(resolve(dir, 'package.json'))) return dir
}
throw new Error(`${name} is not installed for apps/mobile; run npm ci at the repository root`)
}
mkdirSync(resolve(distDir, 'mobile/vendor'), { recursive: true })
cpSync(resolve(root, '../mobile/node_modules/qrcode-generator/dist/qrcode.mjs'), resolve(distDir, 'mobile/vendor/qrcode.mjs'))
cpSync(resolve(root, '../mobile/node_modules/jsqr/dist/jsQR.js'), resolve(distDir, 'mobile/vendor/jsQR.js'))
cpSync(resolve(root, '../mobile/node_modules/jsqr/LICENSE'), resolve(distDir, 'mobile/vendor/jsQR-LICENSE.txt'))
cpSync(resolve(mobilePackageDir('qrcode-generator'), 'dist/qrcode.mjs'), resolve(distDir, 'mobile/vendor/qrcode.mjs'))
cpSync(resolve(mobilePackageDir('jsqr'), 'dist/jsQR.js'), resolve(distDir, 'mobile/vendor/jsQR.js'))
cpSync(resolve(mobilePackageDir('jsqr'), 'LICENSE'), resolve(distDir, 'mobile/vendor/jsQR-LICENSE.txt'))
+21 -1
View File
@@ -1474,7 +1474,27 @@ def run_doctor(args):
check_ok(name, "(optional)")
except ImportError:
check_warn(name, "(optional, not installed)")
if sys.platform == "win32":
# Voice wheels can be installed yet unloadable when the Microsoft VC++
# runtime is missing; package presence alone hides that failure.
from tools.windows_native_runtime import native_load_error
for module, name in (
("onnxruntime", "ONNX Runtime (wake word)"),
("ctranslate2", "CTranslate2 (local speech-to-text)"),
):
try:
installed = importlib.util.find_spec(module) is not None
except (ImportError, ValueError):
installed = False
if not installed:
continue
if native_load_error(module) is None:
check_ok(name, "(native runtime loads)")
else:
check_warn(name, "(Microsoft VC++ runtime missing - restart AITURK IDE to restore it)")
_section("Configuration Files")
# Managed scope (administrator-pinned config/env), when present.
managed_scope_check()
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.11",
"version": "1.0.0-beta.12",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",
+13
View File
@@ -625,6 +625,19 @@ def _neutralize_webbrowser(monkeypatch):
return opened
@pytest.fixture(autouse=True)
def _neutralize_native_runtime_probe(monkeypatch):
"""Report native voice wheels as loadable unless a test says otherwise.
The real probe imports onnxruntime/ctranslate2 in a child interpreter, so
its answer depends on the host's VC++ runtime rather than the code under
test. tests/tools/test_windows_native_runtime.py exercises the real probe.
"""
import tools.windows_native_runtime as _native_runtime
monkeypatch.setattr(_native_runtime, "native_load_error", lambda module: None)
@pytest.fixture(autouse=True)
def _neutralize_macos_keychain_creds(request, monkeypatch):
"""Default Anthropic credential resolution away from the real macOS Keychain."""
+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"]
+8
View File
@@ -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
View File
@@ -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,
}
+77
View File
@@ -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})"