Skip to content

Commit fc14f38

Browse files
feat: OneTrainerWeb — Electron + React frontend with FastAPI bridge
Replaces the customtkinter desktop GUI with an Electron + React web frontend, connected to the existing Python training backend via a FastAPI bridge. Existing backend code (modules/, scripts/, training_presets/) is unchanged — the bridge communicates only through the TrainCallbacks / TrainCommands interface. Adds: - web/gui/ Electron + React + Vite + Tailwind (TypeScript strict) - web/backend/ FastAPI bridge importing existing modules/ directly - web/scripts/ AST-based generators that emit TypeScript types and UI schema from the Python config/enum/CTk sources - start-web-ui.{bat,sh}, install/update script integration - .github/workflows/web-ui-lint.yml (ESLint + Prettier check on PRs) - .pre-commit-config.yaml hooks: ESLint shim + hardcoded-options guard Also folds in a follow-up pass porting general code-quality improvements (bugfixes, refactors, small UX additions) from a parallel fork's more advanced branch, while deliberately excluding that branch's new features (job queue, DPO training, cloud/RunPod, validation checker, OFT merging, distillation, bucket analysis) and the field-override binding system those features depend on: - Drop the allowed_roots allow-list from validate_path(); presets.py sandboxes its own paths via base_match() instead. - main.py reuses paths.py's PROJECT_ROOT instead of duplicating the project-root/sys.path bootstrap. - Add real tensorboard process management (/tensorboard/launch, /stop). - Add /system/cache/status, /system/cache/clear, and Scalene-backed profiling endpoints (dump-stacks, toggle). - Simplify custom-sample field copying to sample_config.from_dict(...). - Let /concepts GET/PUT target a path override, and add POST /concepts/save-caption for the mask editor's caption panel. - Let /tools/convert accept per-conversion quantization overrides. - start_training() accepts a pre-built config; fix the always-on tensorboard subprocess handle not being a class attribute (a fresh TrainerService instance could otherwise lose track of an already-running subprocess). - _safe_literal_eval() in generate_types.py fixes optimizer defaults using float('inf') silently extracting as {} (ast.literal_eval rejects Call nodes); the OptimizerKeyDetail type union is now derived from actual data. - _sanitize_default() in generate_ui_schema.py encodes Infinity/-Infinity/NaN as strings so the browser's JSON.parse doesn't choke on them. - FormEntry no longer clobbers what the user is typing when the parsed value echoes back through config (e.g. "-0" -> "0" mid-keystroke). - ModalBase gains size="full" and closeOnEscape. - TopBar's preset dropdown is now a custom menu supporting inline deletion of user presets. - SampleParamsModal gains prompt/negative-prompt fields and a seed reroll. - MaskEditorPage gains a caption editor panel with arrow-key navigation. - BottomBar gains a tensorboard launch button. - Fixed default-value/config bugs: ConceptsPage's resolution_override ("" -> "512"), and SamplingPage's hardcoded image-format dropdown now sources from the generated ImageFormatValues enum. - Vite's dev server already falls back off a busy port 5173, but Electron's main process had no way to discover which port it landed on; added a Vite plugin that writes the resolved port to .vite-port and an Electron-side poller that reads it, replacing the hardcoded dev-server URL and a wait-on-a-fixed-port step that would have hung forever if 5173 was taken. Verified with a standalone script (web/gui/scripts/verify-port-fallback.mjs, run via `npm run verify:port-fallback`). Claude-Session: https://claude.ai/code/session_019pHqnjipbR4G8eGRQcemTq
1 parent 07254ad commit fc14f38

179 files changed

Lines changed: 36380 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/web-ui-lint.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: web-ui-lint
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "web/gui/**"
7+
- ".github/workflows/web-ui-lint.yml"
8+
push:
9+
branches: [master, Webui]
10+
paths:
11+
- "web/gui/**"
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
defaults:
17+
run:
18+
working-directory: web/gui
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: "20"
24+
- run: npm install --no-audit --no-fund
25+
- run: npm run lint
26+
- run: npm run format:check

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ debug*.py
2020
config.json
2121
secrets.json
2222
*.zip
23+
.env*
24+
!.env.example
2325

2426
# environments
2527
/.venv*
@@ -38,3 +40,14 @@ pixi.toml
3840
train.bat
3941
debug_report.log
4042
config_diff.txt
43+
44+
# Web UI
45+
web/gui/dist/
46+
web/gui/release/
47+
web/gui/node_modules/
48+
web/gui/public/ui-schema.json
49+
web/gui/src/renderer/types/generated/
50+
web/gui/.vite-port
51+
web/backend/__pycache__/
52+
web/backend/**/__pycache__/
53+
web/backend/generated/

