139 lines
4.9 KiB
Python
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 ✓")
|