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
52107def _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
66125COURSES : 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