.pre-commit-config.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,25 @@ repos:
1616
- repo: https://github.com/astral-sh/ruff-pre-commit
1717
rev: v0.15.17
1818
hooks:
19-
# Run the Ruff linter, but not the formatter.
2019
- id: ruff
2120
args: ["--fix"]
2221
types_or: [ python, pyi, jupyter ]
2322

23+
- repo: local
24+
hooks:
25+
- id: eslint
26+
name: ESLint (frontend)
27+
entry: python -m web.scripts.run_eslint
28+
language: system
29+
files: '^web/gui/.*\.(ts|tsx)$'
30+
pass_filenames: false
31+
32+
- id: check-hardcoded-dropdown-options
33+
name: No hardcoded <Select options> outside types/generated/
34+
entry: python -m web.scripts.check_hardcoded_options
35+
language: system
36+
files: '^web/gui/src/renderer/.*\.(ts|tsx)$'
37+
pass_filenames: false
38+
2439
ci:
2540
autofix_prs: false

CLAUDE.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Failure IS an option.
4+
5+
IMPORTANT: ALWAYS RUN RUFF AND ESLINT ON STRICT MODE BEFORE FINALISING ANY TASK. NO FILES ARE EXEMPT FROM THIS RULE.
6+
7+
## Project Overview
8+
9+
OneTrainer is a mature diffusion model training framework supporting 21+ model architectures, 4 training methods (Fine-Tune, LoRA, Embedding, Fine-Tune VAE), and 40+ optimizers. The project is migrating from a customtkinter desktop GUI to an Electron + React web-based frontend (**OneTrainerWeb**), with a FastAPI bridge layer connecting to the existing Python backend.
10+
11+
## Critical Constraints
12+
13+
- **DO NOT modify existing backend code** in `modules/`, `scripts/`, or `training_presets/` unless absolutely necessary. The existing backend must remain 100% backwards compatible.
14+
- All new code lives under `web/gui/` (frontend) and `web/backend/` (FastAPI bridge).
15+
- The existing `TrainCallbacks`/`TrainCommands` interface is the only integration point between UI and training backend.
16+
17+
## Tech Stack
18+
19+
- **Frontend:** React 19.2.4, Vite 7.3.1, Tailwind CSS 4.2.0, Electron 40.6.0, TypeScript (strict mode)
20+
- **Backend bridge:** Python 3.12, FastAPI, Uvicorn
21+
- **Existing backend:** Python 3.10-3.12, PyTorch 2.8, built on Hugging Face diffusers
22+
23+
## Build & Development Commands
24+
25+
### Python (existing backend)
26+
```bash
27+
# Install (creates venv automatically)
28+
./install.bat # Windows
29+
./install.sh # Linux/Mac
30+
31+
# Run existing GUI
32+
./start-ui.bat # Windows
33+
./start-ui.sh # Linux/Mac
34+
35+
# CLI headless training
36+
python scripts/train.py --config-path <path-to-config.json>
37+
38+
# Install dev dependencies (run OUTSIDE venv)
39+
pip install -r requirements-dev.txt
40+
pre-commit install
41+
```
42+
43+
### Linting (Python)
44+
```bash
45+
# Ruff linter with auto-fix (configured in pyproject.toml)
46+
ruff check --fix .
47+
48+
# Pre-commit runs Ruff automatically on commit
49+
pre-commit run --all-files
50+
```
51+
52+
Ruff config: line-length 120, see `pyproject.toml` for full rule set. Import ordering uses custom sections (first-party `modules`, then `mgds`, `torch`, `hf`).
53+
54+
### Frontend (web/gui/)
55+
```bash
56+
cd web/gui
57+
npm install
58+
npm run dev # Vite dev server
59+
npm run build # Production build
60+
npm run typecheck # TypeScript strict checking
61+
npm run lint # ESLint
62+
npm test # Vitest unit tests
63+
npm run test:e2e # Playwright E2E tests
64+
```
65+
66+
### Backend bridge (web/backend/)
67+
```bash
68+
cd web/backend
69+
pip install fastapi uvicorn
70+
uvicorn main:app --reload --port 8000
71+
pytest # Backend tests
72+
```
73+
74+
## Architecture
75+
76+
### Existing Backend (modules/)
77+
78+
The codebase follows a modular architecture where each module type is interchangeable:
79+
80+
- **`modules/trainer/`** — Training loop (`GenericTrainer`, `MultiTrainer`, `CloudTrainer`). Communicates with UI exclusively through `TrainCallbacks`/`TrainCommands`.
81+
- **`modules/util/config/`** — JSON-native config system. `TrainConfig` has 200+ parameters with a 10-generation version migration system. Config classes extend `BaseConfig` for serialization.
82+
- **`modules/util/enum/`** — 27 enum definitions (ModelType, TrainingMethod, Optimizer, DataType, etc.) that must be mirrored as TypeScript types.
83+
- **`modules/util/create.py`** — Central factory (86KB) that instantiates all modules based on config.
84+
- **`modules/util/callbacks/TrainCallbacks.py`** — Trainer-to-UI interface (progress, status, samples).
85+
- **`modules/util/commands/TrainCommands.py`** — UI-to-Trainer interface (stop, sample, backup, save).
86+
- **`modules/ui/`** — Legacy customtkinter GUI (27 files). Reference for feature parity but not modified.
87+
- **`modules/model/`, `modelLoader/`, `modelSampler/`, `modelSaver/`, `modelSetup/`** — Per-architecture implementations.
88+
- **`modules/dataLoader/`** — MGDS-based data loading per architecture.
89+
90+
### New Frontend Architecture (web/)
91+
92+
```
93+
web/
94+
├── gui/ # Electron + React + Vite + Tailwind
95+
│ └── src/
96+
│ ├── main/ # Electron main process (spawn FastAPI, window mgmt)
97+
│ ├── renderer/ # React SPA
98+
│ │ ├── components/ # Shared UI components
99+
│ │ ├── pages/ # Tab pages (General, Model, Training, etc.)
100+
│ │ ├── hooks/ # Custom React hooks
101+
│ │ ├── store/ # State management
102+
│ │ ├── api/ # REST/WebSocket client layer
103+
│ │ └── types/ # TypeScript types (generated from Python enums)
104+
│ └── shared/ # Types shared between main/renderer
105+
├── backend/ # FastAPI bridge server
106+
│ ├── routers/ # REST endpoints (config, training, sampling, tools, system, presets)
107+
│ ├── services/ # Business logic wrappers
108+
│ ├── ws/ # WebSocket handlers (training progress, tensorboard, system metrics)
109+
│ └── models/ # Pydantic request/response models
110+
└── docs/
111+
└── breakdown.md # Comprehensive change documentation (required)
112+
```
113+
114+
### Process Lifecycle
115+
1. Electron spawns FastAPI/Uvicorn as child process
116+
2. Waits for health check endpoint
117+
3. Loads React renderer pointing to FastAPI backend
118+
4. React communicates via REST (config CRUD) and WebSocket (real-time progress/metrics)
119+
5. FastAPI imports existing `modules/` directly — no backend code changes
120+
121+
### Key Communication Pattern
122+
```
123+
React UI → REST/WebSocket → FastAPI Bridge → TrainCallbacks/TrainCommands → GenericTrainer
124+
```
125+
126+
The FastAPI bridge imports `modules.util.config.TrainConfig`, `modules.util.create`, `modules.util.callbacks.TrainCallbacks`, and `modules.util.commands.TrainCommands` directly.
127+
128+
## Dynamic UI Behavior
129+
130+
The Model and Training tabs completely rebuild based on `model_type` (21 types) and `training_method` (4 methods). The LoRA and Embedding tabs appear/disappear based on training method. The React frontend should use schema-driven rendering (declarative config per model type) rather than large if/elif chains.
131+
132+
## Configuration System
133+
134+
- All configs serialize to/from JSON via `BaseConfig.to_dict()`/`from_dict()`
135+
- `TrainConfig` version is currently 10, with migrations from version 0-9
136+
- Config presets live in `training_presets/*.json` (built-in prefixed with `#`)
137+
- Concepts in `training_concepts/concepts.json`, samples in `training_samples/samples.json`
138+
- Secrets in `secrets.json` (never commit)
139+
- TypeScript types must be generated from Python config/enum classes at build time
140+
141+
## Testing
142+
143+
The existing project has zero automated tests. The web frontend must build a comprehensive test suite:
144+
- **Config round-trip tests** (highest priority): load preset → serialize → deserialize → diff must be zero
145+
- **Parameter parity tests**: automated checks that React forms match `TrainConfig` fields
146+
- **Dynamic UI tests**: verify correct parameter sets per model type
147+
- Frontend: Vitest + React Testing Library (unit), Playwright (E2E)
148+
- Backend: pytest + httpx (API), websockets (WebSocket)
149+
150+
## Code Style
151+
152+
- Python: 4-space indent, 120 char line length, double quotes, Ruff linting
153+
- TypeScript: strict mode, all linting rules enabled
154+
- File encoding: UTF-8, LF line endings (CRLF for .bat files only)
155+
156+
## Design Reference
157+
158+
See `OneTrainerWeb.md` for complete brand spec (color palette, typography, elevation, motion, accessibility requirements). Key tokens: `--cobalt-600: #2563EB`, `--azure-500: #38BDF8`, dark surface `--surface-dark: #0B1120`, light surface `--surface-light: #F0F5FF`.
159+
160+
## Reference Documents
161+
162+
- `FeasibilityStudy.md` — Full technical analysis, UI migration map, risk assessment, implementation roadmap
163+
- `OneTrainerWeb.md` — Project requirements, design system, brand guidelines
164+
- `docs/ProjectStructure.md` — Module architecture explanation
165+
- `docs/Overview.md` — General OneTrainer overview

