@@ -122,6 +122,29 @@ def _load_python_from_config() -> None:
122122 PYTHON = ML_VENV_PYTHON
123123
124124
125+ OUTPUT_DIR : Path = Path .home () / "AutoNote"
126+
127+
128+ def _load_output_dir_from_config () -> None :
129+ """Load user-configured output directory from config.json."""
130+ global OUTPUT_DIR
131+ config_file = DATA_DIR / "config.json"
132+ if config_file .exists ():
133+ try :
134+ cfg = json .load (open (config_file ))
135+ p = cfg .get ("OUTPUT_DIR" , "" ).strip ()
136+ if p :
137+ OUTPUT_DIR = Path (p )
138+ except Exception :
139+ pass
140+
141+
142+ def _get_output_dir () -> Path :
143+ """Return current output directory, creating it if needed."""
144+ OUTPUT_DIR .mkdir (parents = True , exist_ok = True )
145+ return OUTPUT_DIR
146+
147+
125148COURSES : dict [int , str ] = {} # populated from Canvas API after token is entered
126149
127150_SKIP_KEYWORDS = [
@@ -434,7 +457,7 @@ def _worker() -> None:
434457 stdout = subprocess .PIPE ,
435458 stderr = subprocess .STDOUT ,
436459 text = True ,
437- cwd = PROJECT_DIR ,
460+ cwd = str ( _get_output_dir ()) ,
438461 bufsize = 1 ,
439462 )
440463 for line in state .proc .stdout :
@@ -1174,6 +1197,7 @@ def _save_config_all(data: dict) -> None:
11741197 "canvas_url" : _cfg .get ("CANVAS_URL" , "" ),
11751198 "panopto" : _cfg .get ("PANOPTO_HOST" , "" ),
11761199 "python_path" : _cfg .get ("PYTHON_PATH" , "" ),
1200+ "output_dir" : _cfg .get ("OUTPUT_DIR" , str (Path .home () / "AutoNote" )),
11771201 "canvas" : canvas_file .read_text ().strip () if canvas_file .exists () else "" ,
11781202 "openai" : openai_file .read_text ().strip () if openai_file .exists () else "" ,
11791203 "anthropic" : anthropic_file .read_text ().strip () if anthropic_file .exists () else "" ,
@@ -1196,6 +1220,9 @@ def _mk_tf(key: str, **kwargs) -> ft.TextField:
11961220 tf_python_path = _mk_tf ("python_path" ,
11971221 hint_text = "/path/to/conda/envs/auto-note/bin/python (leave blank for system python3)" ,
11981222 expand = True , dense = True , bgcolor = C_OUTPUT_BG , border_color = C_PRIMARY , text_size = 12 )
1223+ tf_output_dir = _mk_tf ("output_dir" ,
1224+ hint_text = "Output directory for all pipeline files (default: ~/AutoNote)" ,
1225+ expand = True , dense = True , bgcolor = C_OUTPUT_BG , border_color = C_PRIMARY , text_size = 12 )
11991226 tf_canvas = _mk_tf ("canvas" ,
12001227 password = True , can_reveal_password = True ,
12011228 hint_text = "Canvas API token" ,
@@ -1222,6 +1249,7 @@ def _mk_tf(key: str, **kwargs) -> ft.TextField:
12221249 _field_row ("Canvas URL" , tf_canvas_url ),
12231250 _field_row ("Panopto Host" , tf_panopto ),
12241251 _field_row ("Python Path" , tf_python_path ),
1252+ _field_row ("Output Dir" , tf_output_dir ),
12251253 ], spacing = 10 ))
12261254
12271255 refresh_status = ft .Text ("" , size = 11 ,
@@ -1238,6 +1266,7 @@ def _do_refresh():
12381266 "CANVAS_URL" : _v ["canvas_url" ].strip (),
12391267 "PANOPTO_HOST" : _v ["panopto" ].strip (),
12401268 "PYTHON_PATH" : _v ["python_path" ].strip (),
1269+ "OUTPUT_DIR" : _v ["output_dir" ].strip (),
12411270 })
12421271 if _v ["canvas" ].strip ():
12431272 canvas_file .write_text (_v ["canvas" ].strip ())
@@ -1669,9 +1698,14 @@ def _save_all(_):
16691698 "CANVAS_URL" : _v ["canvas_url" ].strip (),
16701699 "PANOPTO_HOST" : _v ["panopto" ].strip (),
16711700 "PYTHON_PATH" : _v ["python_path" ].strip (),
1701+ "OUTPUT_DIR" : _v ["output_dir" ].strip (),
16721702 })
16731703 p = _v ["python_path" ].strip ()
16741704 PYTHON = p if p else _DEFAULT_PYTHON
1705+ global OUTPUT_DIR
1706+ od = _v ["output_dir" ].strip ()
1707+ if od :
1708+ OUTPUT_DIR = Path (od )
16751709 except Exception as e :
16761710 errors .append (f"Connection: { e } " )
16771711 for path , key in [
@@ -1738,16 +1772,233 @@ def _save_all(_):
17381772
17391773# ── Main app ──────────────────────────────────────────────────────────────────
17401774
1741- def main (page : ft .Page ) -> None :
1742- page .title = "AUTO NOTE"
1743- page .theme_mode = ft .ThemeMode .DARK
1744- page .bgcolor = C_SURFACE
1745- page .theme = ft .Theme (color_scheme_seed = ft .Colors .CYAN )
1746- page .window .width = 1080
1747- page .window .height = 780
1748- page .window .min_width = 720
1749- page .window .min_height = 520
1750- page .padding = 0
1775+ def _show_installer (page : ft .Page ) -> None :
1776+ """Full-screen first-run setup wizard shown when venv is not yet installed."""
1777+
1778+ def _on_install_complete () -> None :
1779+ """Called by the installer after the venv is ready; launches main UI."""
1780+ page .controls .clear ()
1781+ _load_python_from_config ()
1782+ _load_output_dir_from_config ()
1783+ _load_courses_from_canvas ()
1784+ _show_main_app (page )
1785+
1786+ # ── Log area ──────────────────────────────────────────────────────────
1787+ inst_log = ft .TextField (
1788+ value = "" , multiline = True , read_only = True , min_lines = 4 , max_lines = 18 ,
1789+ text_size = 11 , bgcolor = C_OUTPUT_BG ,
1790+ border_color = ft .Colors .TRANSPARENT ,
1791+ color = ft .Colors .with_opacity (0.85 , ft .Colors .WHITE ),
1792+ expand = True ,
1793+ )
1794+ inst_status = ft .Text ("Ready to install." , size = 12 , color = C_PRIMARY )
1795+ inst_btn = ft .FilledButton (
1796+ "Install ML Environment" ,
1797+ icon = ft .Icons .DOWNLOAD_OUTLINED ,
1798+ style = ft .ButtonStyle (bgcolor = C_SECONDARY , color = ft .Colors .BLACK ),
1799+ )
1800+
1801+ _base_py_v2 = {"v" : "" }
1802+ tf_base_py2 = ft .TextField (
1803+ hint_text = "Python path (auto-detected — paste here only if auto-detect fails)" ,
1804+ expand = True , dense = True , bgcolor = C_OUTPUT_BG , border_color = C_PRIMARY ,
1805+ text_size = 11 , visible = False ,
1806+ on_change = lambda e : _base_py_v2 .update ({"v" : e .control .value }),
1807+ )
1808+
1809+ def _append (line : str ) -> None :
1810+ inst_log .value = (inst_log .value or "" ) + line + "\n "
1811+ page .update ()
1812+
1813+ def _run_install (_ = None ) -> None :
1814+ inst_btn .disabled = True
1815+ inst_status .value = "Installing…"
1816+ inst_status .color = C_WARN
1817+ page .update ()
1818+
1819+ def _worker ():
1820+ import shutil as _sh
1821+ home = Path .home ()
1822+ base_py = _base_py_v2 ["v" ].strip ()
1823+
1824+ if not base_py :
1825+ _append ("► Probing login shell for Python …" )
1826+ for shell_cmd in [
1827+ ["bash" , "-l" , "-c" ,
1828+ "python3 -c 'import ssl,venv,sys; print(sys.executable)'" ],
1829+ ["zsh" , "-l" , "-c" ,
1830+ "python3 -c 'import ssl,venv,sys; print(sys.executable)'" ],
1831+ ]:
1832+ try :
1833+ r = subprocess .run (shell_cmd , capture_output = True ,
1834+ text = True , timeout = 15 )
1835+ if r .returncode == 0 :
1836+ for line in reversed (r .stdout .strip ().splitlines ()):
1837+ line = line .strip ()
1838+ if line and Path (line ).exists ():
1839+ base_py = line
1840+ break
1841+ if base_py :
1842+ break
1843+ except Exception :
1844+ pass
1845+
1846+ if not base_py :
1847+ for cand in [
1848+ str (home / "miniconda3/bin/python3" ),
1849+ str (home / "miniconda3/bin/python" ),
1850+ str (home / "anaconda3/bin/python3" ),
1851+ str (home / "anaconda3/bin/python" ),
1852+ str (home / "miniforge3/bin/python3" ),
1853+ str (home / "miniforge3/bin/python" ),
1854+ str (home / "mambaforge/bin/python3" ),
1855+ str (home / "mambaforge/bin/python" ),
1856+ str (home / ".local/share/mamba/bin/python3" ),
1857+ "/opt/conda/bin/python3" ,
1858+ "/opt/miniconda3/bin/python3" ,
1859+ "/opt/anaconda3/bin/python3" ,
1860+ _sh .which ("python3" ) or "" ,
1861+ _sh .which ("python" ) or "" ,
1862+ "/usr/bin/python3" ,
1863+ "/usr/local/bin/python3" ,
1864+ ]:
1865+ if not cand or not Path (cand ).exists ():
1866+ continue
1867+ r = subprocess .run ([cand , "-c" , "import ssl, venv" ],
1868+ capture_output = True , timeout = 5 )
1869+ if r .returncode == 0 :
1870+ base_py = cand
1871+ break
1872+
1873+ if not base_py :
1874+ _append ("ERROR: No Python 3 with SSL support found." )
1875+ _append (" Paste your Python path in the field below and click Install again." )
1876+ tf_base_py2 .visible = True
1877+ inst_status .value = "✗ Python not found — enter path below"
1878+ inst_status .color = C_ERROR
1879+ inst_btn .disabled = False
1880+ page .update ()
1881+ return
1882+
1883+ _append (f"► Using Python: { base_py } " )
1884+
1885+ import shutil as _sh2
1886+ if ML_VENV_DIR .exists ():
1887+ _append ("► Removing old venv …" )
1888+ _sh2 .rmtree (str (ML_VENV_DIR ))
1889+ _append (f"► Creating venv at { ML_VENV_DIR } …" )
1890+ r = subprocess .run ([base_py , "-m" , "venv" , str (ML_VENV_DIR )],
1891+ capture_output = True , text = True )
1892+ if r .returncode != 0 :
1893+ _append ("STDERR: " + r .stderr .strip ())
1894+ _append ("ERROR: venv creation failed." )
1895+ inst_status .value = "✗ Setup failed"
1896+ inst_status .color = C_ERROR
1897+ inst_btn .disabled = False
1898+ page .update ()
1899+ return
1900+ _append (" venv created." )
1901+
1902+ pip = str (ML_VENV_DIR / (
1903+ "Scripts/pip.exe" if sys .platform == "win32" else "bin/pip"
1904+ ))
1905+ _append ("► Upgrading pip …" )
1906+ subprocess .run ([pip , "install" , "--upgrade" , "pip" ],
1907+ capture_output = True , text = True )
1908+
1909+ cuda = _detect_cuda ()
1910+ idx = _torch_index_url (cuda )
1911+ if cuda :
1912+ _append (f"► CUDA { cuda [0 ]} .{ cuda [1 ]} detected — installing torch (GPU) …" )
1913+ else :
1914+ _append ("► No GPU detected — installing torch (CPU) …" )
1915+ torch_cmd = [pip , "install" , "torch" ]
1916+ if idx :
1917+ torch_cmd += ["--index-url" , idx ]
1918+ proc = subprocess .Popen (torch_cmd , stdout = subprocess .PIPE ,
1919+ stderr = subprocess .STDOUT , text = True )
1920+ for line in proc .stdout :
1921+ _append (line .rstrip ())
1922+ proc .wait ()
1923+
1924+ _append ("► Installing ML packages …" )
1925+ proc = subprocess .Popen ([pip , "install" ] + _ML_PACKAGES ,
1926+ stdout = subprocess .PIPE ,
1927+ stderr = subprocess .STDOUT , text = True )
1928+ for line in proc .stdout :
1929+ _append (line .rstrip ())
1930+ proc .wait ()
1931+
1932+ _append ("► Installing Playwright browsers …" )
1933+ proc = subprocess .Popen (
1934+ [ML_VENV_PYTHON , "-m" , "playwright" , "install" , "chromium" ],
1935+ stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True )
1936+ for line in proc .stdout :
1937+ _append (line .rstrip ())
1938+ proc .wait ()
1939+
1940+ global PYTHON
1941+ PYTHON = ML_VENV_PYTHON
1942+ _append ("\n ✓ Installation complete!" )
1943+ inst_status .value = "✓ Done — launching app…"
1944+ inst_status .color = C_SUCCESS
1945+ inst_btn .disabled = False
1946+ page .update ()
1947+ import time as _t ; _t .sleep (1 )
1948+ _on_install_complete ()
1949+
1950+ threading .Thread (target = _worker , daemon = True ).start ()
1951+
1952+ inst_btn .on_click = _run_install
1953+
1954+ page .add (
1955+ ft .Container (
1956+ content = ft .Column (
1957+ controls = [
1958+ ft .Container (height = 40 ),
1959+ ft .Row (
1960+ controls = [ft .Icon (ft .Icons .AUTO_AWESOME , color = C_PRIMARY , size = 32 ),
1961+ ft .Text ("AUTO NOTE" , size = 28 ,
1962+ weight = ft .FontWeight .BOLD , color = ft .Colors .WHITE )],
1963+ alignment = ft .MainAxisAlignment .CENTER , spacing = 12 ,
1964+ ),
1965+ ft .Container (height = 8 ),
1966+ ft .Text (
1967+ "First-time setup: install the ML environment to get started." ,
1968+ size = 13 , color = ft .Colors .with_opacity (0.6 , ft .Colors .WHITE ),
1969+ text_align = ft .TextAlign .CENTER ,
1970+ ),
1971+ ft .Container (height = 24 ),
1972+ _card (ft .Column (controls = [
1973+ ft .Text ("ML Environment Setup" , size = 14 ,
1974+ weight = ft .FontWeight .BOLD , color = ft .Colors .WHITE ),
1975+ ft .Text (
1976+ "This will create ~/.auto_note/venv/ and install torch, "
1977+ "faster-whisper, sentence-transformers and all pipeline "
1978+ "dependencies. Internet connection required. Takes ~5 min." ,
1979+ size = 11 ,
1980+ color = ft .Colors .with_opacity (0.55 , ft .Colors .WHITE ),
1981+ ),
1982+ ft .Container (height = 6 ),
1983+ ft .Row (controls = [inst_status ], spacing = 8 ),
1984+ inst_btn ,
1985+ tf_base_py2 ,
1986+ ft .Container (content = inst_log , expand = True ),
1987+ ], spacing = 10 )),
1988+ ],
1989+ horizontal_alignment = ft .CrossAxisAlignment .CENTER ,
1990+ scroll = ft .ScrollMode .AUTO ,
1991+ expand = True ,
1992+ ),
1993+ expand = True ,
1994+ padding = ft .Padding .symmetric (horizontal = 80 , vertical = 0 ),
1995+ )
1996+ )
1997+ page .update ()
1998+
1999+
2000+ def _show_main_app (page : ft .Page ) -> None :
2001+ """Build and display the full navigation shell (called after install or on normal launch)."""
17512002
17522003 # One shared console per session (preserves history across page switches)
17532004 console = OutputConsole (page )
@@ -1774,12 +2025,6 @@ def _build_pages() -> list:
17742025 on_courses_changed = lambda : _rebuild_ref [0 ] and _rebuild_ref [0 ]()),
17752026 ]
17762027
1777- # Load user-configured Python interpreter (e.g. conda env) from config
1778- _load_python_from_config ()
1779-
1780- # Try to populate courses immediately if credentials are already on disk
1781- _load_courses_from_canvas ()
1782-
17832028 pages = _build_pages ()
17842029
17852030 # page_content swaps between tab pages; console stays fixed at the bottom
@@ -1889,6 +2134,32 @@ def _rebuild() -> None:
18892134 spacing = 0 ,
18902135 )
18912136 )
2137+ page .update ()
2138+
2139+
2140+ def main (page : ft .Page ) -> None :
2141+ page .title = "AUTO NOTE"
2142+ page .theme_mode = ft .ThemeMode .DARK
2143+ page .bgcolor = C_SURFACE
2144+ page .theme = ft .Theme (color_scheme_seed = ft .Colors .CYAN )
2145+ page .window .width = 1080
2146+ page .window .height = 780
2147+ page .window .min_width = 720
2148+ page .window .min_height = 520
2149+ page .padding = 0
2150+
2151+ # Load user-configured settings from config
2152+ _load_python_from_config ()
2153+ _load_output_dir_from_config ()
2154+
2155+ # ── First-run installer ────────────────────────────────────────────────
2156+ if not Path (ML_VENV_PYTHON ).exists ():
2157+ _show_installer (page )
2158+ return
2159+
2160+ # Try to populate courses immediately if credentials are already on disk
2161+ _load_courses_from_canvas ()
2162+ _show_main_app (page )
18922163
18932164
18942165if __name__ == "__main__" :
0 commit comments