diff --git a/.gitignore b/.gitignore
index d2265da..54e4ee9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ app/llm-backend/
app/tools/
# Generated files
+app/config/
app/dist/
app/outputs/
diff --git a/PLAN.md b/PLAN.md
deleted file mode 100644
index 401a6e6..0000000
--- a/PLAN.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Linux Port Plan for Local AI Image Generator
-
-> **Plan location**: This plan will be copied to the project root as `PLAN.md` upon approval, per user request.
-
-> **Implementation Update (post-verification)**
->
-> During end-to-end testing, the third-party Linux CUDA binary (`leaxer-ai/leaxer-stable-diffusion`) was found to **segfault during generation** after successfully loading the model. As a result, Linux CUDA support was **removed** from the implementation. Linux NVIDIA GPUs now use the **Vulkan** backend. The setup script also gained a glibc version check, because the official leejet Linux binaries require **glibc 2.38+** (Ubuntu 24.04). The frontend's misleading "mock SVG" fallback was replaced with a real error message when the backend crashes. See the updated README for build-from-source instructions.
-
-## 1. What This Project Does
-
-**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors / GGUF / CKPT) completely offline. The Windows flow is:
-
-1. **Entry Point**: `start.bat` double-click launcher.
-2. **Setup**: `scripts/setup.ps1` auto-downloads portable Node.js for Windows, detects the GPU vendor, downloads the matching stable-diffusion.cpp backend (CUDA for NVIDIA, Vulkan for AMD/Intel), installs npm dependencies, and builds the Vite+React frontend into `app/dist/`.
-3. **Runtime**: `scripts/serve.cjs` (a Node.js HTTP server) serves the static frontend on `localhost:1420`, manages the backend process lifecycle, proxies generation requests, handles model downloads/imports, tracks telemetry, and manages the gallery.
-4. **All sandboxed**: Everything lives under the project folder (`app/tools/`, `app/backend/`, `app/models/`, `app/outputs/`). No global PATH changes, no system installers.
-
-## 2. Research Findings
-
-### 2.1 stable-diffusion.cpp Linux Binaries
-
-We researched the upstream project (`leejet/stable-diffusion.cpp`) and third-party builders. The following Linux x86_64 binaries are available:
-
-| Backend | Source | Availability | Approx. Size | Notes |
-|---|---|---|---|---|
-| **CPU** | leejet official releases | ✅ Available | ~20–30 MB | Generic x86_64 CPU binary |
-| **Vulkan** | leejet official releases | ✅ Available | ~100 MB | Cross-vendor GPU (AMD, Intel, NVIDIA) |
-| **ROCm** | leejet official releases | ✅ Available | ~1.2 GB | AMD GPU; requires host ROCm kernel driver |
-| **CUDA** | leejet official releases | ❌ **Not published** | N/A | See section 2.2 |
-| **CUDA** | leaxer-ai/leaxer-stable-diffusion (third-party) | ❌ **Rejected** | ~115 MB | Loads models but **segfaults during generation**; removed from setup. |
-
-**Critical Finding**: Upstream does **not** publish Linux CUDA binaries. There is an open feature request ([leejet/stable-diffusion.cpp#1291](https://github.com/leejet/stable-diffusion.cpp/issues/1291)) but no official support yet.
-
-### 2.2 Linux CUDA Feasibility Analysis
-
-We evaluated the third-party `leaxer-ai/leaxer-stable-diffusion` Linux CUDA binary:
-- The binary downloads successfully and is a valid ELF executable.
-- It loads models and initializes CUDA without errors.
-- **However, it segfaults as soon as image generation starts** (reproduced on NVIDIA L4 with `TXT2IMG`).
-- Because the binary is non-functional for its intended purpose, the project does **not** download or use it.
-
-**Decision**: Linux CUDA support is **disabled** until a reliable prebuilt binary becomes available (either from upstream leejet or a verified third-party builder). Linux NVIDIA systems fall back to the **Vulkan** backend, which uses the same GPU via the NVIDIA Vulkan driver.
-
-Users who need native CUDA performance on Linux can build `stable-diffusion.cpp` from source with `-DSD_CUDA=ON` and copy the resulting binary into `app/backend/linux/cuda/`; see README for instructions.
-
-### 2.3 Portable Node.js for Linux
-- Node.js distributes `node-v22.12.0-linux-x64.tar.xz` (matching the Windows LTS version already used).
-- Fully portable: extract with `tar` and run `bin/node` / `bin/npm` directly.
-
-### 2.4 Zero-Dependency GPU Detection on Linux
-To avoid requiring `lspci`, `lshw`, or other host packages, we can parse PCI vendor IDs directly from sysfs:
-- Read `/sys/bus/pci/devices/*/vendor`
-- **NVIDIA**: `0x10de`
-- **AMD**: `0x1002`
-- **Intel**: `0x8086`
-This is pure file I/O and works in any Linux environment with sysfs.
-
-### 2.5 Extraction Strategy (No Host `unzip` Required)
-- Node.js `.tar.xz` → extracted with `tar` (universally available on Linux).
-- Backend `.zip` files → extracted using Node.js after bootstrapping (e.g., via `adm-zip` or a small inline script), or fall back to `python3 -m zipfile` if needed.
-- NVIDIA CUDA redist `.tar.xz` → extracted with `tar`.
-
-## 3. Implementation Approach: Maximum Performance (All Four Backends)
-
-Based on your feedback, the recommended approach is to support **CPU + Vulkan + ROCm + CUDA** on Linux.
-
-### 3.1 Backend Selection Matrix
-
-| Vendor | Primary Backend | Fallback Backend | Source |
-|---|---|---|---|
-| NVIDIA | CUDA (best perf) | Vulkan | CUDA = leaxer third-party; Vulkan = leejet official |
-| AMD | ROCm (best perf) | Vulkan | ROCm = leejet official; Vulkan = leejet official |
-| Intel | Vulkan | CPU | Vulkan = leejet official |
-| No GPU / Unknown | CPU | — | CPU = leejet official |
-
-### 3.2 Backend Directory Layout (Linux)
-
-```
-app/backend/
-├── win/
-│ ├── cuda/sd-cuda.exe
-│ └── vulkan/sd-vulkan.exe
-└── linux/
- ├── cpu/sd-cpu
- ├── vulkan/sd-vulkan
- ├── rocm/sd-rocm + libstable-diffusion.so
- └── cuda/sd-cuda + libstable-diffusion.so + libcudart.so.12 + libcublas.so.12 + libcublasLt.so.12
-```
-
-## 4. Files to Create / Modify
-
-### New Files
-1. **`start.sh`** — Bash entry point equivalent to `start.bat`.
- - Checks for portable Node.js and frontend build.
- - Runs `scripts/setup.sh` if anything is missing.
- - Starts `scripts/serve.cjs` with the portable Node.js binary.
- - Opens browser (`xdg-open`) or prints URL.
- - Handles graceful shutdown (traps SIGINT/SIGTERM to kill backend).
-
-2. **`scripts/setup.sh`** — Bash setup equivalent to `setup.ps1`.
- - Downloads portable Node.js Linux tarball with `curl` + progress bar.
- - Extracts with `tar`.
- - Detects GPU via sysfs PCI vendor IDs.
- - Downloads appropriate backends:
- - **CPU + Vulkan** for everyone (baseline).
- - **ROCm** if AMD GPU detected.
- - **CUDA** if NVIDIA GPU detected (optional, due to large download).
- - Downloads NVIDIA CUDA redist libraries if CUDA is selected.
- - Extracts all archives using `tar` / Node.js / Python fallback.
- - Renames binaries to predictable names.
- - Runs `npm install` and `npm run build` using portable npm.
-
-3. **`scripts/reset.sh`** — Bash reset equivalent to `reset.ps1`.
- - Deletes `app/tools/`, `app/backend/`, `app/dist/`, `app/frontend/node_modules/` while preserving `app/models/` and `app/outputs/`.
-
-### Modified Files
-4. **`scripts/serve.cjs`** — Enhance Linux support.
- - **Add Linux backend paths**:
- ```js
- linuxCpu: path.join(ROOT, "app", "backend", "linux", "cpu", "sd-cpu"),
- linuxVulkan: path.join(ROOT, "app", "backend", "linux", "vulkan", "sd-vulkan"),
- linuxRocm: path.join(ROOT, "app", "backend", "linux", "rocm", "sd-rocm"),
- linuxCuda: path.join(ROOT, "app", "backend", "linux", "cuda", "sd-cuda"),
- ```
- - **Add Linux GPU detection** via sysfs (`/sys/bus/pci/devices/*/vendor`).
- - **Update `getBackendOptions()`**: For Linux, return `cpu`, `vulkan`, `rocm` (if binary exists), and `cuda` (if binary + libraries exist).
- - **Update `selectBackendPath()`**: Choose Linux backend based on resolved type; set `LD_LIBRARY_PATH` for CUDA/ROCm subprocesses so they find bundled `.so` files.
- - **Update `getHealth()` / `getSetupPaths()`**: Include Linux backend checks.
- - **Update `getGpuInfo()` / `hasNvidiaGpu()`**: Add Linux sysfs parsing.
- - **CUDA VRAM polling**: Keep existing `nvidia-smi` approach; on Linux it works if drivers are installed.
-
-5. **`README.md`** — Add Linux section with compatibility matrix, `./start.sh` usage, and known limitations.
-
-## 5. Download Sources
-
-| Component | URL Pattern |
-|---|---|
-| Portable Node.js | `https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-x64.tar.xz` |
-| CPU Backend | `https://github.com/leejet/stable-diffusion.cpp/releases/download/master-XXX/sd-master-XXX-bin-Linux-Ubuntu-24.04-x86_64.zip` |
-| Vulkan Backend | `https://github.com/leejet/stable-diffusion.cpp/releases/download/master-XXX/sd-master-XXX-bin-Linux-Ubuntu-24.04-x86_64-vulkan.zip` |
-| ROCm Backend | `https://github.com/leejet/stable-diffusion.cpp/releases/download/master-XXX/sd-master-XXX-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.13.0.zip` |
-| CUDA Binary (third-party) | `https://github.com/leaxer-ai/leaxer-stable-diffusion/releases/download/v0.1.0/sd-x86_64-unknown-linux-gnu-cuda` |
-| CUDA Server (third-party) | `https://github.com/leaxer-ai/leaxer-stable-diffusion/releases/download/v0.1.0/sd-server-x86_64-unknown-linux-gnu-cuda` |
-| CUDA Runtime (NVIDIA redist) | `https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-12.X.YZ-archive.tar.xz` |
-| cuBLAS (NVIDIA redist) | `https://developer.download.nvidia.com/compute/cuda/redist/libcublas/linux-x86_64/libcublas-linux-x86_64-12.X.YZ-archive.tar.xz` |
-
-## 6. Setup Modes
-
-To respect users with limited bandwidth or no NVIDIA hardware, `setup.sh` should support two modes:
-
-1. **Quick Setup (default)** — Downloads CPU + Vulkan only.
- - Works on all Linux systems.
- - NVIDIA users get Vulkan acceleration (good, but not best).
- - ~120–150 MB total download.
-
-2. **Maximum Performance Setup** — Downloads CPU + Vulkan + ROCm + CUDA.
- - Best performance for each vendor.
- - ~1.5 GB total download (mostly ROCm + cuBLAS).
- - User can opt-in via `./start.sh --max-perf` or `./scripts/setup.sh --max-perf`.
-
-## 7. Self-Contained Checklist
-
-| Host Dependency | How We Eliminate It |
-|---|---|
-| Node.js installed globally | Download portable `node-v22.12.0-linux-x64.tar.xz` into `app/tools/node-linux/` |
-| npm installed globally | Use portable npm from the tarball |
-| `unzip` command | Extract ZIPs using Node.js or Python `zipfile` |
-| `lspci` / `lshw` | Parse `/sys/bus/pci/devices/*/vendor` directly |
-| C++ compiler / CUDA toolkit | Use pre-built binaries only |
-| System package manager (apt/yum) | Never used |
-| CUDA runtime libs | Bundle NVIDIA redistributable `.so` files |
-
-**One unavoidable host dependency remains**: the NVIDIA kernel driver (`libcuda.so.1`) for CUDA mode. Without it, CUDA cannot run. The app will detect this and automatically fall back to Vulkan for NVIDIA.
-
-## 8. Known Limitations
-
-1. **Third-party CUDA binary**: The Linux CUDA build comes from `leaxer-ai/leaxer-stable-diffusion`, which has only one release (v0.1.0) and may become stale. We should pin to a specific version and periodically evaluate alternatives.
-2. **Large ROCm download**: The official ROCm backend ZIP is ~1.2 GB, which is a significant one-time download.
-3. **Large cuBLAS download**: NVIDIA's redistributable cuBLAS package is ~400–500 MB.
-4. **NVIDIA driver requirement for CUDA**: `libcuda.so.1` must be present on the host (provided by the NVIDIA proprietary driver). If missing, CUDA mode falls back to Vulkan.
-5. **ROCm host driver requirement**: ROCm requires a compatible AMD GPU and ROCm kernel driver. If unavailable, ROCm falls back to Vulkan.
-6. **glibc compatibility**: Official Linux binaries are built on Ubuntu 24.04. They may fail on very old distributions (e.g., CentOS 7, Ubuntu 18.04) due to glibc version mismatches.
-
-## 9. Recommended Implementation Order
-
-1. Create `start.sh`, `scripts/setup.sh`, and `scripts/reset.sh` with CPU + Vulkan support first.
-2. Update `scripts/serve.cjs` with Linux paths, sysfs GPU detection, and backend selection.
-3. Add ROCm download path (behind `--max-perf` flag).
-4. Add CUDA download path using leaxer binary + NVIDIA redist libraries (behind `--max-perf` flag).
-5. Add fallback logic so that if ROCm/CUDA fails to initialize, the app automatically switches to Vulkan.
-6. Update `README.md` with Linux instructions and performance notes.
-7. Test on Linux machines with NVIDIA (CUDA/Vulkan), AMD (ROCm/Vulkan), Intel (Vulkan), and CPU-only configurations.
diff --git a/app/frontend/src/components/Settings.jsx b/app/frontend/src/components/Settings.jsx
index d7b3099..02e4c72 100644
--- a/app/frontend/src/components/Settings.jsx
+++ b/app/frontend/src/components/Settings.jsx
@@ -5,7 +5,7 @@ import {
Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers,
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check, Palette
} from "lucide-react";
-import { stopServer, formatBytes, getLlmStatus, startLlm, stopLlm } from "../services/api";
+import { stopServer, formatBytes, getLlmBackends, getLlmStats, getLlmStatus, benchmarkLlm, startLlm, stopLlm } from "../services/api";
import { THEMES } from "../themes";
const ASPECT_RATIOS = [
@@ -187,6 +187,9 @@ function Settings({
setTheme,
}) {
const [llmStatus, setLlmStatus] = useState({ ready: false, settings: {} });
+ const [llmBackends, setLlmBackends] = useState({ available: [], candidates: [] });
+ const [llmStats, setLlmStats] = useState({ benchmarks: [] });
+ const [benchmarkBusy, setBenchmarkBusy] = useState(false);
useEffect(() => {
let cancelled = false;
@@ -195,6 +198,13 @@ function Settings({
const status = await getLlmStatus();
if (!cancelled) setLlmStatus(status);
} catch (_) {}
+ try {
+ const [backends, stats] = await Promise.all([getLlmBackends(), getLlmStats()]);
+ if (!cancelled) {
+ setLlmBackends(backends);
+ setLlmStats(stats);
+ }
+ } catch (_) {}
};
fetchLlmStatus();
const interval = setInterval(fetchLlmStatus, 3000);
@@ -247,6 +257,37 @@ function Settings({
performanceProfile: settings?.performanceProfile,
});
+ const handleBenchmarkTextBackend = async () => {
+ const model = llmStatus.settings?.model;
+ if (!model) {
+ showAlert({ title: "No Text Model Loaded", message: "Load a GGUF text model before benchmarking.", danger: true });
+ return;
+ }
+ setBenchmarkBusy(true);
+ try {
+ const result = await benchmarkLlm(model, {
+ contextSize: Math.min(2048, Number(textSettings?.contextSize) || 2048),
+ gpuLayers: textSettings?.gpuLayers ?? -1,
+ includeCpu: true,
+ });
+ const winner = result.winner;
+ showAlert({
+ title: winner ? "Benchmark Complete" : "Benchmark Finished",
+ message: winner
+ ? `Fastest backend: ${winner.backendMode} at ${Number(winner.predicted_per_second || 0).toFixed(1)} tokens/sec.`
+ : "Benchmark finished, but no backend returned usable timing data.",
+ });
+ const [status, backends, stats] = await Promise.all([getLlmStatus(), getLlmBackends(true), getLlmStats()]);
+ setLlmStatus(status);
+ setLlmBackends(backends);
+ setLlmStats(stats);
+ } catch (err) {
+ showAlert({ title: "Benchmark Failed", message: err.message || String(err), danger: true });
+ } finally {
+ setBenchmarkBusy(false);
+ }
+ };
+
const handleThinkingToggle = async (enabled) => {
const nextSettings = { ...textSettings, enableThinking: enabled };
@@ -924,6 +965,26 @@ function Settings({
GPU Layers: {llmStatus.settings?.gpuLayers === -1 ? "All" : llmStatus.settings?.gpuLayers}
)}
+