install.bat

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,38 @@ if errorlevel 1 (
273273
echo %GRN%CUDA is available.%RESET%
274274
)
275275

276+
rem 7) Generate UI schema and build web UI
277+
echo.
278+
echo %CYAN%Generating UI schema...%RESET%
279+
python -m web.scripts.generate_ui_schema
280+
if errorlevel 1 (
281+
echo %YEL%WARNING: UI schema generation failed. Using existing schema.%RESET%
282+
)
283+
284+
echo.
285+
where node >NUL 2>NUL
286+
if errorlevel 1 (
287+
echo %YEL%Node.js not found. Skipping web UI build.%RESET%
288+
echo To use the web UI, install Node.js from https://nodejs.org/ and re-run install.bat
289+
) else (
290+
echo %CYAN%Building web UI...%RESET%
291+
pushd web\gui
292+
call npm install
293+
if errorlevel 1 (
294+
echo %YEL%WARNING: npm install failed. Web UI will not be available.%RESET%
295+
popd
296+
goto :skip_web_build
297+
)
298+
call npm run build:electron
299+
if errorlevel 1 (
300+
echo %YEL%WARNING: Web UI build failed. Web UI will not be available.%RESET%
301+
) else (
302+
echo %GRN%Web UI built successfully.%RESET%
303+
)
304+
popd
305+
)
306+
:skip_web_build
307+
276308
echo.
277309
echo %GRN%**** Install successful^^! ****%RESET%
278310
echo.

