Skip to content

Commit a21edfa

Browse files
committed
Add auto ML environment setup — no manual conda required
New "ML Environment" card in Settings: - Creates ~/.auto_note/venv/ using system python3 - Auto-detects CUDA version via nvidia-smi and installs the matching torch build (cu128/cu126/cu124/cu121 or CPU) - pip installs all pipeline deps: faster-whisper, sentence-transformers, faiss-cpu, pymupdf, python-pptx, python-docx, openai, anthropic, google-generativeai, playwright, canvasapi, tqdm, etc. - Installs Playwright Chromium browsers - Streams install log live in the UI - On completion, automatically sets PYTHON to the venv interpreter - At startup, auto-uses the venv if it exists (no manual Python Path needed)
1 parent 22a81f0 commit a21edfa

1 file changed

Lines changed: 219 additions & 1 deletion

File tree

gui.py

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,64 @@
4848

4949
_DEFAULT_PYTHON = PYTHON # auto-detected fallback; may be overridden by user config
5050

51+
ML_VENV_DIR = DATA_DIR / "venv"
52+
ML_VENV_PYTHON = str(ML_VENV_DIR / (
53+
"Scripts/python.exe" if sys.platform == "win32" else "bin/python"
54+
))
55+
56+
# ML packages needed by the pipeline scripts (GUI requirements are bundled separately)
57+
_ML_PACKAGES = [
58+
"tqdm",
59+
"faster-whisper",
60+
"sentence-transformers",
61+
"faiss-cpu",
62+
"pymupdf",
63+
"python-pptx",
64+
"python-docx",
65+
"openai",
66+
"anthropic",
67+
"google-generativeai",
68+
"requests",
69+
"pillow",
70+
"httpx",
71+
"playwright",
72+
"canvasapi",
73+
]
74+
75+
76+
def _detect_cuda() -> tuple[int, int] | None:
77+
"""Return (major, minor) CUDA version from nvidia-smi, or None if no GPU."""
78+
try:
79+
r = subprocess.run(
80+
["nvidia-smi"], capture_output=True, text=True, timeout=10
81+
)
82+
if r.returncode == 0:
83+
m = re.search(r"CUDA Version:\s*(\d+)\.(\d+)", r.stdout)
84+
if m:
85+
return (int(m.group(1)), int(m.group(2)))
86+
return (12, 0) # nvidia-smi works but version unreadable → assume 12.x
87+
except Exception:
88+
pass
89+
return None
90+
91+
92+
def _torch_index_url(cuda: tuple[int, int] | None) -> str | None:
93+
"""Return the PyTorch extra-index-url for the detected CUDA version."""
94+
if cuda is None:
95+
return None # CPU build from PyPI
96+
major, minor = cuda
97+
version = major * 10 + minor # 12.8 → 128
98+
if version >= 128:
99+
return "https://download.pytorch.org/whl/cu128"
100+
if version >= 126:
101+
return "https://download.pytorch.org/whl/cu126"
102+
if version >= 124:
103+
return "https://download.pytorch.org/whl/cu124"
104+
return "https://download.pytorch.org/whl/cu121"
105+
51106

52107
def _load_python_from_config() -> None:
53-
"""Override PYTHON global with the user-configured interpreter path."""
108+
"""Override PYTHON global with the user-configured path or the managed venv."""
54109
global PYTHON
55110
config_file = DATA_DIR / "config.json"
56111
if config_file.exists():
@@ -59,8 +114,12 @@ def _load_python_from_config() -> None:
59114
p = cfg.get("PYTHON_PATH", "").strip()
60115
if p:
61116
PYTHON = p
117+
return
62118
except Exception:
63119
pass
120+
# Auto-use managed venv if it exists and no explicit path is configured
121+
if Path(ML_VENV_PYTHON).exists():
122+
PYTHON = ML_VENV_PYTHON
64123

65124

