From 344da9a6a78d669728f54d338bf7a8e0f37ac9ad Mon Sep 17 00:00:00 2001 From: Daniel Joaquin Trujillo <54636507+danieljtrujillo@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:52:32 -0700 Subject: [PATCH 1/2] settings: add local-first model onboarding slice --- backend/modules/storage/router.py | 88 ++ backend/modules/storage/store.py | 11 +- ...-12-noob-friendly-model-onboarding-plan.md | 984 ++++++++++++++++++ docs/reports/feature-doc-coverage-report.md | 2 +- docs/reports/feature-doc-coverage.json | 4 +- docs/screenshots/manifest.json | 2 +- .../src/components/layout/SettingsModal.tsx | 233 +++-- frontend/src/components/ui/PathInput.tsx | 93 ++ frontend/src/lib/storageClient.ts | 13 + tests/test_storage_store.py | 46 + 10 files changed, 1359 insertions(+), 117 deletions(-) create mode 100644 docs/plans/2026-06-12-noob-friendly-model-onboarding-plan.md create mode 100644 frontend/src/components/ui/PathInput.tsx create mode 100644 tests/test_storage_store.py diff --git a/backend/modules/storage/router.py b/backend/modules/storage/router.py index 46cd73e..6e7465a 100644 --- a/backend/modules/storage/router.py +++ b/backend/modules/storage/router.py @@ -8,6 +8,8 @@ GET /local-only is no-download mode on? PUT /local-only toggle no-download mode {enabled} POST /open open a known location in the OS file explorer + POST /pick-folder open a native folder picker on the local machine + POST /pick-file open a native file picker on the local machine Sizes come from a recursive walk cached for 60 seconds per path (pass ``refresh=1`` to force). The WSL-side Magenta locations are probed through @@ -47,6 +49,7 @@ _WSL_TIMEOUT = 10 _wsl_cache: dict[str, tuple[float, dict]] = {} +_PICKER_TIMEOUT_SECONDS = 300 def _dir_stats(path: Path) -> dict: @@ -324,6 +327,11 @@ class OpenBody(BaseModel): path: str +class PickerResult(BaseModel): + path: str | None = None + cancelled: bool = False + + def _allowed_open_roots() -> list[str]: roots = [str(p) for _, _, p in _windows_locations()] roots += [e["path"] for e in get_registry().list_checkpoints()] @@ -352,3 +360,83 @@ def storage_open(body: OpenBody) -> dict: except OSError as e: raise HTTPException(404, f"Could not open {target!r}: {e}") from e return {"opened": target} + + +def _run_windows_picker(script: str) -> PickerResult: + """Run a small STA PowerShell picker and return the selected local path. + + The frontend cannot read absolute folder paths from a normal browser file + input. Because theDAW runs as a trusted local app, Settings asks the backend + to show the native Windows dialog. Scripts are static constants so no user + text is interpolated into PowerShell. + """ + if sys.platform != "win32": + raise HTTPException(501, "Native path picker is implemented for Windows only.") + try: + result = subprocess.run( + [ + "powershell.exe", + "-NoProfile", + "-STA", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=_PICKER_TIMEOUT_SECONDS, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + check=False, + ) + except subprocess.TimeoutExpired as e: + raise HTTPException(408, "Path picker timed out.") from e + except OSError as e: + raise HTTPException(500, f"Could not open path picker: {e}") from e + + if result.returncode == 3: + return PickerResult(cancelled=True) + if result.returncode != 0: + detail = (result.stderr or result.stdout or "path picker failed").strip() + raise HTTPException(500, detail[:500]) + picked = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "" + if not picked: + return PickerResult(cancelled=True) + return PickerResult(path=picked, cancelled=False) + + +@router.post("/pick-folder") +def storage_pick_folder() -> dict: + script = r""" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +Add-Type -AssemblyName System.Windows.Forms +$dialog = New-Object System.Windows.Forms.FolderBrowserDialog +$dialog.Description = 'Select a folder for theDAW' +$dialog.ShowNewFolderButton = $true +if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + Write-Output $dialog.SelectedPath +} else { + exit 3 +} +""" + return _run_windows_picker(script).model_dump() + + +@router.post("/pick-file") +def storage_pick_file() -> dict: + script = r""" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +Add-Type -AssemblyName System.Windows.Forms +$dialog = New-Object System.Windows.Forms.OpenFileDialog +$dialog.Title = 'Select a file for theDAW' +$dialog.Filter = 'Model/config files (*.safetensors;*.json)|*.safetensors;*.json|All files (*.*)|*.*' +$dialog.Multiselect = $false +if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + Write-Output $dialog.FileName +} else { + exit 3 +} +""" + return _run_windows_picker(script).model_dump() diff --git a/backend/modules/storage/store.py b/backend/modules/storage/store.py index a288e05..ea04e47 100644 --- a/backend/modules/storage/store.py +++ b/backend/modules/storage/store.py @@ -9,8 +9,10 @@ ``local_only`` mirrors the ``SA3_LOCAL_ONLY`` environment switch that ``stable_audio_3.model_configs`` reads on every resolve: when on, model resolution never touches the network and fails loudly instead of downloading. -The flag is applied to ``os.environ`` at import time so a backend restart -keeps the user's choice. +Fresh installs default this safety switch ON so beginners do not trigger a +surprise multi-GB model download; an explicit persisted user choice still wins. +The flag is applied to ``os.environ`` at import time so a backend restart keeps +the user's choice. """ from __future__ import annotations @@ -32,7 +34,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[3] REGISTRY_PATH = PROJECT_ROOT / "data" / "local_checkpoints.json" -_DEFAULT: dict[str, Any] = {"local_only": False, "checkpoints": []} +_DEFAULT: dict[str, Any] = {"local_only": True, "checkpoints": []} class CheckpointRegistry: @@ -55,7 +57,8 @@ def _load(self) -> dict[str, Any]: if not isinstance(raw, dict): return deepcopy(_DEFAULT) merged = deepcopy(_DEFAULT) - merged["local_only"] = bool(raw.get("local_only", False)) + if "local_only" in raw: + merged["local_only"] = bool(raw.get("local_only")) entries = raw.get("checkpoints") if isinstance(entries, list): merged["checkpoints"] = [ diff --git a/docs/plans/2026-06-12-noob-friendly-model-onboarding-plan.md b/docs/plans/2026-06-12-noob-friendly-model-onboarding-plan.md new file mode 100644 index 0000000..3e75723 --- /dev/null +++ b/docs/plans/2026-06-12-noob-friendly-model-onboarding-plan.md @@ -0,0 +1,984 @@ +# Noob-friendly Models, Downloads, and Settings Onboarding Plan + +**Date:** 2026-06-12 +**Scope:** Settings modal, model/API readiness, local checkpoint onboarding, safe no-download defaults, path pickers, compact module tiles. +**Primary user goal:** make the repo/app easy for beginners to download, launch, configure models, and understand what is installed without reading dense docs or typing paths by hand. + +--- + +## 0. Source feature requests + +The requested behavior came from the user during planning: + +> in settings menu, I want there to be a models section where it has magenta, suno, stable, demucs, any other model or model API input area. Display what is hooked up, healthy, needs config file, w/e. Keep it compact and put it DIRECTLY below the restart and shutdown. Local only (never download) should be turned on by default. If a user tries to use the system with no models, it will warn them and take them to the area to select one (some or all, their choice) +> +> The Checkpoint thing shouldnt only be a text input, it should have a folder button where you can navigate to it via clicking, not typing. +> +> Where everything lives is a stupid label, change it to Location or similar. Make it so a hover over of the size will show what models are in the directory, where, and their size. It should also show what the currently downloaded options are for the user, and recommend which one to use if there are multiples. +> +> Models needs to explain how and where to get the config.JSON, or have a button to generate one if not found or something. +> +> Directly below models should be Layout Settings +> +> Below that should be all the Modules. Tiles, not a list. I want minimal scrolling, as much on every row as possible (but dont throw esthetic and 'clenliness' to the wayside, make good UI/UX design choices) +> +> Every place where there is a location that a user can/should input something should be a type location, and click to folder location type input. +> +> I know I suggested the dropdowns, but we should try to not have those, I want the user to instantly be able to see what they have installed, what is set active, where it is located, etc. + +--- + +## 1. Current implementation snapshot + +Relevant files inspected during planning: + +- `frontend/src/components/layout/SettingsModal.tsx` + - Current order: pinned **Admin** → `SunoKeySettings` → `LayoutSettingsSection` → Background features → VJ Recording → `StorageSettingsSection` → Backend Modules. + - `StorageSettingsSection` already has: + - Local-only toggle. + - Stable model catalog chips (`local`, `cached`, `download`). + - Registered local checkpoints. + - Raw checkpoint path text input. + - “Where everything lives” location rows. + - Hugging Face cache breakdown. + - Backend modules are currently grouped in collapsible list sections, not compact tiles. +- `frontend/src/lib/storageClient.ts` + - Existing client wrappers: + - `fetchCheckpoints()` + - `addCheckpoint()` + - `removeCheckpoint()` + - `fetchLocations()` + - `fetchHfCache()` + - `setLocalOnly()` + - `openLocation()` +- `backend/modules/storage/store.py` + - `local_only` currently defaults to `False`. + - Registry persists to `data/local_checkpoints.json`. + - `SA3_LOCAL_ONLY` env is updated from registry state. +- `backend/modules/storage/router.py` + - Existing endpoints: + - `GET /api/storage/locations` + - `GET /api/storage/hf-cache` + - `GET /api/storage/checkpoints` + - `POST /api/storage/checkpoints` + - `DELETE /api/storage/checkpoints/{ck_id}` + - `GET /api/storage/local-only` + - `PUT /api/storage/local-only` + - `POST /api/storage/open` + - No native folder picker endpoint yet. + - Location rows do not yet include model inventories or recommendations. +- Existing health/config endpoints to reuse: + - Stable/local checkpoints: `/api/storage/checkpoints`, `/api/model-info`, `/api/model/load` + - Magenta: `/api/magenta/probe`, `/api/magenta/engine/status` + - Suno: `/api/suno/status` + - Demucs/stems: `/api/stems/probe`, `/api/stems/status` + - Modules: `/api/modules/all` + +--- + +## 2. Deliverables + +1. Durable Settings UI reshuffle: + - Admin remains pinned. + - Models section appears directly under Restart / Shutdown. + - Layout Settings appears directly under Models. + - Modules appear directly under Layout Settings as compact tiles. +2. Safe noob default: + - Local-only / never-download defaults ON for fresh installs. + - Explicit existing user choice is preserved. +3. Model/API readiness view: + - Stable Audio, registered checkpoints, Magenta, Suno, Demucs/stems, MIDI where practical. + - Shows connected/healthy/needs key/needs setup/missing config/download blocked. + - Shows active/current/recommended model where practical. +4. Checkpoint onboarding improvements: + - Path input supports typing and folder/file browsing. + - Explains config JSON requirements. + - Adds inspect-before-register feedback. + - Optional safe config generation/copy for recognized known SA3 variants only. +5. Location clarity: + - Rename “Where everything lives” to “Locations” or “Storage Locations.” + - Size hover details show model/repo names, paths, and sizes. + - Downloaded/current options are visible and recommendations are clear. +6. Missing-model warning flow: + - If a user tries to generate with no usable model/API, warn and open Settings → Models. +7. Path picker pattern: + - Any user-editable location path gets a typed location input plus click-to-folder/file picker where feasible. +8. Documentation + screenshot coverage updated after feature changes. + +--- + +## 3. Success criteria + +- Fresh install has local-only enabled by default. +- Existing `data/local_checkpoints.json` with `local_only: false` remains false. +- Settings opens with Models immediately visible below Admin controls. +- User can add a checkpoint without typing a path by using a folder/file picker. +- Checkpoint registration failure explains exactly what is missing. +- Users can identify available/active/recommended model options without opening a dropdown. +- “Where everything lives” label is gone/replaced. +- Modules are visible as tiles with minimal scrolling. +- Missing usable models routes users to Settings → Models. +- TypeScript passes. +- Ruff check/format passes. +- Focused backend tests pass. +- No unrelated features are removed. + +--- + +## 4. Constraints and repo rules + +- **Do not duplicate code.** Extract reusable path input and status helpers rather than copying UI blocks. +- **Windows shell:** do not use `&&` commands. +- Preserve existing behavior until replacement is validated. +- Do not delete features or modules. +- Follow `CLAUDE.md` hard rules: + - Never downgrade external models/APIs/libraries. + - Never allow ruff version drift. + - Form controls need real labels and valid ARIA. +- Use Tailwind v4 class forms from `CLAUDE.md`. +- For model/API health, prefer non-spawning probes where possible. +- For config generation, never hallucinate unknown architecture configs. Only copy/generate for recognized built-in model variants. + +--- + +## 5. Context-safe checkpoint strategy + +To avoid context loss or broken intermediate state: + +1. Implement one phase at a time. +2. After each phase: + - run the focused validation for that phase, + - fix failures, + - commit/save if clean. +3. Never start a new phase with failing TypeScript/Python checks from the previous phase. +4. Keep backend API additions backward-compatible. +5. Avoid deleting old UI sections until the replacement is working. +6. If context gets tight, stop after a validated checkpoint and update this plan with the exact next task. + +--- + +## 6. Detailed implementation phases + +### Phase 0 — Guardrails and baseline snapshot + +#### Task 0.1 — Confirm working tree + +- Run: + +```powershell +git status --short --branch +``` + +- Note any pre-existing changes before editing. +- Do not touch unrelated visible/open work unless the task requires it. + +#### Task 0.2 — Establish validation commands + +Run separately, never chained: + +```powershell +uv run ruff check . +``` + +```powershell +uv run ruff format --check . +``` + +```powershell +cd frontend +``` + +```powershell +npx tsc -b +``` + +#### Save checkpoint + +- No commit required if no files changed. +- If unexpected dirty files appear, stop and report. + +--- + +### Phase 1 — Local-only default ON without breaking existing users + +#### Task 1.1 — Backend default change + +File: `backend/modules/storage/store.py` + +Subtasks: + +- Change `_DEFAULT.local_only` from `False` to `True`. +- Preserve explicit existing user choices: + - missing file or missing key → true. + - explicit `local_only: false` → false. +- Update docstring/comments to explain safe default. + +#### Task 1.2 — Frontend copy clarity + +File: `frontend/src/components/layout/SettingsModal.tsx` + +Subtasks: + +- Confirm `fetchCheckpoints()` displays backend value. +- Make local-only row read as a default safety mode. +- Add short copy: “Safe default: theDAW will not download models until you explicitly allow it.” + +#### Task 1.3 — Regression tests + +Likely file: `tests/test_backend_contract.py` or a new focused storage test. + +Subtasks: + +- Missing registry defaults local-only to true. +- Explicit false remains false. +- `SA3_LOCAL_ONLY` env mirrors the setting. + +#### Validate + +```powershell +uv run ruff check . +``` + +Focused storage/local-only test. + +#### Save checkpoint + +Suggested commit: + +```text +settings: default model loading to local-only +``` + +--- + +### Phase 2 — Native folder/file picker API for local paths + +Browser apps cannot normally read absolute folder paths from a standard HTML picker. Since this is a local Windows app with a local backend, use the backend to launch a native Windows picker. + +#### Task 2.1 — Add backend picker endpoints + +File: `backend/modules/storage/router.py` + +Endpoint candidates: + +- `POST /api/storage/pick-folder` +- `POST /api/storage/pick-file` + +Subtasks: + +- Implement Windows folder picker via PowerShell and `System.Windows.Forms.FolderBrowserDialog`. +- Implement file picker for `.json`, `.safetensors`, and future path fields if useful. +- Return `{ path, cancelled }`. +- Non-Windows returns 501 with clear message. +- Add timeout. +- Avoid user-input shell injection. + +#### Task 2.2 — Add frontend storage client wrappers + +File: `frontend/src/lib/storageClient.ts` + +Subtasks: + +- Add `pickFolder()`. +- Add `pickFile()` if useful. +- Keep errors user-readable. + +#### Task 2.3 — Create reusable path input component + +New file: + +`frontend/src/components/ui/PathInput.tsx` + +Props concept: + +```ts +interface PathInputProps { + id: string; + name: string; + label: string; + value: string; + onChange: (value: string) => void; + placeholder?: string; + kind: 'folder' | 'file'; + description?: string; + disabled?: boolean; +} +``` + +Subtasks: + +- Render text input plus compact folder/file button. +- Button calls picker endpoint and fills the input. +- Preserve accessibility labels: + - native input has stable `id` and `name`. + - visible `