lib.include.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,39 @@ function prepare_runtime_environment {
444444
else
445445
install_requirements_in_active_env_if_necessary
446446
fi
447+
448+
# Regenerate the web UI schema and rebuild the Electron bundle when possible.
449+
print "Generating UI schema..."
450+
if ! run_python_in_active_env -m web.scripts.generate_ui_schema; then
451+
print_warning "UI schema generation failed. Using existing schema."
452+
fi
453+
454+
build_web_ui_if_available
455+
}
456+
457+
# Builds the Electron+React web UI when Node.js is available.
458+
# During "install" runs it builds unconditionally; otherwise only rebuilds
459+
# when a previously built bundle is detected, so regular headless users
460+
# are not forced to install Node.js.
461+
function build_web_ui_if_available {
462+
if ! command -v node &> /dev/null; then
463+
print "Node.js not found. Skipping web UI build."
464+
print "To use the web UI, install Node.js from https://nodejs.org/"
465+
return 0
466+
fi
467+
468+
local gui_dir="${SCRIPT_DIR}/web/gui"
469+
local dist_marker="${gui_dir}/dist/main/main/index.cjs"
470+
471+
if [[ -f "${dist_marker}" ]] || [[ "${1:-}" == "install" ]]; then
472+
print "Building web UI..."
473+
(
474+
cd "${gui_dir}"
475+
npm install || { print_warning "npm install failed. Web UI may be stale."; return 0; }
476+
npm run build:electron || { print_warning "Web UI build failed. Web UI may be stale."; return 0; }
477+
print "Web UI built successfully."
478+
)
479+
else
480+
print "Web UI not previously built. Run start-web-ui.sh to build and launch."
481+
fi
447482
}

0 commit comments

Comments
 (0)