66125
COURSES: dict[int, str] = {} # populated from Canvas API after token is entered
@@ -1217,6 +1276,164 @@ def _do_refresh():
12171276
], spacing=8),
12181277
], spacing=10))
12191278

1279+
# ── ML Environment ────────────────────────────────────────────────────────
1280+
1281+
_venv_exists = Path(ML_VENV_PYTHON).exists()
1282+
env_status = ft.Text(
1283+
("✓ Installed at " + str(ML_VENV_DIR)) if _venv_exists else "Not installed",
1284+
size=11,
1285+
color=C_SUCCESS if _venv_exists else ft.Colors.with_opacity(0.5, ft.Colors.WHITE),
1286+
)
1287+
env_log = ft.TextField(
1288+
value="", multiline=True, read_only=True, min_lines=1, max_lines=12,
1289+
text_size=11, bgcolor=C_OUTPUT_BG, border_color=ft.Colors.TRANSPARENT,
1290+
color=ft.Colors.with_opacity(0.85, ft.Colors.WHITE),
1291+
expand=True, visible=False,
1292+
)
1293+
env_setup_btn = ft.FilledButton(
1294+
"Install ML Environment",
1295+
icon=ft.Icons.DOWNLOAD_OUTLINED,
1296+
style=ft.ButtonStyle(bgcolor=C_SECONDARY, color=ft.Colors.BLACK),
1297+
)
1298+
env_reinstall_btn = ft.OutlinedButton(
1299+
"Reinstall",
1300+
icon=ft.Icons.REFRESH,
1301+
style=ft.ButtonStyle(side=ft.BorderSide(1, C_SECONDARY), color=C_SECONDARY),
1302+
visible=_venv_exists,
1303+
)
1304+
1305+
def _append_log(line: str) -> None:
1306+
env_log.value = (env_log.value or "") + line + "\n"
1307+
env_log.visible = True
1308+
page.update()
1309+
1310+
def _run_env_setup(_=None) -> None:
1311+
global PYTHON
1312+
env_setup_btn.disabled = True
1313+
env_reinstall_btn.disabled = True
1314+
env_log.value = ""
1315+
env_log.visible = True
1316+
env_status.value = "Setting up…"
1317+
env_status.color = C_WARN
1318+
page.update()
1319+
1320+
def _worker():
1321+
global PYTHON
1322+
base_py = _DEFAULT_PYTHON
1323+
if not Path(base_py).exists():
1324+
import shutil as _sh
1325+
base_py = _sh.which("python3") or _sh.which("python") or ""
1326+
if not base_py:
1327+
_append_log("ERROR: No system Python 3 found. Install Python 3 first.")
1328+
env_status.value = "✗ Setup failed — Python 3 not found"
1329+
env_status.color = C_ERROR
1330+
env_setup_btn.disabled = False
1331+
env_reinstall_btn.disabled = False
1332+
page.update()
1333+
return
1334+
1335+
# Step 1 — create venv
1336+
_append_log(f"► Creating venv at {ML_VENV_DIR} …")
1337+
r = subprocess.run(
1338+
[base_py, "-m", "venv", str(ML_VENV_DIR)],
1339+
capture_output=True, text=True,
1340+
)
1341+
if r.returncode != 0:
1342+
_append_log("STDERR: " + r.stderr.strip())
1343+
_append_log("ERROR: venv creation failed.")
1344+
env_status.value = "✗ Setup failed"
1345+
env_status.color = C_ERROR
1346+
env_setup_btn.disabled = False
1347+
env_reinstall_btn.disabled = False
1348+
page.update()
1349+
return
1350+
_append_log(" venv created.")
1351+
1352+
pip = str(ML_VENV_DIR / (
1353+
"Scripts/pip.exe" if sys.platform == "win32" else "bin/pip"
1354+
))
1355+
1356+
# Step 2 — upgrade pip
1357+
_append_log("► Upgrading pip …")
1358+
subprocess.run([pip, "install", "--upgrade", "pip"],
1359+
capture_output=True, text=True)
1360+
1361+
# Step 3 — detect CUDA and install torch
1362+
cuda = _detect_cuda()
1363+
idx = _torch_index_url(cuda)
1364+
if cuda:
1365+
_append_log(f"► CUDA {cuda[0]}.{cuda[1]} detected — installing torch (GPU) …")
1366+
else:
1367+
_append_log("► No GPU detected — installing torch (CPU) …")
1368+
torch_cmd = [pip, "install", "torch"]
1369+
if idx:
1370+
torch_cmd += ["--index-url", idx]
1371+
proc = subprocess.Popen(
1372+
torch_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
1373+
)
1374+
for line in proc.stdout:
1375+
_append_log(line.rstrip())
1376+
proc.wait()
1377+
1378+
# Step 4 — install remaining ML packages
1379+
_append_log("► Installing ML packages …")
1380+
proc = subprocess.Popen(
1381+
[pip, "install"] + _ML_PACKAGES,
1382+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
1383+
)
1384+
for line in proc.stdout:
1385+
_append_log(line.rstrip())
1386+
proc.wait()
1387+
1388+
# Step 5 — playwright browsers
1389+
_append_log("► Installing Playwright browsers …")
1390+
venv_py = ML_VENV_PYTHON
1391+
proc = subprocess.Popen(
1392+
[venv_py, "-m", "playwright", "install", "chromium"],
1393+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
1394+
)
1395+
for line in proc.stdout:
1396+
_append_log(line.rstrip())
1397+
proc.wait()
1398+
1399+
# Done — activate venv python
1400+
PYTHON = ML_VENV_PYTHON
1401+
try:
1402+
_save_config_all({"PYTHON_PATH": ""}) # clear manual override; venv is auto-detected
1403+
except Exception:
1404+
pass
1405+
env_status.value = "✓ Installed at " + str(ML_VENV_DIR)
1406+
env_status.color = C_SUCCESS
1407+
env_setup_btn.disabled = False
1408+
env_reinstall_btn.disabled = False
1409+
env_reinstall_btn.visible = True
1410+
_append_log("\n✓ ML environment ready. You can now run the pipeline.")
1411+
page.update()
1412+
1413+
threading.Thread(target=_worker, daemon=True).start()
1414+
1415+
env_setup_btn.on_click = _run_env_setup
1416+
env_reinstall_btn.on_click = _run_env_setup
1417+
1418+
env_card = _card(ft.Column(controls=[
1419+
ft.Text("ML Environment", size=13,
1420+
weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE),
1421+
ft.Text(
1422+
"Creates a virtual environment at ~/.auto_note/venv/ and installs all "
1423+
"pipeline dependencies (torch, faster-whisper, sentence-transformers, …) "
1424+
"automatically. Only needed once.",
1425+
size=11, color=ft.Colors.with_opacity(0.5, ft.Colors.WHITE),
1426+
),
1427+
ft.Container(height=4),
1428+
ft.Row(controls=[
1429+
ft.Text("Status:", size=11,
1430+
color=ft.Colors.with_opacity(0.5, ft.Colors.WHITE), width=60),
1431+
env_status,
1432+
], spacing=8),
1433+
ft.Row(controls=[env_setup_btn, env_reinstall_btn], spacing=10),
1434+
env_log,
1435+
], spacing=10))
1436+
12201437
# ── Tunable Constants ─────────────────────────────────────────────────────
12211438

12221439
_SCRIPT_COLORS = {
@@ -1429,6 +1646,7 @@ def _save_all(_):
14291646
_section_title("Settings", ft.Icons.SETTINGS_OUTLINED),
14301647
conn_card,
14311648
keys_card,
1649+
env_card,
14321650
const_card,
14331651
ft.Container(height=8),
14341652
ft.Row(controls=[

0 commit comments

Comments
 (0)