Skip to content

Commit df7472e

Browse files
committed
Add first-run installer wizard and user-configurable output directory
First-run installer: - On first launch (no venv), shows a full-screen setup wizard instead of the main app; proceeds to main UI automatically after install - Installer uses the same Python-detection logic (login shell probe first) Output directory: - New "Output Dir" field in Settings → Connection (default: ~/AutoNote) - Saved as OUTPUT_DIR in config.json; loaded at startup - All pipeline subprocesses run with cwd=OUTPUT_DIR so course folders (e.g. 85397/materials/) are created there instead of the temp AppImage dir - Fixed: "No such file or directory: /tmp/.mount_.../85397/materials"
1 parent f855b3d commit df7472e

1 file changed

Lines changed: 288 additions & 17 deletions

File tree

gui.py

Lines changed: 288 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
125148
COURSES: 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

18942165
if __name__ == "__main__":

0 commit comments

Comments
 (0)