Files
2026-08-25 05:42:36 +03:00

139 lines
4.9 KiB
Python

"""
MARK XL — Dependency auto-installer.
Called automatically on first launch and after engine reconfiguration.
Installs only the packages that are actually missing, then exits cleanly.
"""
from __future__ import annotations
import importlib.util
import platform
import subprocess
import sys
from typing import Callable
# ── Package lists ─────────────────────────────────────────────────────────
# Each entry: (import_name, pip_package_name)
_CORE: list[tuple[str, str]] = [
("psutil", "psutil"),
("PIL", "pillow"),
("sounddevice", "sounddevice"),
("numpy", "numpy"),
("requests", "requests"),
("bs4", "beautifulsoup4"),
("ddgs", "ddgs"),
("pyautogui", "pyautogui"),
("pyperclip", "pyperclip"),
("pygetwindow", "pygetwindow"),
("mss", "mss"),
("cv2", "opencv-python"),
("soundfile", "soundfile"),
("miniaudio", "miniaudio"),
("send2trash", "send2trash"),
("pptx", "python-pptx"),
("youtube_transcript_api", "youtube-transcript-api"),
]
# Windows-only (pywinauto, pycaw, win10toast, comtypes)
_WINDOWS: list[tuple[str, str]] = [
("comtypes", "comtypes"),
("pycaw", "pycaw"),
("win10toast", "win10toast"),
("pywinauto", "pywinauto"),
]
# STT engine packages
_STT: dict[str, list[tuple[str, str]]] = {
"whisper": [("faster_whisper", "faster-whisper")],
"vosk": [("vosk", "vosk")],
}
# TTS engine packages
_TTS: dict[str, list[tuple[str, str]]] = {
"edgetts": [("edge_tts", "edge-tts")],
# kokoro>=0.9 dropped AlbertModel/AutoModel from transformers — version pin is critical
"kokoro": [("kokoro", "kokoro>=0.9"), ("soundfile", "soundfile")],
"elevenlabs": [], # uses only requests, already in core
}
# ── Helpers ───────────────────────────────────────────────────────────────
def _available(module: str) -> bool:
"""Return True if the module can be imported (no actual import)."""
return importlib.util.find_spec(module) is not None
def _pip(package: str, log: Callable | None = None) -> bool:
if log:
log(f"SYS: pip install {package} …")
result = subprocess.run(
[
sys.executable, "-m", "pip", "install", package,
"--quiet", "--disable-pip-version-check",
],
capture_output=True,
)
ok = result.returncode == 0
if not ok and log:
stderr = result.stderr.decode(errors="replace").strip()
log(f"ERR: {package} install failed — {stderr[:140]}")
return ok
# ── Public API ────────────────────────────────────────────────────────────
def install_for_config(config: dict, log: Callable | None = None) -> None:
"""
Install all missing packages required by *config*.
Blocking — always call from a background thread.
Progress is reported via the optional *log* callback (receives a str).
"""
stt = config.get("stt_engine", "whisper").lower()
tts = config.get("tts_engine", "edgetts").lower()
needed: list[tuple[str, str]] = list(_CORE)
needed += _STT.get(stt, [])
needed += _TTS.get(tts, [])
if platform.system() == "Windows":
needed += _WINDOWS
# Deduplicate (preserve order, key = pip name)
seen: set[str] = set()
unique: list[tuple[str, str]] = []
for mod, pkg in needed:
if pkg not in seen:
seen.add(pkg)
unique.append((mod, pkg))
missing = [(mod, pkg) for mod, pkg in unique if not _available(mod)]
if not missing:
if log:
log("SYS: All dependencies already installed ✓")
return
pkg_names = ", ".join(p for _, p in missing)
if log:
log(f"SYS: Installing {len(missing)} package(s): {pkg_names}")
for _mod, pkg in missing:
_pip(pkg, log)
# Playwright: install the package + download Chromium browser
if not _available("playwright"):
_pip("playwright", log)
if log:
log("SYS: Downloading Playwright browser (Chromium, ~150 MB — one-time)…")
subprocess.run(
[sys.executable, "-m", "playwright", "install", "chromium"],
capture_output=True,
)
if log:
log("SYS: Playwright browser ready.")
if log:
log("SYS: All dependencies ready ✓")