From bf206efb7764b19ef4ea36452aee0472fb89408f Mon Sep 17 00:00:00 2001 From: Tech Jarves Date: Thu, 11 Jun 2026 10:02:47 +0000 Subject: [PATCH 1/8] fix chnages i --- scripts/serve.cjs | 61 +++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/scripts/serve.cjs b/scripts/serve.cjs index c63775e..7de2651 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -34,6 +34,7 @@ const BACKEND_PATHS = { linuxCuda: path.join(ROOT, "app", "backend", "linux", "cuda", "sd-cuda"), }; let BACKEND_PATH = ""; +const backendSupportsFlags = {}; if (osPlatform === "win32") { let hasNvidia = false; try { @@ -640,17 +641,25 @@ function backendAccepts(binaryPath, backendName) { "--model", path.join(MODELS, "__backend_probe_missing__.safetensors"), "--listen-port", "18082", ]; - let result = spawnSync(binaryPath, probeArgs, { encoding: "utf8", timeout: 5000 }); + const spawnEnv = { ...process.env }; + if (osPlatform === "linux") { + const extraLibs = [path.dirname(binaryPath)]; + const existing = spawnEnv.LD_LIBRARY_PATH || ""; + spawnEnv.LD_LIBRARY_PATH = extraLibs.join(":") + (existing ? ":" + existing : ""); + } + let result = spawnSync(binaryPath, probeArgs, { env: spawnEnv, encoding: "utf8", timeout: 5000 }); let output = `${result.stdout || ""}\n${result.stderr || ""}`; + let supportsFlags = true; // Some binaries do not support --backend. If we see "unknown argument", // retry without backend flags so we can still verify the binary launches. if (output.includes("unknown argument") && output.includes("--backend")) { + supportsFlags = false; const fallbackArgs = [ "--model", path.join(MODELS, "__backend_probe_missing__.safetensors"), "--listen-port", "18082", ]; - result = spawnSync(binaryPath, fallbackArgs, { encoding: "utf8", timeout: 5000 }); + result = spawnSync(binaryPath, fallbackArgs, { env: spawnEnv, encoding: "utf8", timeout: 5000 }); output = `${result.stdout || ""}\n${result.stderr || ""}`; } @@ -663,7 +672,11 @@ function backendAccepts(binaryPath, backendName) { return false; } // A healthy binary prints project-specific log lines when it tries to load the model. - return lower.includes("stable-diffusion.cpp") || lower.includes("loading model"); + const isOk = lower.includes("stable-diffusion.cpp") || lower.includes("loading model"); + if (isOk) { + backendSupportsFlags[binaryPath] = supportsFlags; + } + return isOk; } catch (_) { return false; } @@ -852,36 +865,28 @@ async function startBackend(settings = {}) { "--threads", String(runThreads), ]; + const supportsFlags = backendSupportsFlags[backendPath] !== false; const requestedBackend = resolveBackendType(currentSettings.useGpu, currentSettings.backendType); if (requestedBackend === "cpu") { - args.push( - "--backend", "cpu", - "--params-backend", "cpu", - "--rng", "cpu", - "--sampler-rng", "cpu", - ); + if (supportsFlags) { + args.push("--backend", "cpu", "--params-backend", "cpu"); + } + args.push("--rng", "cpu", "--sampler-rng", "cpu"); } else if (requestedBackend === "vulkan") { - args.push( - "--backend", "vulkan0", - "--params-backend", "vulkan0", - "--rng", "cpu", - "--sampler-rng", "cpu", - ); + if (supportsFlags) { + args.push("--backend", "vulkan0", "--params-backend", "vulkan0"); + } + args.push("--rng", "cpu", "--sampler-rng", "cpu"); } else if (requestedBackend === "cuda") { - // CUDA is only supported on Windows. Linux has no reliable prebuilt CUDA binary. - args.push( - "--backend", "cuda0", - "--params-backend", "cuda0", - "--rng", "cuda", - "--sampler-rng", "cuda", - ); + if (supportsFlags) { + args.push("--backend", "cuda0", "--params-backend", "cuda0"); + } + args.push("--rng", "cuda", "--sampler-rng", "cuda"); } else if (requestedBackend === "rocm") { - args.push( - "--backend", "rocm0", - "--params-backend", "rocm0", - "--rng", "cpu", - "--sampler-rng", "cpu", - ); + if (supportsFlags) { + args.push("--backend", "rocm0", "--params-backend", "rocm0"); + } + args.push("--rng", "cpu", "--sampler-rng", "cpu"); } if (currentSettings.vaeTiling) { From 84034531c40664291646d59fcabc998ccb7c5545 Mon Sep 17 00:00:00 2001 From: Tech Jarves Date: Thu, 11 Jun 2026 10:14:14 +0000 Subject: [PATCH 2/8] Refactor entry points: split launcher into linux.sh and mac.sh, rename start.bat to windows.bat, isolate OS node_modules --- .gitignore | 2 + README.md | 23 +++--- start.sh => linux.sh | 61 ++++++++-------- mac.sh | 149 +++++++++++++++++++++++++++++++++++++++ scripts/reset.ps1 | 13 ++++ scripts/reset.sh | 12 +++- scripts/setup.ps1 | 12 +++- scripts/setup.sh | 25 ++++++- start.bat => windows.bat | 0 9 files changed, 255 insertions(+), 42 deletions(-) rename start.sh => linux.sh (70%) create mode 100755 mac.sh rename start.bat => windows.bat (100%) diff --git a/.gitignore b/.gitignore index 35a19d6..541bb3b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ app/outputs/ # Ignore portable Node.js binaries and packages app/tools/ **/node_modules/ +**/node_modules_mac/ +**/node_modules_linux/ node_modules/ # Ignore backend folder completely (contains massive DLLs, EXEs, and ZIPs >1GB) diff --git a/README.md b/README.md index 7eecc48..4b69955 100644 --- a/README.md +++ b/README.md @@ -22,27 +22,27 @@ --- ## 📖 Overview -**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows, Linux, and macOS. Double-clicking `start.bat` (Windows) or running `./start.sh` (Linux/macOS) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace. +**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows, Linux, and macOS. Running `windows.bat` (Windows), `./linux.sh` (Linux), or `./mac.sh` (macOS) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace. --- ## ⚡ Quick Start ### Windows -1. **Launch:** Double-click **`start.bat`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run). +1. **Launch:** Double-click **`windows.bat`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run). 2. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). 3. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. ### Linux 1. **Check compatibility:** Prebuilt Linux backends are built on Ubuntu 24.04 and require **glibc 2.38+**. Run `ldd --version` to verify. -2. **Launch:** Open a terminal in the project folder and run **`./start.sh`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run). - - For maximum AMD GPU performance, use **`./start.sh --max-perf`** on first setup (adds the ROCm backend). +2. **Launch:** Open a terminal in the project folder and run **`./linux.sh`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run). + - For maximum AMD GPU performance, use **`./linux.sh --max-perf`** on first setup (adds the ROCm backend). 3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). 4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. ### macOS 1. **Check compatibility:** The prebuilt macOS backend is for **Apple Silicon (M1 or newer)** and uses **Metal** GPU acceleration. -2. **Launch:** Open Terminal in the project folder and run **`./start.sh`** (downloads portable Node.js and the pre-compiled Metal backend on first run). +2. **Launch:** Open Terminal in the project folder and run **`./mac.sh`** (downloads portable Node.js and the pre-compiled Metal backend on first run). 3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). 4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. @@ -61,8 +61,9 @@ ## 📁 Repository Structure ``` local-ai-image-generator/ -├── start.bat # Main double-click entrypoint (Windows) -├── start.sh # Main terminal entrypoint (Linux/macOS) +├── windows.bat # Main double-click entrypoint (Windows) +├── linux.sh # Main terminal entrypoint (Linux) +├── mac.sh # Main terminal entrypoint (macOS) ├── PLAN.md # Linux port implementation plan ├── LICENSE # MIT Open Source license ├── .gitignore @@ -114,8 +115,8 @@ local-ai-image-generator/ - **Apple Silicon (M1 or newer)** is required for the prebuilt macOS Metal backend. **Linux setup modes:** -- **Default (`./start.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB). -- **Maximum Performance (`./start.sh --max-perf`)**: Also downloads the ROCm backend. Total download ~1.3 GB. +- **Default (`./linux.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB). +- **Maximum Performance (`./linux.sh --max-perf`)**: Also downloads the ROCm backend. Total download ~1.3 GB. --- @@ -137,7 +138,7 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution * **Linux backends fail to start with `GLIBC_2.38' not found`:** The prebuilt binaries require glibc 2.38+ (Ubuntu 24.04). Upgrade your distribution or build stable-diffusion.cpp from source (see below). * **Linux ROCm not loading:** Make sure your AMD GPU and kernel are compatible with ROCm 7.13. The app will automatically fall back to Vulkan if ROCm cannot initialize. * **Windows exits with code `3221225781`:** This is `0xC0000135`, which means Windows could not load a required backend DLL. For AMD/Intel Vulkan, update the GPU driver with Vulkan support, then rerun setup so `app/backend/win/vulkan/` is repaired. For NVIDIA CUDA, update the NVIDIA driver and rerun setup so CUDA runtime DLLs are restored. -* **Generation shows "server is not responding or crashed":** The backend process exited. Check the terminal where you ran `./start.sh` for the exact error (common causes are glibc mismatch, missing Vulkan drivers, or out-of-memory). +* **Generation shows \"server is not responding or crashed\":** The backend process exited. Check the terminal where you ran `./linux.sh` or `./mac.sh` for the exact error (common causes are glibc mismatch, missing Vulkan drivers, or out-of-memory). --- @@ -190,7 +191,7 @@ After copying, rename the server binary to match what `scripts/serve.cjs` expect - Vulkan: `sd` → `sd-vulkan` - ROCm: `sd` → `sd-rocm` -Then restart the app with `./start.sh`. +Then restart the app with `./linux.sh` (Linux) or `./mac.sh` (macOS). --- diff --git a/start.sh b/linux.sh similarity index 70% rename from start.sh rename to linux.sh index f6eeec8..5eccac2 100755 --- a/start.sh +++ b/linux.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# Local AI Image Generator - Linux/macOS Launcher -# Double-click or run: ./start.sh +# Local AI Image Generator - Linux Launcher +# Double-click or run: ./linux.sh # Use --max-perf to enable ROCm backend downloads on Linux first setup. # @@ -10,17 +10,18 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP_DIR="$SCRIPT_DIR/app" PLATFORM="$(uname -s)" -if [[ "$PLATFORM" == "Darwin" ]]; then - NODE_DIR="$APP_DIR/tools/node-mac" - BACKEND_PATH="$APP_DIR/backend/mac/sd" - PLATFORM_LABEL="macOS" -else - NODE_DIR="$APP_DIR/tools/node-linux" - BACKEND_PATH="$APP_DIR/backend/linux/vulkan/sd-vulkan" - CPU_BACKEND_PATH="$APP_DIR/backend/linux/cpu/sd-cpu" - PLATFORM_LABEL="Linux" + +if [[ "$PLATFORM" != "Linux" ]]; then + echo "[ERROR] This script is for Linux only. Please run ./mac.sh on macOS." >&2 + exit 1 fi + +NODE_DIR="$APP_DIR/tools/node-linux" NODE_BIN="$NODE_DIR/bin/node" +BACKEND_PATH="$APP_DIR/backend/linux/vulkan/sd-vulkan" +CPU_BACKEND_PATH="$APP_DIR/backend/linux/cpu/sd-cpu" +PLATFORM_LABEL="Linux" + DIST_INDEX="$APP_DIR/dist/index.html" SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh" SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs" @@ -35,30 +36,37 @@ if [[ "${1:-}" == "--max-perf" ]]; then MAX_PERF_FLAG="--max-perf" fi +# ── Symlink node_modules to avoid OS conflicts ────────────────────────────── +FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules" +LINUX_NODE_MODULES="$APP_DIR/frontend/node_modules_linux" + +if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then + echo " >> Migrating existing node_modules to node_modules_linux..." + mv "$FRONTEND_NODE_MODULES" "$LINUX_NODE_MODULES" +fi + +rm -f "$FRONTEND_NODE_MODULES" +mkdir -p "$LINUX_NODE_MODULES" +ln -sf "node_modules_linux" "$FRONTEND_NODE_MODULES" + # ── First-time setup check ───────────────────────────────────────────────── if [[ ! -d "$NODE_DIR" ]]; then SETUP_MODE="First-Time Setup" fi if [[ ! -x "$NODE_BIN" ]]; then - SETUP_REASON="Portable Node.js is missing." + SETUP_REASON="Portable Node.js for Linux is missing." fi if [[ ! -f "$DIST_INDEX" ]]; then SETUP_REASON="Frontend build is missing." fi -if [[ "$PLATFORM" == "Darwin" ]]; then - if [[ ! -x "$BACKEND_PATH" ]]; then - SETUP_REASON="No macOS Metal backend binary is installed." - fi -else - # At minimum we need CPU or Vulkan backend on Linux, and both CLI and server binaries must be executable - CPU_SERVER_PATH="$APP_DIR/backend/linux/cpu/sd-server-cpu" - VULKAN_SERVER_PATH="$APP_DIR/backend/linux/vulkan/sd-server-vulkan" - if [[ ! -x "$CPU_BACKEND_PATH" || ! -x "$CPU_SERVER_PATH" ]] && [[ ! -x "$BACKEND_PATH" || ! -x "$VULKAN_SERVER_PATH" ]]; then - SETUP_REASON="Linux backend binaries are missing or not executable." - fi +# At minimum we need CPU or Vulkan backend on Linux, and both CLI and server binaries must be executable +CPU_SERVER_PATH="$APP_DIR/backend/linux/cpu/sd-server-cpu" +VULKAN_SERVER_PATH="$APP_DIR/backend/linux/vulkan/sd-server-vulkan" +if [[ ! -x "$CPU_BACKEND_PATH" || ! -x "$CPU_SERVER_PATH" ]] && [[ ! -x "$BACKEND_PATH" || ! -x "$VULKAN_SERVER_PATH" ]]; then + SETUP_REASON="Linux backend binaries are missing or not executable." fi if [[ -n "$SETUP_REASON" ]]; then @@ -68,7 +76,7 @@ if [[ -n "$SETUP_REASON" ]]; then echo " ============================================================" echo "" if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then - echo " This looks like your first run. Setting up automatically..." + echo " This looks like your first run on Linux. Setting up automatically..." else echo " Local AI Image Generator needs a quick repair before launch." fi @@ -116,10 +124,7 @@ SERVER_PID=$! sleep 2 # Open browser -if [[ "$PLATFORM" == "Darwin" ]] && command -v open >/dev/null 2>&1; then - echo " Opening browser at http://localhost:${FRONTEND_PORT}" - open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 & -elif command -v xdg-open >/dev/null 2>&1; then +if command -v xdg-open >/dev/null 2>&1; then echo " Opening browser at http://localhost:${FRONTEND_PORT}" xdg-open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 & else diff --git a/mac.sh b/mac.sh new file mode 100755 index 0000000..594de33 --- /dev/null +++ b/mac.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# Local AI Image Generator - macOS Launcher +# Double-click or run: ./mac.sh +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$SCRIPT_DIR/app" +PLATFORM="$(uname -s)" + +if [[ "$PLATFORM" != "Darwin" ]]; then + echo "[ERROR] This script is for macOS only. Please run ./linux.sh on Linux." >&2 + exit 1 +fi + +NODE_DIR="$APP_DIR/tools/node-mac" +NODE_BIN="$NODE_DIR/bin/node" +BACKEND_PATH="$APP_DIR/backend/mac/sd" +PLATFORM_LABEL="macOS" + +DIST_INDEX="$APP_DIR/dist/index.html" +SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh" +SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs" + +FRONTEND_PORT="${FRONTEND_PORT:-1420}" +SETUP_REASON="" +SETUP_MODE="Repair" + +# ── Symlink node_modules to avoid OS conflicts ────────────────────────────── +FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules" +MAC_NODE_MODULES="$APP_DIR/frontend/node_modules_mac" + +if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then + echo " >> Migrating existing node_modules to node_modules_mac..." + mv "$FRONTEND_NODE_MODULES" "$MAC_NODE_MODULES" +fi + +rm -f "$FRONTEND_NODE_MODULES" +mkdir -p "$MAC_NODE_MODULES" +ln -sf "node_modules_mac" "$FRONTEND_NODE_MODULES" + +# ── First-time setup check ───────────────────────────────────────────────── +if [[ ! -d "$NODE_DIR" ]]; then + SETUP_MODE="First-Time Setup" +fi + +if [[ ! -x "$NODE_BIN" ]]; then + SETUP_REASON="Portable Node.js for macOS is missing." +fi + +if [[ ! -f "$DIST_INDEX" ]]; then + SETUP_REASON="Frontend build is missing." +fi + +if [[ ! -x "$BACKEND_PATH" ]]; then + SETUP_REASON="No macOS Metal backend binary is installed." +fi + +if [[ -n "$SETUP_REASON" ]]; then + echo "" + echo " ============================================================" + echo " LOCAL AI IMAGE GENERATOR | $PLATFORM_LABEL $SETUP_MODE" + echo " ============================================================" + echo "" + if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then + echo " This looks like your first run on macOS. Setting up automatically..." + else + echo " Local AI Image Generator needs a quick repair before launch." + fi + echo " Reason: $SETUP_REASON" + echo " Models are not downloaded during setup. Download or import them in the app." + echo "" + read -rp " Press Enter to continue, or Ctrl+C to cancel." + + # Clear any existing frontend server process + if command -v fuser >/dev/null 2>&1; then + fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true + fi + + if ! bash "$SETUP_SCRIPT"; then + echo "" + echo " [ERROR] Setup failed. Please check the output above." + read -rp " Press Enter to close..." + exit 1 + fi +fi + +# ── Launch ───────────────────────────────────────────────────────────────── +clear 2>/dev/null || true +echo "" +echo " ============================================================" +echo " LOCAL AI IMAGE GENERATOR | Launching..." +echo " ============================================================" +echo "" + +# Clear frontend port +if command -v fuser >/dev/null 2>&1; then + fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true +fi + +# Start the server +echo " Starting Local AI Image Generator..." +export PATH="$NODE_DIR/bin:$PATH" +export FRONTEND_PORT="$FRONTEND_PORT" + +# Run server in background and capture PID +"$NODE_BIN" "$SERVE_SCRIPT" & +SERVER_PID=$! + +# Wait for server to be ready +sleep 2 + +# Open browser +if command -v open >/dev/null 2>&1; then + echo " Opening browser at http://localhost:${FRONTEND_PORT}" + open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 & +else + echo " Open your browser to: http://localhost:${FRONTEND_PORT}" +fi + +echo "" +echo " ============================================================" +echo " Running!" +echo " Web UI: http://localhost:${FRONTEND_PORT}" +echo " GPU API: Auto-selected by the app (starts at 8080)" +echo "" +echo " Press Ctrl+C in this window to stop all services." +echo " ============================================================" +echo "" + +# Cleanup on exit +cleanup() { + echo "" + echo " Shutting down..." + if kill -0 "$SERVER_PID" >/dev/null 2>&1; then + kill -TERM "$SERVER_PID" >/dev/null 2>&1 || true + sleep 1 + kill -KILL "$SERVER_PID" >/dev/null 2>&1 || true + fi + echo " Done. Goodbye!" + exit 0 +} +trap cleanup SIGINT SIGTERM + +# Keep script alive +wait "$SERVER_PID" || true +cleanup diff --git a/scripts/reset.ps1 b/scripts/reset.ps1 index 65d1b01..bdc7515 100644 --- a/scripts/reset.ps1 +++ b/scripts/reset.ps1 @@ -45,6 +45,19 @@ if (Test-Path $nodeModulesDir) { Remove-Item $nodeModulesDir -Recurse -Force -ErrorAction SilentlyContinue } +$nodeModulesMac = Join-Path $appDir "frontend\node_modules_mac" +if (Test-Path $nodeModulesMac) { + Write-Host " >> Removing frontend node_modules_mac..." -ForegroundColor Cyan + Remove-Item $nodeModulesMac -Recurse -Force -ErrorAction SilentlyContinue +} + +$nodeModulesLinux = Join-Path $appDir "frontend\node_modules_linux" +if (Test-Path $nodeModulesLinux) { + Write-Host " >> Removing frontend node_modules_linux..." -ForegroundColor Cyan + Remove-Item $nodeModulesLinux -Recurse -Force -ErrorAction SilentlyContinue +} + + # Delete package-lock.json in frontend $lockFile = Join-Path $appDir "frontend\package-lock.json" if (Test-Path $lockFile) { diff --git a/scripts/reset.sh b/scripts/reset.sh index ca27b07..9334147 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -40,11 +40,21 @@ if [[ -d "$APP_DIR/models" ]]; then fi # Delete node_modules in frontend -if [[ -d "$APP_DIR/frontend/node_modules" ]]; then +if [[ -L "$APP_DIR/frontend/node_modules" || -d "$APP_DIR/frontend/node_modules" ]]; then echo " >> Removing frontend node_modules..." rm -rf "$APP_DIR/frontend/node_modules" fi +if [[ -d "$APP_DIR/frontend/node_modules_mac" ]]; then + echo " >> Removing frontend node_modules_mac..." + rm -rf "$APP_DIR/frontend/node_modules_mac" +fi + +if [[ -d "$APP_DIR/frontend/node_modules_linux" ]]; then + echo " >> Removing frontend node_modules_linux..." + rm -rf "$APP_DIR/frontend/node_modules_linux" +fi + # Delete package-lock.json in frontend if [[ -f "$APP_DIR/frontend/package-lock.json" ]]; then echo " >> Removing frontend package-lock.json..." diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index f3664e2..cfa6836 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -388,6 +388,16 @@ if (-not (Test-Path $npmCmd)) { Read-Host; exit 1 } +# If node_modules is a Unix symlink/junction, remove it so Windows can install natively into a real directory +$nodeModulesDir = Join-Path $frontendDir "node_modules" +if (Test-Path $nodeModulesDir) { + $item = Get-Item $nodeModulesDir + if ($item.Attributes -match "ReparsePoint") { + Print-Info "Removing Unix symlink for node_modules on Windows..." + Remove-Item $nodeModulesDir -Force -ErrorAction SilentlyContinue + } +} + Push-Location $frontendDir $oldPath = $env:PATH try { @@ -419,7 +429,7 @@ try { # ── Done ───────────────────────────────────────────────────────────────────── Write-Host "" Write-Host " ============================================================" -ForegroundColor Green -Write-Host " Setup complete! Just double-click start.bat to launch." -ForegroundColor Green +Write-Host " Setup complete! Just double-click windows.bat to launch." -ForegroundColor Green Write-Host " ============================================================" -ForegroundColor Green Write-Host "" Read-Host " Press Enter to close..." diff --git a/scripts/setup.sh b/scripts/setup.sh index b8dc7f1..0caff8a 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -361,6 +361,25 @@ if [[ ! -x "$NPM_BIN" ]]; then exit 1 fi +# Ensure correct OS-specific node_modules folder is symlinked to avoid conflicts +FRONTEND_NODE_MODULES="$FRONTEND_DIR/node_modules" +if [[ "$PLATFORM" == "Darwin" ]]; then + OS_NODE_MODULES="$FRONTEND_DIR/node_modules_mac" + OS_LABEL="node_modules_mac" +else + OS_NODE_MODULES="$FRONTEND_DIR/node_modules_linux" + OS_LABEL="node_modules_linux" +fi + +if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then + print_info "Migrating existing node_modules to $OS_LABEL..." + mv "$FRONTEND_NODE_MODULES" "$OS_NODE_MODULES" +fi + +rm -f "$FRONTEND_NODE_MODULES" +mkdir -p "$OS_NODE_MODULES" +ln -sf "$OS_LABEL" "$FRONTEND_NODE_MODULES" + cd "$FRONTEND_DIR" export PATH="$NODE_DIR/bin:$PATH" @@ -384,7 +403,11 @@ fi # ── Done ──────────────────────────────────────────────────────────────────── echo "" echo " ============================================================" -echo " Setup complete! Run ./start.sh to launch." +if [[ "$PLATFORM" == "Darwin" ]]; then + echo " Setup complete! Run ./mac.sh to launch." +else + echo " Setup complete! Run ./linux.sh to launch." +fi echo " ============================================================" echo "" diff --git a/start.bat b/windows.bat similarity index 100% rename from start.bat rename to windows.bat From 4a8b129c07a6624495cd8b1823f2969109b55f07 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sat, 13 Jun 2026 15:36:33 +0530 Subject: [PATCH 3/8] feat: implement model management system and integrate OpenVINO NPU support for image generation. --- .gitattributes | 1 + .gitignore | 9 ++++----- app/dist/assets/event-Df1n27Ua.js | 1 - app/dist/assets/index-CG3WZqfX.js | 23 ----------------------- app/dist/assets/index-DW6G7wc2.css | 1 - app/dist/index.html | 18 ------------------ 6 files changed, 5 insertions(+), 48 deletions(-) create mode 100644 .gitattributes delete mode 100644 app/dist/assets/event-Df1n27Ua.js delete mode 100644 app/dist/assets/index-CG3WZqfX.js delete mode 100644 app/dist/assets/index-DW6G7wc2.css delete mode 100644 app/dist/index.html diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 541bb3b..c0b03c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,12 @@ app/models/*.safetensors app/models/*.gguf app/models/*.ckpt app/models +app/openvino-models/ app/outputs/ +# Ignore generated frontend build output +app/dist/ + # Ignore portable Node.js binaries and packages app/tools/ **/node_modules/ @@ -22,8 +26,3 @@ app/backend/ Thumbs.db *.log *.tmp - - - -# output images folder -/app/outputs \ No newline at end of file diff --git a/app/dist/assets/event-Df1n27Ua.js b/app/dist/assets/event-Df1n27Ua.js deleted file mode 100644 index 4628874..0000000 --- a/app/dist/assets/event-Df1n27Ua.js +++ /dev/null @@ -1 +0,0 @@ -import{i as D,t as i}from"./index-CG3WZqfX.js";var _;(function(r){r.WINDOW_RESIZED="tauri://resize",r.WINDOW_MOVED="tauri://move",r.WINDOW_CLOSE_REQUESTED="tauri://close-requested",r.WINDOW_DESTROYED="tauri://destroyed",r.WINDOW_FOCUS="tauri://focus",r.WINDOW_BLUR="tauri://blur",r.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",r.WINDOW_THEME_CHANGED="tauri://theme-changed",r.WINDOW_CREATED="tauri://window-created",r.WINDOW_SUSPENDED="tauri://suspended",r.WINDOW_RESUMED="tauri://resumed",r.WEBVIEW_CREATED="tauri://webview-created",r.DRAG_ENTER="tauri://drag-enter",r.DRAG_OVER="tauri://drag-over",r.DRAG_DROP="tauri://drag-drop",r.DRAG_LEAVE="tauri://drag-leave"})(_||(_={}));async function l(r,a){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(r,a),await D("plugin:event|unlisten",{event:r,eventId:a})}async function n(r,a,e){var t;const W=typeof e?.target=="string"?{kind:"AnyLabel",label:e.target}:(t=e?.target)!==null&&t!==void 0?t:{kind:"Any"};return D("plugin:event|listen",{event:r,target:W,handler:i(a)}).then(d=>async()=>l(r,d))}export{_ as TauriEvent,n as listen}; diff --git a/app/dist/assets/index-CG3WZqfX.js b/app/dist/assets/index-CG3WZqfX.js deleted file mode 100644 index 8b288ae..0000000 --- a/app/dist/assets/index-CG3WZqfX.js +++ /dev/null @@ -1,23 +0,0 @@ -(function(){const b=document.createElement("link").relList;if(b&&b.supports&&b.supports("modulepreload"))return;for(const x of document.querySelectorAll('link[rel="modulepreload"]'))r(x);new MutationObserver(x=>{for(const E of x)if(E.type==="childList")for(const q of E.addedNodes)q.tagName==="LINK"&&q.rel==="modulepreload"&&r(q)}).observe(document,{childList:!0,subtree:!0});function N(x){const E={};return x.integrity&&(E.integrity=x.integrity),x.referrerPolicy&&(E.referrerPolicy=x.referrerPolicy),x.crossOrigin==="use-credentials"?E.credentials="include":x.crossOrigin==="anonymous"?E.credentials="omit":E.credentials="same-origin",E}function r(x){if(x.ep)return;x.ep=!0;const E=N(x);fetch(x.href,E)}})();function fm(f){return f&&f.__esModule&&Object.prototype.hasOwnProperty.call(f,"default")?f.default:f}var Rs={exports:{}},ai={};var Zd;function Ny(){if(Zd)return ai;Zd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.fragment");function N(r,x,E){var q=null;if(E!==void 0&&(q=""+E),x.key!==void 0&&(q=""+x.key),"key"in x){E={};for(var H in x)H!=="key"&&(E[H]=x[H])}else E=x;return x=E.ref,{$$typeof:f,type:r,key:q,ref:x!==void 0?x:null,props:E}}return ai.Fragment=b,ai.jsx=N,ai.jsxs=N,ai}var Kd;function Ey(){return Kd||(Kd=1,Rs.exports=Ny()),Rs.exports}var c=Ey(),Bs={exports:{}},le={};var Jd;function Ay(){if(Jd)return le;Jd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.portal"),N=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),x=Symbol.for("react.profiler"),E=Symbol.for("react.consumer"),q=Symbol.for("react.context"),H=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),A=Symbol.for("react.lazy"),O=Symbol.for("react.activity"),Q=Symbol.iterator;function P(d){return d===null||typeof d!="object"?null:(d=Q&&d[Q]||d["@@iterator"],typeof d=="function"?d:null)}var J={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},$=Object.assign,fe={};function te(d,j,R){this.props=d,this.context=j,this.refs=fe,this.updater=R||J}te.prototype.isReactComponent={},te.prototype.setState=function(d,j){if(typeof d!="object"&&typeof d!="function"&&d!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,d,j,"setState")},te.prototype.forceUpdate=function(d){this.updater.enqueueForceUpdate(this,d,"forceUpdate")};function lt(){}lt.prototype=te.prototype;function Ae(d,j,R){this.props=d,this.context=j,this.refs=fe,this.updater=R||J}var ke=Ae.prototype=new lt;ke.constructor=Ae,$(ke,te.prototype),ke.isPureReactComponent=!0;var Ve=Array.isArray;function De(){}var ee={H:null,A:null,T:null,S:null},Se=Object.prototype.hasOwnProperty;function ot(d,j,R){var G=R.ref;return{$$typeof:f,type:d,key:j,ref:G!==void 0?G:null,props:R}}function Mt(d,j){return ot(d.type,j,d.props)}function de(d){return typeof d=="object"&&d!==null&&d.$$typeof===f}function Ze(d){var j={"=":"=0",":":"=2"};return"$"+d.replace(/[=:]/g,function(R){return j[R]})}var ct=/\/+/g;function Ke(d,j){return typeof d=="object"&&d!==null&&d.key!=null?Ze(""+d.key):j.toString(36)}function Le(d){switch(d.status){case"fulfilled":return d.value;case"rejected":throw d.reason;default:switch(typeof d.status=="string"?d.then(De,De):(d.status="pending",d.then(function(j){d.status==="pending"&&(d.status="fulfilled",d.value=j)},function(j){d.status==="pending"&&(d.status="rejected",d.reason=j)})),d.status){case"fulfilled":return d.value;case"rejected":throw d.reason}}throw d}function _(d,j,R,G,V){var I=typeof d;(I==="undefined"||I==="boolean")&&(d=null);var se=!1;if(d===null)se=!0;else switch(I){case"bigint":case"string":case"number":se=!0;break;case"object":switch(d.$$typeof){case f:case b:se=!0;break;case A:return se=d._init,_(se(d._payload),j,R,G,V)}}if(se)return V=V(d),se=G===""?"."+Ke(d,0):G,Ve(V)?(R="",se!=null&&(R=se.replace(ct,"$&/")+"/"),_(V,j,R,"",function(yt){return yt})):V!=null&&(de(V)&&(V=Mt(V,R+(V.key==null||d&&d.key===V.key?"":(""+V.key).replace(ct,"$&/")+"/")+se)),j.push(V)),1;se=0;var Qe=G===""?".":G+":";if(Ve(d))for(var Me=0;Me>>1,ce=_[me];if(0>>1;mex(R,Z))Gx(V,R)?(_[me]=V,_[G]=Z,me=G):(_[me]=R,_[j]=Z,me=j);else if(Gx(V,Z))_[me]=V,_[G]=Z,me=G;else break e}}return C}function x(_,C){var Z=_.sortIndex-C.sortIndex;return Z!==0?Z:_.id-C.id}if(f.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var E=performance;f.unstable_now=function(){return E.now()}}else{var q=Date,H=q.now();f.unstable_now=function(){return q.now()-H}}var w=[],h=[],A=1,O=null,Q=3,P=!1,J=!1,$=!1,fe=!1,te=typeof setTimeout=="function"?setTimeout:null,lt=typeof clearTimeout=="function"?clearTimeout:null,Ae=typeof setImmediate<"u"?setImmediate:null;function ke(_){for(var C=N(h);C!==null;){if(C.callback===null)r(h);else if(C.startTime<=_)r(h),C.sortIndex=C.expirationTime,b(w,C);else break;C=N(h)}}function Ve(_){if($=!1,ke(_),!J)if(N(w)!==null)J=!0,De||(De=!0,Ze());else{var C=N(h);C!==null&&Le(Ve,C.startTime-_)}}var De=!1,ee=-1,Se=5,ot=-1;function Mt(){return fe?!0:!(f.unstable_now()-ot_&&Mt());){var me=O.callback;if(typeof me=="function"){O.callback=null,Q=O.priorityLevel;var ce=me(O.expirationTime<=_);if(_=f.unstable_now(),typeof ce=="function"){O.callback=ce,ke(_),C=!0;break t}O===N(w)&&r(w),ke(_)}else r(w);O=N(w)}if(O!==null)C=!0;else{var d=N(h);d!==null&&Le(Ve,d.startTime-_),C=!1}}break e}finally{O=null,Q=Z,P=!1}C=void 0}}finally{C?Ze():De=!1}}}var Ze;if(typeof Ae=="function")Ze=function(){Ae(de)};else if(typeof MessageChannel<"u"){var ct=new MessageChannel,Ke=ct.port2;ct.port1.onmessage=de,Ze=function(){Ke.postMessage(null)}}else Ze=function(){te(de,0)};function Le(_,C){ee=te(function(){_(f.unstable_now())},C)}f.unstable_IdlePriority=5,f.unstable_ImmediatePriority=1,f.unstable_LowPriority=4,f.unstable_NormalPriority=3,f.unstable_Profiling=null,f.unstable_UserBlockingPriority=2,f.unstable_cancelCallback=function(_){_.callback=null},f.unstable_forceFrameRate=function(_){0>_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):Se=0<_?Math.floor(1e3/_):5},f.unstable_getCurrentPriorityLevel=function(){return Q},f.unstable_next=function(_){switch(Q){case 1:case 2:case 3:var C=3;break;default:C=Q}var Z=Q;Q=C;try{return _()}finally{Q=Z}},f.unstable_requestPaint=function(){fe=!0},f.unstable_runWithPriority=function(_,C){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var Z=Q;Q=_;try{return C()}finally{Q=Z}},f.unstable_scheduleCallback=function(_,C,Z){var me=f.unstable_now();switch(typeof Z=="object"&&Z!==null?(Z=Z.delay,Z=typeof Z=="number"&&0me?(_.sortIndex=Z,b(h,_),N(w)===null&&_===N(h)&&($?(lt(ee),ee=-1):$=!0,Le(Ve,Z-me))):(_.sortIndex=ce,b(w,_),J||P||(J=!0,De||(De=!0,Ze()))),_},f.unstable_shouldYield=Mt,f.unstable_wrapCallback=function(_){var C=Q;return function(){var Z=Q;Q=C;try{return _.apply(this,arguments)}finally{Q=Z}}}})(qs)),qs}var Fd;function Oy(){return Fd||(Fd=1,Gs.exports=Cy()),Gs.exports}var Ls={exports:{}},Tt={};var Id;function Uy(){if(Id)return Tt;Id=1;var f=Zs();function b(w){var h="https://react.dev/errors/"+w;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(f)}catch(b){console.error(b)}}return f(),Ls.exports=Uy(),Ls.exports}var em;function Ry(){if(em)return ni;em=1;var f=Oy(),b=Zs(),N=wy();function r(e){var t="https://react.dev/errors/"+e;if(1ce||(e.current=me[ce],me[ce]=null,ce--)}function R(e,t){ce++,me[ce]=e.current,e.current=t}var G=d(null),V=d(null),I=d(null),se=d(null);function Qe(e,t){switch(R(I,t),R(V,e),R(G,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=yd(t),e=gd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}j(G),R(G,e)}function Me(){j(G),j(V),j(I)}function yt(e){e.memoizedState!==null&&R(se,e);var t=G.current,l=gd(t,e.type);t!==l&&(R(V,e),R(G,l))}function Nt(e){V.current===e&&(j(G),j(V)),se.current===e&&(j(se),Pn._currentValue=Z)}var Je,Ht;function zt(e){if(Je===void 0)try{throw Error()}catch(l){var t=l.stack.trim().match(/\n( *(at )?)/);Je=t&&t[1]||"",Ht=-1)":-1n||o[a]!==p[n]){var z=` -`+o[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{k=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?zt(l):""}function D(e,t){switch(e.tag){case 26:case 27:case 5:return zt(e.type);case 16:return zt("Lazy");case 13:return e.child!==t&&t!==null?zt("Suspense Fallback"):zt("Suspense");case 19:return zt("SuspenseList");case 0:case 15:return ie(e.type,!1);case 11:return ie(e.type.render,!1);case 1:return ie(e.type,!0);case 31:return zt("Activity");default:return""}}function B(e){try{var t="",l=null;do t+=D(e,l),l=e,e=e.return;while(e);return t}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var W=Object.prototype.hasOwnProperty,ae=f.unstable_scheduleCallback,At=f.unstable_cancelCallback,Gt=f.unstable_shouldYield,dl=f.unstable_requestPaint,we=f.unstable_now,al=f.unstable_getCurrentPriorityLevel,$e=f.unstable_ImmediatePriority,za=f.unstable_UserBlockingPriority,Ta=f.unstable_NormalPriority,L=f.unstable_LowPriority,he=f.unstable_IdlePriority,Re=f.log,gt=f.unstable_setDisableYieldValue,st=null,Pe=null;function qt(e){if(typeof Re=="function"&>(e),Pe&&typeof Pe.setStrictMode=="function")try{Pe.setStrictMode(st,e)}catch{}}var rt=Math.clz32?Math.clz32:be,pt=Math.log,ve=Math.LN2;function be(e){return e>>>=0,e===0?32:31-(pt(e)/ve|0)|0}var Be=256,xe=262144,We=4194304;function _e(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function dt(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,i=e.suspendedLanes,u=e.pingedLanes;e=e.warmLanes;var s=a&134217727;return s!==0?(a=s&~i,a!==0?n=_e(a):(u&=s,u!==0?n=_e(u):l||(l=s&~e,l!==0&&(n=_e(l))))):(s=a&~i,s!==0?n=_e(s):u!==0?n=_e(u):l||(l=a&~e,l!==0&&(n=_e(l)))),n===0?0:t!==0&&t!==n&&(t&i)===0&&(i=n&-n,l=t&-t,i>=l||i===32&&(l&4194048)!==0)?t:n}function vt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function cl(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ci(){var e=We;return We<<=1,(We&62914560)===0&&(We=4194304),e}function ju(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function mn(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ym(e,t,l,a,n,i){var u=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var s=e.entanglements,o=e.expirationTimes,p=e.hiddenUpdates;for(l=u&~l;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var xm=/[\n"\\]/g;function Jt(e){return e.replace(xm,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Au(e,t,l,a,n,i,u,s){e.name="",u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"?e.type=u:e.removeAttribute("type"),t!=null?u==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Kt(t)):e.value!==""+Kt(t)&&(e.value=""+Kt(t)):u!=="submit"&&u!=="reset"||e.removeAttribute("value"),t!=null?Du(e,u,Kt(t)):l!=null?Du(e,u,Kt(l)):a!=null&&e.removeAttribute("value"),n==null&&i!=null&&(e.defaultChecked=!!i),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"?e.name=""+Kt(s):e.removeAttribute("name")}function sf(e,t,l,a,n,i,u,s){if(i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(e.type=i),t!=null||l!=null){if(!(i!=="submit"&&i!=="reset"||t!=null)){Eu(e);return}l=l!=null?""+Kt(l):"",t=t!=null?""+Kt(t):l,s||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=s?e.checked:!!a,e.defaultChecked=!!a,u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.name=u),Eu(e)}function Du(e,t,l){t==="number"&&oi(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function Ca(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ru=!1;if(yl)try{var pn={};Object.defineProperty(pn,"passive",{get:function(){Ru=!0}}),window.addEventListener("test",pn,pn),window.removeEventListener("test",pn,pn)}catch{Ru=!1}var Ul=null,Bu=null,di=null;function yf(){if(di)return di;var e,t=Bu,l=t.length,a,n="value"in Ul?Ul.value:Ul.textContent,i=n.length;for(e=0;e=Sn),xf=" ",_f=!1;function jf(e,t){switch(e){case"keyup":return $m.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function zf(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ra=!1;function Fm(e,t){switch(e){case"compositionend":return zf(t);case"keypress":return t.which!==32?null:(_f=!0,xf);case"textInput":return e=t.data,e===xf&&_f?null:e;default:return null}}function Im(e,t){if(Ra)return e==="compositionend"||!Yu&&jf(e,t)?(e=yf(),di=Bu=Ul=null,Ra=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=Of(l)}}function wf(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wf(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Rf(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=oi(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=oi(e.document)}return t}function ku(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var uh=yl&&"documentMode"in document&&11>=document.documentMode,Ba=null,Vu=null,zn=null,Zu=!1;function Bf(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Zu||Ba==null||Ba!==oi(a)||(a=Ba,"selectionStart"in a&&ku(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),zn&&jn(zn,a)||(zn=a,a=iu(Vu,"onSelect"),0>=u,n-=u,sl=1<<32-rt(t)+n|l<ue?(ge=X,X=null):ge=X.sibling;var ze=v(y,X,g[ue],T);if(ze===null){X===null&&(X=ge);break}e&&X&&ze.alternate===null&&t(y,X),m=i(ze,m,ue),je===null?K=ze:je.sibling=ze,je=ze,X=ge}if(ue===g.length)return l(y,X),pe&&pl(y,ue),K;if(X===null){for(;ueue?(ge=X,X=null):ge=X.sibling;var ta=v(y,X,ze.value,T);if(ta===null){X===null&&(X=ge);break}e&&X&&ta.alternate===null&&t(y,X),m=i(ta,m,ue),je===null?K=ta:je.sibling=ta,je=ta,X=ge}if(ze.done)return l(y,X),pe&&pl(y,ue),K;if(X===null){for(;!ze.done;ue++,ze=g.next())ze=M(y,ze.value,T),ze!==null&&(m=i(ze,m,ue),je===null?K=ze:je.sibling=ze,je=ze);return pe&&pl(y,ue),K}for(X=a(X);!ze.done;ue++,ze=g.next())ze=S(X,y,ue,ze.value,T),ze!==null&&(e&&ze.alternate!==null&&X.delete(ze.key===null?ue:ze.key),m=i(ze,m,ue),je===null?K=ze:je.sibling=ze,je=ze);return e&&X.forEach(function(My){return t(y,My)}),pe&&pl(y,ue),K}function Ue(y,m,g,T){if(typeof g=="object"&&g!==null&&g.type===$&&g.key===null&&(g=g.props.children),typeof g=="object"&&g!==null){switch(g.$$typeof){case P:e:{for(var K=g.key;m!==null;){if(m.key===K){if(K=g.type,K===$){if(m.tag===7){l(y,m.sibling),T=n(m,g.props.children),T.return=y,y=T;break e}}else if(m.elementType===K||typeof K=="object"&&K!==null&&K.$$typeof===Se&&ga(K)===m.type){l(y,m.sibling),T=n(m,g.props),Dn(T,g),T.return=y,y=T;break e}l(y,m);break}else t(y,m);m=m.sibling}g.type===$?(T=ra(g.props.children,y.mode,T,g.key),T.return=y,y=T):(T=_i(g.type,g.key,g.props,null,y.mode,T),Dn(T,g),T.return=y,y=T)}return u(y);case J:e:{for(K=g.key;m!==null;){if(m.key===K)if(m.tag===4&&m.stateNode.containerInfo===g.containerInfo&&m.stateNode.implementation===g.implementation){l(y,m.sibling),T=n(m,g.children||[]),T.return=y,y=T;break e}else{l(y,m);break}else t(y,m);m=m.sibling}T=Pu(g,y.mode,T),T.return=y,y=T}return u(y);case Se:return g=ga(g),Ue(y,m,g,T)}if(Le(g))return Y(y,m,g,T);if(Ze(g)){if(K=Ze(g),typeof K!="function")throw Error(r(150));return g=K.call(g),F(y,m,g,T)}if(typeof g.then=="function")return Ue(y,m,Ai(g),T);if(g.$$typeof===Ae)return Ue(y,m,Ti(y,g),T);Di(y,g)}return typeof g=="string"&&g!==""||typeof g=="number"||typeof g=="bigint"?(g=""+g,m!==null&&m.tag===6?(l(y,m.sibling),T=n(m,g),T.return=y,y=T):(l(y,m),T=Iu(g,y.mode,T),T.return=y,y=T),u(y)):l(y,m)}return function(y,m,g,T){try{An=0;var K=Ue(y,m,g,T);return Ka=null,K}catch(X){if(X===Za||X===Ni)throw X;var je=Yt(29,X,null,y.mode);return je.lanes=T,je.return=y,je}}}var va=io(!0),uo=io(!1),Gl=!1;function rc(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function dc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function ql(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ll(e,t,l){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Te&2)!==0){var n=a.pending;return n===null?t.next=t:(t.next=n.next,n.next=t),a.pending=t,t=xi(e),Qf(e,null,l),t}return Si(e,a,t,l),xi(e)}function Cn(e,t,l){if(t=t.updateQueue,t!==null&&(t=t.shared,(l&4194048)!==0)){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Ws(e,l)}}function mc(e,t){var l=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,i=null;if(l=l.firstBaseUpdate,l!==null){do{var u={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};i===null?n=i=u:i=i.next=u,l=l.next}while(l!==null);i===null?n=i=t:i=i.next=t}else n=i=t;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:i,shared:a.shared,callbacks:a.callbacks},e.updateQueue=l;return}e=l.lastBaseUpdate,e===null?l.firstBaseUpdate=t:e.next=t,l.lastBaseUpdate=t}var hc=!1;function On(){if(hc){var e=Va;if(e!==null)throw e}}function Un(e,t,l,a){hc=!1;var n=e.updateQueue;Gl=!1;var i=n.firstBaseUpdate,u=n.lastBaseUpdate,s=n.shared.pending;if(s!==null){n.shared.pending=null;var o=s,p=o.next;o.next=null,u===null?i=p:u.next=p,u=o;var z=e.alternate;z!==null&&(z=z.updateQueue,s=z.lastBaseUpdate,s!==u&&(s===null?z.firstBaseUpdate=p:s.next=p,z.lastBaseUpdate=o))}if(i!==null){var M=n.baseState;u=0,z=p=o=null,s=i;do{var v=s.lane&-536870913,S=v!==s.lane;if(S?(ye&v)===v:(a&v)===v){v!==0&&v===ka&&(hc=!0),z!==null&&(z=z.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});e:{var Y=e,F=s;v=t;var Ue=l;switch(F.tag){case 1:if(Y=F.payload,typeof Y=="function"){M=Y.call(Ue,M,v);break e}M=Y;break e;case 3:Y.flags=Y.flags&-65537|128;case 0:if(Y=F.payload,v=typeof Y=="function"?Y.call(Ue,M,v):Y,v==null)break e;M=O({},M,v);break e;case 2:Gl=!0}}v=s.callback,v!==null&&(e.flags|=64,S&&(e.flags|=8192),S=n.callbacks,S===null?n.callbacks=[v]:S.push(v))}else S={lane:v,tag:s.tag,payload:s.payload,callback:s.callback,next:null},z===null?(p=z=S,o=M):z=z.next=S,u|=v;if(s=s.next,s===null){if(s=n.shared.pending,s===null)break;S=s,s=S.next,S.next=null,n.lastBaseUpdate=S,n.shared.pending=null}}while(!0);z===null&&(o=M),n.baseState=o,n.firstBaseUpdate=p,n.lastBaseUpdate=z,i===null&&(n.shared.lanes=0),Vl|=u,e.lanes=u,e.memoizedState=M}}function co(e,t){if(typeof e!="function")throw Error(r(191,e));e.call(t)}function so(e,t){var l=e.callbacks;if(l!==null)for(e.callbacks=null,e=0;ei?i:8;var u=_.T,s={};_.T=s,Uc(e,!1,t,l);try{var o=n(),p=_.S;if(p!==null&&p(s,o),o!==null&&typeof o=="object"&&typeof o.then=="function"){var z=yh(o,a);Bn(e,t,z,Zt(e))}else Bn(e,t,a,Zt(e))}catch(M){Bn(e,t,{then:function(){},status:"rejected",reason:M},Zt())}finally{C.p=i,u!==null&&s.types!==null&&(u.types=s.types),_.T=u}}function xh(){}function Cc(e,t,l,a){if(e.tag!==5)throw Error(r(476));var n=Yo(e).queue;Lo(e,n,t,Z,l===null?xh:function(){return Xo(e),l(a)})}function Yo(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:Z,baseState:Z,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xl,lastRenderedState:Z},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xl,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Xo(e){var t=Yo(e);t.next===null&&(t=e.alternate.memoizedState),Bn(e,t.next.queue,{},Zt())}function Oc(){return xt(Pn)}function Qo(){return tt().memoizedState}function ko(){return tt().memoizedState}function _h(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=Zt();e=ql(l);var a=Ll(t,e,l);a!==null&&(Bt(a,t,l),Cn(a,t,l)),t={cache:cc()},e.payload=t;return}t=t.return}}function jh(e,t,l){var a=Zt();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Li(e)?Zo(t,l):(l=Wu(e,t,l,a),l!==null&&(Bt(l,e,a),Ko(l,t,a)))}function Vo(e,t,l){var a=Zt();Bn(e,t,l,a)}function Bn(e,t,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(Li(e))Zo(t,n);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var u=t.lastRenderedState,s=i(u,l);if(n.hasEagerState=!0,n.eagerState=s,Lt(s,u))return Si(e,t,n,0),He===null&&bi(),!1}catch{}if(l=Wu(e,t,n,a),l!==null)return Bt(l,e,a),Ko(l,t,a),!0}return!1}function Uc(e,t,l,a){if(a={lane:2,revertLane:rs(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Li(e)){if(t)throw Error(r(479))}else t=Wu(e,l,a,2),t!==null&&Bt(t,e,2)}function Li(e){var t=e.alternate;return e===ne||t!==null&&t===ne}function Zo(e,t){$a=Ui=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function Ko(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Ws(e,l)}}var Hn={readContext:xt,use:Bi,useCallback:Fe,useContext:Fe,useEffect:Fe,useImperativeHandle:Fe,useLayoutEffect:Fe,useInsertionEffect:Fe,useMemo:Fe,useReducer:Fe,useRef:Fe,useState:Fe,useDebugValue:Fe,useDeferredValue:Fe,useTransition:Fe,useSyncExternalStore:Fe,useId:Fe,useHostTransitionStatus:Fe,useFormState:Fe,useActionState:Fe,useOptimistic:Fe,useMemoCache:Fe,useCacheRefresh:Fe};Hn.useEffectEvent=Fe;var Jo={readContext:xt,use:Bi,useCallback:function(e,t){return Et().memoizedState=[e,t===void 0?null:t],e},useContext:xt,useEffect:Co,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Gi(4194308,4,Ro.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Gi(4194308,4,e,t)},useInsertionEffect:function(e,t){Gi(4,2,e,t)},useMemo:function(e,t){var l=Et();t=t===void 0?null:t;var a=e();if(ba){qt(!0);try{e()}finally{qt(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=Et();if(l!==void 0){var n=l(t);if(ba){qt(!0);try{l(t)}finally{qt(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=jh.bind(null,ne,e),[a.memoizedState,e]},useRef:function(e){var t=Et();return e={current:e},t.memoizedState=e},useState:function(e){e=Mc(e);var t=e.queue,l=Vo.bind(null,ne,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:Ac,useDeferredValue:function(e,t){var l=Et();return Dc(l,e,t)},useTransition:function(){var e=Mc(!1);return e=Lo.bind(null,ne,e.queue,!0,!1),Et().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ne,n=Et();if(pe){if(l===void 0)throw Error(r(407));l=l()}else{if(l=t(),He===null)throw Error(r(349));(ye&127)!==0||yo(a,t,l)}n.memoizedState=l;var i={value:l,getSnapshot:t};return n.queue=i,Co(po.bind(null,a,i,e),[e]),a.flags|=2048,Fa(9,{destroy:void 0},go.bind(null,a,i,l,t),null),l},useId:function(){var e=Et(),t=He.identifierPrefix;if(pe){var l=fl,a=sl;l=(a&~(1<<32-rt(a)-1)).toString(32)+l,t="_"+t+"R_"+l,l=wi++,0<\/script>",i=i.removeChild(i.firstChild);break;case"select":i=typeof a.is=="string"?u.createElement("select",{is:a.is}):u.createElement("select"),a.multiple?i.multiple=!0:a.size&&(i.size=a.size);break;default:i=typeof a.is=="string"?u.createElement(n,{is:a.is}):u.createElement(n)}}i[bt]=t,i[Dt]=a;e:for(u=t.child;u!==null;){if(u.tag===5||u.tag===6)i.appendChild(u.stateNode);else if(u.tag!==4&&u.tag!==27&&u.child!==null){u.child.return=u,u=u.child;continue}if(u===t)break e;for(;u.sibling===null;){if(u.return===null||u.return===t)break e;u=u.return}u.sibling.return=u.return,u=u.sibling}t.stateNode=i;e:switch(jt(i,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break e;case"img":a=!0;break e;default:a=!1}a&&jl(t)}}return qe(t),Kc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,l),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==a&&jl(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(r(166));if(e=I.current,Xa(t)){if(e=t.stateNode,l=t.memoizedProps,a=null,n=St,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}e[bt]=t,e=!!(e.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||md(e.nodeValue,l)),e||Bl(t,!0)}else e=uu(e).createTextNode(a),e[bt]=t,t.stateNode=e}return qe(t),null;case 31:if(l=t.memoizedState,e===null||e.memoizedState!==null){if(a=Xa(t),l!==null){if(e===null){if(!a)throw Error(r(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(r(557));e[bt]=t}else da(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;qe(t),e=!1}else l=ac(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=l),e=!0;if(!e)return t.flags&256?(Qt(t),t):(Qt(t),null);if((t.flags&128)!==0)throw Error(r(558))}return qe(t),null;case 13:if(a=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(n=Xa(t),a!==null&&a.dehydrated!==null){if(e===null){if(!n)throw Error(r(318));if(n=t.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(r(317));n[bt]=t}else da(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;qe(t),n=!1}else n=ac(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),n=!0;if(!n)return t.flags&256?(Qt(t),t):(Qt(t),null)}return Qt(t),(t.flags&128)!==0?(t.lanes=l,t):(l=a!==null,e=e!==null&&e.memoizedState!==null,l&&(a=t.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),i=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(i=a.memoizedState.cachePool.pool),i!==n&&(a.flags|=2048)),l!==e&&l&&(t.child.flags|=8192),Vi(t,t.updateQueue),qe(t),null);case 4:return Me(),e===null&&ys(t.stateNode.containerInfo),qe(t),null;case 10:return bl(t.type),qe(t),null;case 19:if(j(et),a=t.memoizedState,a===null)return qe(t),null;if(n=(t.flags&128)!==0,i=a.rendering,i===null)if(n)qn(a,!1);else{if(Ie!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(i=Oi(e),i!==null){for(t.flags|=128,qn(a,!1),e=i.updateQueue,t.updateQueue=e,Vi(t,e),t.subtreeFlags=0,e=l,l=t.child;l!==null;)kf(l,e),l=l.sibling;return R(et,et.current&1|2),pe&&pl(t,a.treeForkCount),t.child}e=e.sibling}a.tail!==null&&we()>Wi&&(t.flags|=128,n=!0,qn(a,!1),t.lanes=4194304)}else{if(!n)if(e=Oi(i),e!==null){if(t.flags|=128,n=!0,e=e.updateQueue,t.updateQueue=e,Vi(t,e),qn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!pe)return qe(t),null}else 2*we()-a.renderingStartTime>Wi&&l!==536870912&&(t.flags|=128,n=!0,qn(a,!1),t.lanes=4194304);a.isBackwards?(i.sibling=t.child,t.child=i):(e=a.last,e!==null?e.sibling=i:t.child=i,a.last=i)}return a.tail!==null?(e=a.tail,a.rendering=e,a.tail=e.sibling,a.renderingStartTime=we(),e.sibling=null,l=et.current,R(et,n?l&1|2:l&1),pe&&pl(t,a.treeForkCount),e):(qe(t),null);case 22:case 23:return Qt(t),gc(),a=t.memoizedState!==null,e!==null?e.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(l&536870912)!==0&&(t.flags&128)===0&&(qe(t),t.subtreeFlags&6&&(t.flags|=8192)):qe(t),l=t.updateQueue,l!==null&&Vi(t,l.retryQueue),l=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==l&&(t.flags|=2048),e!==null&&j(ya),null;case 24:return l=null,e!==null&&(l=e.memoizedState.cache),t.memoizedState.cache!==l&&(t.flags|=2048),bl(at),qe(t),null;case 25:return null;case 30:return null}throw Error(r(156,t.tag))}function Eh(e,t){switch(tc(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return bl(at),Me(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Nt(t),null;case 31:if(t.memoizedState!==null){if(Qt(t),t.alternate===null)throw Error(r(340));da()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Qt(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));da()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return j(et),null;case 4:return Me(),null;case 10:return bl(t.type),null;case 22:case 23:return Qt(t),gc(),e!==null&&j(ya),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return bl(at),null;case 25:return null;default:return null}}function vr(e,t){switch(tc(t),t.tag){case 3:bl(at),Me();break;case 26:case 27:case 5:Nt(t);break;case 4:Me();break;case 31:t.memoizedState!==null&&Qt(t);break;case 13:Qt(t);break;case 19:j(et);break;case 10:bl(t.type);break;case 22:case 23:Qt(t),gc(),e!==null&&j(ya);break;case 24:bl(at)}}function Ln(e,t){try{var l=t.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&e)===e){a=void 0;var i=l.create,u=l.inst;a=i(),u.destroy=a}l=l.next}while(l!==n)}}catch(s){Ee(t,t.return,s)}}function Ql(e,t,l){try{var a=t.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var i=n.next;a=i;do{if((a.tag&e)===e){var u=a.inst,s=u.destroy;if(s!==void 0){u.destroy=void 0,n=t;var o=l,p=s;try{p()}catch(z){Ee(n,o,z)}}}a=a.next}while(a!==i)}}catch(z){Ee(t,t.return,z)}}function br(e){var t=e.updateQueue;if(t!==null){var l=e.stateNode;try{so(t,l)}catch(a){Ee(e,e.return,a)}}}function Sr(e,t,l){l.props=Sa(e.type,e.memoizedProps),l.state=e.memoizedState;try{l.componentWillUnmount()}catch(a){Ee(e,t,a)}}function Yn(e,t){try{var l=e.ref;if(l!==null){switch(e.tag){case 26:case 27:case 5:var a=e.stateNode;break;case 30:a=e.stateNode;break;default:a=e.stateNode}typeof l=="function"?e.refCleanup=l(a):l.current=a}}catch(n){Ee(e,t,n)}}function ol(e,t){var l=e.ref,a=e.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){Ee(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){Ee(e,t,n)}else l.current=null}function xr(e){var t=e.type,l=e.memoizedProps,a=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break e;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){Ee(e,e.return,n)}}function Jc(e,t,l){try{var a=e.stateNode;Wh(a,e.type,l,t),a[Dt]=t}catch(n){Ee(e,e.return,n)}}function _r(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Wl(e.type)||e.tag===4}function $c(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||_r(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Wl(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Wc(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(e,t):(t=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,t.appendChild(e),l=l._reactRootContainer,l!=null||t.onclick!==null||(t.onclick=hl));else if(a!==4&&(a===27&&Wl(e.type)&&(l=e.stateNode,t=null),e=e.child,e!==null))for(Wc(e,t,l),e=e.sibling;e!==null;)Wc(e,t,l),e=e.sibling}function Zi(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?l.insertBefore(e,t):l.appendChild(e);else if(a!==4&&(a===27&&Wl(e.type)&&(l=e.stateNode),e=e.child,e!==null))for(Zi(e,t,l),e=e.sibling;e!==null;)Zi(e,t,l),e=e.sibling}function jr(e){var t=e.stateNode,l=e.memoizedProps;try{for(var a=e.type,n=t.attributes;n.length;)t.removeAttributeNode(n[0]);jt(t,a,l),t[bt]=e,t[Dt]=l}catch(i){Ee(e,e.return,i)}}var zl=!1,ut=!1,Fc=!1,zr=typeof WeakSet=="function"?WeakSet:Set,ht=null;function Ah(e,t){if(e=e.containerInfo,vs=mu,e=Rf(e),ku(e)){if("selectionStart"in e)var l={start:e.selectionStart,end:e.selectionEnd};else e:{l=(l=e.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,i=a.focusNode;a=a.focusOffset;try{l.nodeType,i.nodeType}catch{l=null;break e}var u=0,s=-1,o=-1,p=0,z=0,M=e,v=null;t:for(;;){for(var S;M!==l||n!==0&&M.nodeType!==3||(s=u+n),M!==i||a!==0&&M.nodeType!==3||(o=u+a),M.nodeType===3&&(u+=M.nodeValue.length),(S=M.firstChild)!==null;)v=M,M=S;for(;;){if(M===e)break t;if(v===l&&++p===n&&(s=u),v===i&&++z===a&&(o=u),(S=M.nextSibling)!==null)break;M=v,v=M.parentNode}M=S}l=s===-1||o===-1?null:{start:s,end:o}}else l=null}l=l||{start:0,end:0}}else l=null;for(bs={focusedElem:e,selectionRange:l},mu=!1,ht=t;ht!==null;)if(t=ht,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,ht=e;else for(;ht!==null;){switch(t=ht,i=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(l=0;l title"))),jt(i,a,l),i[bt]=e,mt(i),a=i;break e;case"link":var u=Dd("link","href",n).get(a+(l.href||""));if(u){for(var s=0;sUe&&(u=Ue,Ue=F,F=u);var y=Uf(s,F),m=Uf(s,Ue);if(y&&m&&(S.rangeCount!==1||S.anchorNode!==y.node||S.anchorOffset!==y.offset||S.focusNode!==m.node||S.focusOffset!==m.offset)){var g=M.createRange();g.setStart(y.node,y.offset),S.removeAllRanges(),F>Ue?(S.addRange(g),S.extend(m.node,m.offset)):(g.setEnd(m.node,m.offset),S.addRange(g))}}}}for(M=[],S=s;S=S.parentNode;)S.nodeType===1&&M.push({element:S,left:S.scrollLeft,top:S.scrollTop});for(typeof s.focus=="function"&&s.focus(),s=0;sl?32:l,_.T=null,l=ns,ns=null;var i=Kl,u=Al;if(ft=0,ln=Kl=null,Al=0,(Te&6)!==0)throw Error(r(331));var s=Te;if(Te|=4,Rr(i.current),Or(i,i.current,u,l),Te=s,Kn(0,!1),Pe&&typeof Pe.onPostCommitFiberRoot=="function")try{Pe.onPostCommitFiberRoot(st,i)}catch{}return!0}finally{C.p=n,_.T=a,Pr(e,t)}}function td(e,t,l){t=Wt(l,t),t=Hc(e.stateNode,t,2),e=Ll(e,t,2),e!==null&&(mn(e,2),rl(e))}function Ee(e,t,l){if(e.tag===3)td(e,e,l);else for(;t!==null;){if(t.tag===3){td(t,e,l);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Zl===null||!Zl.has(a))){e=Wt(l,e),l=lr(2),a=Ll(t,l,2),a!==null&&(ar(l,a,t,e),mn(a,2),rl(a));break}}t=t.return}}function ss(e,t,l){var a=e.pingCache;if(a===null){a=e.pingCache=new Oh;var n=new Set;a.set(t,n)}else n=a.get(t),n===void 0&&(n=new Set,a.set(t,n));n.has(l)||(es=!0,n.add(l),e=Hh.bind(null,e,t,l),t.then(e,e))}function Hh(e,t,l){var a=e.pingCache;a!==null&&a.delete(t),e.pingedLanes|=e.suspendedLanes&l,e.warmLanes&=~l,He===e&&(ye&l)===l&&(Ie===4||Ie===3&&(ye&62914560)===ye&&300>we()-$i?(Te&2)===0&&an(e,0):ts|=l,tn===ye&&(tn=0)),rl(e)}function ld(e,t){t===0&&(t=ci()),e=oa(e,t),e!==null&&(mn(e,t),rl(e))}function Gh(e){var t=e.memoizedState,l=0;t!==null&&(l=t.retryLane),ld(e,l)}function qh(e,t){var l=0;switch(e.tag){case 31:case 13:var a=e.stateNode,n=e.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=e.stateNode;break;case 22:a=e.stateNode._retryCache;break;default:throw Error(r(314))}a!==null&&a.delete(t),ld(e,l)}function Lh(e,t){return ae(e,t)}var lu=null,un=null,fs=!1,au=!1,os=!1,$l=0;function rl(e){e!==un&&e.next===null&&(un===null?lu=un=e:un=un.next=e),au=!0,fs||(fs=!0,Xh())}function Kn(e,t){if(!os&&au){os=!0;do for(var l=!1,a=lu;a!==null;){if(e!==0){var n=a.pendingLanes;if(n===0)var i=0;else{var u=a.suspendedLanes,s=a.pingedLanes;i=(1<<31-rt(42|e)+1)-1,i&=n&~(u&~s),i=i&201326741?i&201326741|1:i?i|2:0}i!==0&&(l=!0,ud(a,i))}else i=ye,i=dt(a,a===He?i:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(i&3)===0||vt(a,i)||(l=!0,ud(a,i));a=a.next}while(l);os=!1}}function Yh(){ad()}function ad(){au=fs=!1;var e=0;$l!==0&&Ih()&&(e=$l);for(var t=we(),l=null,a=lu;a!==null;){var n=a.next,i=nd(a,t);i===0?(a.next=null,l===null?lu=n:l.next=n,n===null&&(un=l)):(l=a,(e!==0||(i&3)!==0)&&(au=!0)),a=n}ft!==0&&ft!==5||Kn(e),$l!==0&&($l=0)}function nd(e,t){for(var l=e.suspendedLanes,a=e.pingedLanes,n=e.expirationTimes,i=e.pendingLanes&-62914561;0s)break;var z=o.transferSize,M=o.initiatorType;z&&hd(M)&&(o=o.responseEnd,u+=z*(o"u"?null:document;function Md(e,t,l){var a=cn;if(a&&typeof t=="string"&&t){var n=Jt(t);n='link[rel="'+e+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),Td.has(n)||(Td.add(n),e={rel:e,crossOrigin:l,href:t},a.querySelector(n)===null&&(t=a.createElement("link"),jt(t,"link",e),mt(t),a.head.appendChild(t)))}}function cy(e){Dl.D(e),Md("dns-prefetch",e,null)}function sy(e,t){Dl.C(e,t),Md("preconnect",e,t)}function fy(e,t,l){Dl.L(e,t,l);var a=cn;if(a&&e&&t){var n='link[rel="preload"][as="'+Jt(t)+'"]';t==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+Jt(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+Jt(l.imageSizes)+'"]')):n+='[href="'+Jt(e)+'"]';var i=n;switch(t){case"style":i=sn(e);break;case"script":i=fn(e)}ll.has(i)||(e=O({rel:"preload",href:t==="image"&&l&&l.imageSrcSet?void 0:e,as:t},l),ll.set(i,e),a.querySelector(n)!==null||t==="style"&&a.querySelector(Fn(i))||t==="script"&&a.querySelector(In(i))||(t=a.createElement("link"),jt(t,"link",e),mt(t),a.head.appendChild(t)))}}function oy(e,t){Dl.m(e,t);var l=cn;if(l&&e){var a=t&&typeof t.as=="string"?t.as:"script",n='link[rel="modulepreload"][as="'+Jt(a)+'"][href="'+Jt(e)+'"]',i=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":i=fn(e)}if(!ll.has(i)&&(e=O({rel:"modulepreload",href:e},t),ll.set(i,e),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(In(i)))return}a=l.createElement("link"),jt(a,"link",e),mt(a),l.head.appendChild(a)}}}function ry(e,t,l){Dl.S(e,t,l);var a=cn;if(a&&e){var n=Aa(a).hoistableStyles,i=sn(e);t=t||"default";var u=n.get(i);if(!u){var s={loading:0,preload:null};if(u=a.querySelector(Fn(i)))s.loading=5;else{e=O({rel:"stylesheet",href:e,"data-precedence":t},l),(l=ll.get(i))&&Ms(e,l);var o=u=a.createElement("link");mt(o),jt(o,"link",e),o._p=new Promise(function(p,z){o.onload=p,o.onerror=z}),o.addEventListener("load",function(){s.loading|=1}),o.addEventListener("error",function(){s.loading|=2}),s.loading|=4,su(u,t,a)}u={type:"stylesheet",instance:u,count:1,state:s},n.set(i,u)}}}function dy(e,t){Dl.X(e,t);var l=cn;if(l&&e){var a=Aa(l).hoistableScripts,n=fn(e),i=a.get(n);i||(i=l.querySelector(In(n)),i||(e=O({src:e,async:!0},t),(t=ll.get(n))&&Ns(e,t),i=l.createElement("script"),mt(i),jt(i,"link",e),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function my(e,t){Dl.M(e,t);var l=cn;if(l&&e){var a=Aa(l).hoistableScripts,n=fn(e),i=a.get(n);i||(i=l.querySelector(In(n)),i||(e=O({src:e,async:!0,type:"module"},t),(t=ll.get(n))&&Ns(e,t),i=l.createElement("script"),mt(i),jt(i,"link",e),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function Nd(e,t,l,a){var n=(n=I.current)?cu(n):null;if(!n)throw Error(r(446));switch(e){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(t=sn(l.href),l=Aa(n).hoistableStyles,a=l.get(t),a||(a={type:"style",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){e=sn(l.href);var i=Aa(n).hoistableStyles,u=i.get(e);if(u||(n=n.ownerDocument||n,u={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},i.set(e,u),(i=n.querySelector(Fn(e)))&&!i._p&&(u.instance=i,u.state.loading=5),ll.has(e)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},ll.set(e,l),i||hy(n,e,l,u.state))),t&&a===null)throw Error(r(528,""));return u}if(t&&a!==null)throw Error(r(529,""));return null;case"script":return t=l.async,l=l.src,typeof l=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=fn(l),l=Aa(n).hoistableScripts,a=l.get(t),a||(a={type:"script",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,e))}}function sn(e){return'href="'+Jt(e)+'"'}function Fn(e){return'link[rel="stylesheet"]['+e+"]"}function Ed(e){return O({},e,{"data-precedence":e.precedence,precedence:null})}function hy(e,t,l,a){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=e.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),jt(t,"link",l),mt(t),e.head.appendChild(t))}function fn(e){return'[src="'+Jt(e)+'"]'}function In(e){return"script[async]"+e}function Ad(e,t,l){if(t.count++,t.instance===null)switch(t.type){case"style":var a=e.querySelector('style[data-href~="'+Jt(l.href)+'"]');if(a)return t.instance=a,mt(a),a;var n=O({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(e.ownerDocument||e).createElement("style"),mt(a),jt(a,"style",n),su(a,l.precedence,e),t.instance=a;case"stylesheet":n=sn(l.href);var i=e.querySelector(Fn(n));if(i)return t.state.loading|=4,t.instance=i,mt(i),i;a=Ed(l),(n=ll.get(n))&&Ms(a,n),i=(e.ownerDocument||e).createElement("link"),mt(i);var u=i;return u._p=new Promise(function(s,o){u.onload=s,u.onerror=o}),jt(i,"link",a),t.state.loading|=4,su(i,l.precedence,e),t.instance=i;case"script":return i=fn(l.src),(n=e.querySelector(In(i)))?(t.instance=n,mt(n),n):(a=l,(n=ll.get(i))&&(a=O({},l),Ns(a,n)),e=e.ownerDocument||e,n=e.createElement("script"),mt(n),jt(n,"link",a),e.head.appendChild(n),t.instance=n);case"void":return null;default:throw Error(r(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,su(a,l.precedence,e));return t.instance}function su(e,t,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,i=n,u=0;u title"):null)}function yy(e,t,l){if(l===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(e=t.disabled,typeof t.precedence=="string"&&e==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Od(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function gy(e,t,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=sn(a.href),i=t.querySelector(Fn(n));if(i){t=i._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=ou.bind(e),t.then(e,e)),l.state.loading|=4,l.instance=i,mt(i);return}i=t.ownerDocument||t,a=Ed(a),(n=ll.get(n))&&Ms(a,n),i=i.createElement("link"),mt(i);var u=i;u._p=new Promise(function(s,o){u.onload=s,u.onerror=o}),jt(i,"link",a),l.instance=i}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(l,t),(t=l.state.preload)&&(l.state.loading&3)===0&&(e.count++,l=ou.bind(e),t.addEventListener("load",l),t.addEventListener("error",l))}}var Es=0;function py(e,t){return e.stylesheets&&e.count===0&&du(e,e.stylesheets),0Es?50:800)+t);return e.unsuspend=l,function(){e.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function ou(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)du(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var ru=null;function du(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ru=new Map,t.forEach(vy,e),ru=null,ou.call(e))}function vy(e,t){if(!(t.state.loading&4)){var l=ru.get(e);if(l)var a=l.get(null);else{l=new Map,ru.set(e,l);for(var n=e.querySelectorAll("link[data-precedence],style[data-precedence]"),i=0;i"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(f)}catch(b){console.error(b)}}return f(),Hs.exports=Ry(),Hs.exports}var Hy=By();const Gy=fm(Hy);const om=(...f)=>f.filter((b,N,r)=>!!b&&b.trim()!==""&&r.indexOf(b)===N).join(" ").trim();const qy=f=>f.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();const Ly=f=>f.replace(/^([A-Z])|[\s-_]+(\w)/g,(b,N,r)=>r?r.toUpperCase():N.toLowerCase());const lm=f=>{const b=Ly(f);return b.charAt(0).toUpperCase()+b.slice(1)};var Ys={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const Yy=f=>{for(const b in f)if(b.startsWith("aria-")||b==="role"||b==="title")return!0;return!1},Xy=U.createContext({}),Qy=()=>U.useContext(Xy),ky=U.forwardRef(({color:f,size:b,strokeWidth:N,absoluteStrokeWidth:r,className:x="",children:E,iconNode:q,...H},w)=>{const{size:h=24,strokeWidth:A=2,absoluteStrokeWidth:O=!1,color:Q="currentColor",className:P=""}=Qy()??{},J=r??O?Number(N??A)*24/Number(b??h):N??A;return U.createElement("svg",{ref:w,...Ys,width:b??h??Ys.width,height:b??h??Ys.height,stroke:f??Q,strokeWidth:J,className:om("lucide",P,x),...!E&&!Yy(H)&&{"aria-hidden":"true"},...H},[...q.map(([$,fe])=>U.createElement($,fe)),...Array.isArray(E)?E:[E]])});const Xe=(f,b)=>{const N=U.forwardRef(({className:r,...x},E)=>U.createElement(ky,{ref:E,iconNode:b,className:om(`lucide-${qy(lm(f))}`,`lucide-${f}`,r),...x}));return N.displayName=lm(f),N};const Vy=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Xs=Xe("check",Vy);const Zy=[["path",{d:"M12 13v8l-4-4",key:"1f5nwf"}],["path",{d:"m12 21 4-4",key:"1lfcce"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284",key:"ui1hmy"}]],am=Xe("cloud-download",Zy);const Ky=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],Jy=Xe("copy",Ky);const $y=[["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M17 20v2",key:"1rnc9c"}],["path",{d:"M17 2v2",key:"11trls"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M2 17h2",key:"7oei6x"}],["path",{d:"M2 7h2",key:"asdhe0"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"M20 17h2",key:"1fpfkl"}],["path",{d:"M20 7h2",key:"1o8tra"}],["path",{d:"M7 20v2",key:"4gnj0m"}],["path",{d:"M7 2v2",key:"1i4yhu"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",key:"1vbyd7"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1",key:"z9xiuo"}]],rm=Xe("cpu",$y);const Wy=[["path",{d:"M6 2v14a2 2 0 0 0 2 2h14",key:"ron5a4"}],["path",{d:"M18 22V8a2 2 0 0 0-2-2H2",key:"7s9ehn"}]],Fy=Xe("crop",Wy);const Iy=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]],Qs=Xe("database",Iy);const Py=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],nm=Xe("download",Py);const e0=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"M12 10v6",key:"1bos4e"}],["path",{d:"m15 13-3 3-3-3",key:"6j2sf0"}]],t0=Xe("folder-down",e0);const l0=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]],im=Xe("folder-open",l0);const a0=[["path",{d:"M10 16h.01",key:"1bzywj"}],["path",{d:"M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"18tbho"}],["path",{d:"M21.946 12.013H2.054",key:"zqlbp7"}],["path",{d:"M6 16h.01",key:"1pmjb7"}]],xu=Xe("hard-drive",a0);const n0=[["path",{d:"M16 5h6",key:"1vod17"}],["path",{d:"M19 2v6",key:"4bpg5p"}],["path",{d:"M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5",key:"1ue2ih"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}]],i0=Xe("image-plus",n0);const u0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]],c0=Xe("image",u0);const s0=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],f0=Xe("info",s0);const o0=[["path",{d:"m16 6 4 14",key:"ji33uf"}],["path",{d:"M12 6v14",key:"1n7gus"}],["path",{d:"M8 8v12",key:"1gg7y9"}],["path",{d:"M4 4v16",key:"6qkkli"}]],r0=Xe("library",o0);const d0=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]],m0=Xe("moon",d0);const h0=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],aa=Xe("refresh-cw",h0);const y0=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],g0=Xe("shield",y0);const p0=[["path",{d:"M10 8h4",key:"1sr2af"}],["path",{d:"M12 21v-9",key:"17s77i"}],["path",{d:"M12 8V3",key:"13r4qs"}],["path",{d:"M17 16h4",key:"h1uq16"}],["path",{d:"M19 12V3",key:"o1uvq1"}],["path",{d:"M19 21v-5",key:"qua636"}],["path",{d:"M3 14h4",key:"bcjad9"}],["path",{d:"M5 10V3",key:"cb8scm"}],["path",{d:"M5 21v-7",key:"1w1uti"}]],Ks=Xe("sliders-vertical",p0);const v0=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",key:"1s2grr"}],["path",{d:"M20 2v4",key:"1rf3ol"}],["path",{d:"M22 4h-4",key:"gwowj6"}],["circle",{cx:"4",cy:"20",r:"2",key:"6kqj1y"}]],Su=Xe("sparkles",v0);const b0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]],ks=Xe("square",b0);const S0=[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]],x0=Xe("sun",S0);const _0=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],j0=Xe("terminal",_0);const z0=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],rn=Xe("trash-2",z0);const T0=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],M0=Xe("triangle-alert",T0);function N0({activeTab:f,setActiveTab:b,specs:N}){return c.jsxs("div",{className:"sidebar",children:[c.jsxs("div",{children:[c.jsxs("div",{className:"sidebar-logo",children:[c.jsx(g0,{className:"sidebar-logo-icon"}),c.jsx("span",{className:"sidebar-logo-text",children:"Local AI Studio"})]}),c.jsxs("div",{className:"nav-list",children:[c.jsxs("div",{className:`nav-item ${f==="generator"?"active":""}`,onClick:()=>b("generator"),children:[c.jsx(c0,{size:20}),c.jsx("span",{children:"Image Generator"})]}),c.jsxs("div",{className:`nav-item ${f==="models"?"active":""}`,onClick:()=>b("models"),children:[c.jsx(t0,{size:20}),c.jsx("span",{children:"Model Manager"})]}),c.jsxs("div",{className:`nav-item ${f==="constraints"?"active":""}`,onClick:()=>b("constraints"),children:[c.jsx(Ks,{size:20}),c.jsx("span",{children:"Image Constraints"})]})]})]}),c.jsxs("div",{className:"sidebar-footer",children:[c.jsxs("div",{className:"sidebar-specs-header",children:[c.jsx(j0,{size:12}),c.jsx("span",{children:"Host Specifications"})]}),c.jsxs("div",{className:"sidebar-specs-item",title:N.cpu_name,children:["CPU: ",N.cpu_name]}),c.jsxs("div",{className:"sidebar-specs-item",title:N.gpu_name,children:["GPU: ",N.gpu_name]}),c.jsxs("div",{className:"sidebar-specs-item",children:["Memory: ",N.ram_total_gb.toFixed(0)," GB RAM (",N.cpu_cores_physical," Cores)"]}),c.jsxs("div",{className:"sidebar-specs-os",children:["OS: ",N.os_name]})]})]})}const E0=U.memo(N0);function A0({telemetry:f,serverRunning:b,activeModel:N,onStopServer:r,isStoppingServer:x=!1,theme:E,setTheme:q}){const H=A=>{const O=Number(A);return Number.isFinite(O)&&O>0?O.toFixed(O>=10?0:1):"--"},w=()=>b?"Server Active":"Local Mode",h=()=>b?"status-indicator busy":"status-indicator offline";return c.jsxs("div",{className:"top-status-bar",children:[c.jsxs("div",{className:"current-model-info",children:[c.jsx("div",{className:h()}),c.jsx("span",{style:{fontWeight:600,fontSize:"0.95rem"},children:w()}),N&&c.jsxs(c.Fragment,{children:[c.jsx("span",{style:{color:"var(--md-sys-color-outline-variant)"},children:"|"}),c.jsx("span",{style:{color:"var(--md-sys-color-primary)",fontWeight:700},children:N})]})]}),c.jsxs("div",{className:"telemetry-group",children:[c.jsx("button",{className:"theme-toggle-btn",onClick:()=>q(E==="dark"?"light":"dark"),title:`Switch to ${E==="dark"?"light":"dark"} theme`,children:E==="dark"?c.jsx(x0,{size:18}):c.jsx(m0,{size:18})}),b&&c.jsxs("button",{className:"m3-btn m3-btn-error",style:{height:"34px",padding:"0 14px"},onClick:r,disabled:x,title:"Stop local model server",children:[x?c.jsx(aa,{className:"progress-spinner",size:14}):c.jsx(ks,{size:14}),c.jsx("span",{children:x?"Stopping":"Stop Server"})]}),c.jsxs("div",{className:"telemetry-chip",title:"CPU Utilization",children:[c.jsx(rm,{className:"telemetry-chip-icon",style:{color:"var(--md-sys-color-primary)"}}),c.jsxs("span",{children:["CPU: ",Number.isFinite(Number(f.cpu_usage))?f.cpu_usage:"--","%"]})]}),c.jsxs("div",{className:"telemetry-chip",title:"System Memory Usage",children:[c.jsx(xu,{className:"telemetry-chip-icon",style:{color:"var(--md-sys-color-secondary)"}}),c.jsxs("span",{children:["RAM: ",H(f.ram_used_gb)," / ",H(f.ram_total_gb)," GB"]})]}),f.vram_total_gb>0&&c.jsxs("div",{className:"telemetry-chip",title:`${f.gpu_name} VRAM`,children:[c.jsx(Qs,{className:"telemetry-chip-icon",style:{color:"var(--md-sys-color-tertiary)"}}),c.jsxs("span",{children:["VRAM: ",H(f.vram_used_gb)," / ",H(f.vram_total_gb)," GB"]})]})]})]})}const D0=U.memo(A0),C0="modulepreload",O0=function(f){return"/"+f},um={},U0=function(b,N,r){let x=Promise.resolve();if(N&&N.length>0){let w=function(h){return Promise.all(h.map(A=>Promise.resolve(A).then(O=>({status:"fulfilled",value:O}),O=>({status:"rejected",reason:O}))))};document.getElementsByTagName("link");const q=document.querySelector("meta[property=csp-nonce]"),H=q?.nonce||q?.getAttribute("nonce");x=w(N.map(h=>{if(h=O0(h),h in um)return;um[h]=!0;const A=h.endsWith(".css"),O=A?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${h}"]${O}`))return;const Q=document.createElement("link");if(Q.rel=A?"stylesheet":C0,A||(Q.as="script"),Q.crossOrigin="",Q.href=h,H&&Q.setAttribute("nonce",H),document.head.appendChild(Q),A)return new Promise((P,J)=>{Q.addEventListener("load",P),Q.addEventListener("error",()=>J(new Error(`Unable to preload CSS for ${h}`)))})}))}function E(q){const H=new Event("vite:preloadError",{cancelable:!0});if(H.payload=q,window.dispatchEvent(H),!H.defaultPrevented)throw q}return x.then(q=>{for(const H of q||[])H.status==="rejected"&&E(H.reason);return b().catch(E)})};function rg(f,b=!1){return window.__TAURI_INTERNALS__.transformCallback(f,b)}async function ja(f,b={},N){return window.__TAURI_INTERNALS__.invoke(f,b,N)}const ia=()=>typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0;let la=null,_u=null;const w0="polish-setup-v1",na=()=>typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1");async function dm(){if(na())try{const f=await Cl(),b=Number(f?.port);if(Number.isInteger(b)&&b>0)return _u=b,b}catch{}return _u||8080}async function mm(){return`http://127.0.0.1:${await dm()}`}function ii(f){const b=Number(f)||0;if(b<=0)return"0 B";const N=["B","KB","MB","GB","TB"],r=Math.min(N.length-1,Math.floor(Math.log(b)/Math.log(1024)));return`${(b/1024**r).toFixed(r===0?0:1)} ${N[r]}`}function ui(f){return typeof f=="string"?{filename:f,sizeBytes:0,size:"Unknown"}:{filename:f?.filename||f?.name||"",sizeBytes:Number(f?.sizeBytes||0),size:f?.size||(f?.sizeBytes?ii(f.sizeBytes):"Unknown")}}async function dn(f,b="The local server returned an invalid response."){const N=await f.text();let r={};try{r=JSON.parse(N||"{}")}catch{const E=N.trim().startsWith("0&&(_u=Number(q.port)),console.log("Backend restart:",q.message),q.message}catch(x){if(console.warn("Could not reach management API:",x.message),na())throw x;return"Backend management unavailable"}}async function Js(){if(ia())return await ja("stop_server");try{await fetch("/api/stop-backend",{method:"POST"})}catch{}return"Backend stopped"}async function Cl(){try{return await(await fetch("/api/backend-status")).json()}catch{return{ready:!1,settings:{}}}}async function Q0(){try{return(await(await fetch("/api/outputs")).json()).outputs||[]}catch{return[]}}async function k0(f,b){const N=await fetch("/api/save-output",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({image:f,metadata:b})});return(await dn(N,"The local server returned an invalid save response.")).output}async function V0(f){const b=await fetch("/api/delete-outputs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({outputs:f})});return await dn(b,"The local server returned an invalid delete response.")}async function Z0(){try{return((await(await fetch("/api/models")).json()).models||[]).map(ui)}catch{return[]}}async function K0(f,b,N,r,x,E,q){console.log("Initiating image generation:",{prompt:f,negativePrompt:b,constraints:N,activeModelName:r});const H=Date.now(),w=await mm(),h={prompt:f,negative_prompt:b||"",width:N.width||512,height:N.height||512,steps:N.steps||20,cfg_scale:N.cfgScale||7,seed:N.seed===-1?Math.floor(Math.random()*1e6):N.seed,sampler:N.sampler||"euler_a",image:x||null,denoising_strength:N.denoisingStrength||.7},A=!!h.image;let O=`${w}/v1/images/generations`,Q={prompt:h.prompt,negative_prompt:h.negative_prompt||"",n:1,size:`${h.width}x${h.height}`,response_format:"b64_json",steps:h.steps,cfg_scale:h.cfg_scale,seed:h.seed,sample_method:h.sampler};if(A){const P=String(h.image).replace(/^data:[^;]+;base64,/,"");O=`${w}/sdapi/v1/img2img`,Q={init_images:[P],prompt:h.prompt,negative_prompt:h.negative_prompt||"",denoising_strength:h.denoising_strength,steps:h.steps,cfg_scale:h.cfg_scale,seed:h.seed,width:h.width,height:h.height,sampler_name:h.sampler,sample_method:h.sampler,batch_size:1,n_iter:1,send_images:!0,save_images:!1}}try{const P=await fetch(O,{method:"POST",headers:{"Content-Type":"application/json"},signal:q,body:JSON.stringify(Q)});if(P.ok){const J=await P.json(),$=J?.data?.[0]?.b64_json??J?.images?.[0];if($){const fe=parseFloat(((Date.now()-H)/1e3).toFixed(1));return{image:`data:image/png;base64,${$}`,seed:J.data?.[0]?.seed??h.seed,duration_sec:fe}}}else{let J=`Generation failed (HTTP ${P.status})`;try{const $=await P.json();($?.detail||$?.error?.message)&&(J=$.detail||$.error.message)}catch{}throw new Error(J)}}catch(P){throw P.name==="AbortError"||P.message.startsWith("Generation failed")?P:(console.warn("Could not reach local server.",P),new Error("The image generation server is not responding or crashed. Try restarting the backend from Model Manager, or check the terminal for a backend error."))}}async function J0(f,b,N){if(f instanceof File){const H=f;if(!na())throw new Error("File import requires the local image generator server.");return await $0(H,b,N),{response:"Model imported successfully"}}if(ia()){const{listen:H}=await U0(async()=>{const{listen:h}=await import("./event-Df1n27Ua.js");return{listen:h}},[]);let w=null;b&&(w=await H("import-progress",h=>{b(h.payload)}));try{return{response:await ja("import_model_file",{sourcePath:f}),unlisten:w}}catch(h){throw w&&w(),h}}const r=f.split(/[\\/]/).pop()||"imported_model.gguf";console.log(`Web Mode: Simulating copying ${r} to USB models folder`);const x=40;for(let H=1;H<=x;H++){if(N?.aborted)throw new DOMException("The user aborted a request.","AbortError");await new Promise(O=>setTimeout(O,150));const w=H/x*100,h=40+Math.sin(H)*5+Math.random()*2,A=(x-H)*.15;b&&b({filename:r,progress:w,speed_mb_s:h,eta_secs:A,status:"Copying to USB..."})}const E=localStorage.getItem("imported-models"),q=E?JSON.parse(E):[];return q.includes(r)||(q.push({filename:r,sizeBytes:0,size:"Unknown"}),localStorage.setItem("imported-models",JSON.stringify(q))),{response:"Model imported successfully"}}function $0(f,b,N){return new Promise((r,x)=>{const E=new XMLHttpRequest,q=Date.now();let H=!1;E.open("POST",`/api/import-model?filename=${encodeURIComponent(f.name)}`),E.setRequestHeader("Content-Type","application/octet-stream");const w=()=>{H=!0,E.abort()};N&&(N.aborted?w():N.addEventListener("abort",w,{once:!0})),E.upload.onprogress=h=>{if(!h.lengthComputable||!b)return;const A=Math.max(.1,(Date.now()-q)/1e3),O=h.loaded/A,Q=Math.max(0,h.total-h.loaded);b({filename:f.name,progress:h.loaded/h.total*100,speed_mb_s:O/(1024*1024),eta_secs:Q/Math.max(1,O),status:"Copying to models folder..."})},E.onload=()=>{let h={};try{h=JSON.parse(E.responseText||"{}")}catch{}E.status>=200&&E.status<300&&h.ok!==!1?(b&&b({filename:f.name,progress:100,speed_mb_s:0,eta_secs:0,status:"Import complete"}),r(h)):x(new Error(h.error||`Import failed (HTTP ${E.status})`))},E.onerror=()=>x(new Error("Import failed while uploading the file.")),E.onabort=()=>x(new DOMException(H?"Import cancelled by user.":"Import aborted.","AbortError")),E.send(f)})}async function W0(f){if(ia())return await ja("delete_model_file",{filename:f});if(typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"))try{const x=await fetch("/api/delete-model",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:f})}),E=await x.json();if(!x.ok)throw new Error(E.error||"Failed to delete model file");return E}catch(x){throw console.error("Failed to delete model via local API:",x),x}const N=localStorage.getItem("imported-models");let r=N?JSON.parse(N):[];return r=r.filter(x=>ui(x).filename!==f),localStorage.setItem("imported-models",JSON.stringify(r)),`Simulated deletion of ${f} from browser session`}async function hm(){const f=await mm();try{return(await fetch(`${f}/v1/models`,{method:"GET"})).ok}catch{return!1}}async function F0(f=30){const b=ia()||na()?f:3;for(let N=0;NsetTimeout(x,500))}return!1}async function I0(f){try{return await(await fetch("/api/download-model",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:f})})).json()}catch(b){return console.error("Failed to start model download:",b),{ok:!1,error:b.message}}}async function P0(){try{return await(await fetch("/api/cancel-download",{method:"POST"})).json()}catch(f){return console.error("Failed to cancel model download:",f),{ok:!1,error:f.message}}}async function cm(){try{return await(await fetch("/api/download-progress")).json()}catch(f){return console.error("Failed to get download progress:",f),{active:!1,error:f.message}}}async function eg(){try{return await(await fetch("/api/generation-progress")).json()}catch(f){return console.error("Failed to get generation progress:",f),{active:!1,error:f.message}}}const tg=U.memo(({img:f,idx:b,isSelected:N,onClick:r})=>{const x=E=>{r(b,E)};return c.jsxs("div",{className:`gallery-item ${N?"selected":""}`,onClick:x,title:`Prompt: ${f.prompt} -Seed: ${f.seed}${f.duration_sec?` -Time: ${f.duration_sec.toFixed(1)}s`:""}`,children:[c.jsx("img",{src:f.url,className:"gallery-thumb",alt:"Thumbnail",loading:"lazy",decoding:"async"}),N&&c.jsx("div",{className:"gallery-select-badge",children:c.jsx(Xs,{size:14})})]})});function lg({prompt:f,setPrompt:b,negativePrompt:N,setNegativePrompt:r,constraints:x,setConstraints:E,activeModel:q,generatedImages:H,setGeneratedImages:w,isGenerating:h,setIsGenerating:A,generationProgress:O,setGenerationProgress:Q,setActiveTab:P,showAlert:J=async({message:fe})=>window.alert(fe),showConfirm:$=async({message:fe})=>window.confirm(fe)}){const[fe,te]=U.useState(null),[lt,Ae]=U.useState(null),[ke,Ve]=U.useState(null),[De,ee]=U.useState(!1),[Se,ot]=U.useState(0),[Mt,de]=U.useState(0),[Ze,ct]=U.useState(0),[Ke,Le]=U.useState(""),[_,C]=U.useState(!1),[Z,me]=U.useState(!1),[ce,d]=U.useState(!1),[j,R]=U.useState(null),[G,V]=U.useState(null),[I,se]=U.useState(!1),[Qe,Me]=U.useState([]),[yt,Nt]=U.useState(null),Je=U.useRef(null),Ht=U.useRef(null),zt=U.useRef(!1),k=L=>{const he=L.target.files?.[0];if(L.target.value="",!he)return;if(!he.type.startsWith("image/")){V("Please choose an image file (PNG, JPG, or WEBP).");return}const Re=new FileReader;Re.onload=()=>Nt(Re.result),Re.onerror=()=>V("Could not read the selected image file."),Re.readAsDataURL(he)},ie=()=>Nt(null),D=async()=>{if(!f.trim())return;V(null),d(!0),R(null);let L=!1;try{const ve=await Cl();if(ve&&ve.settings){const be=ve.settings,Be=be.model?be.model.split(/[\\/]/).pop():null,xe=q?q.split(/[\\/]/).pop():null;(Be!==xe||parseInt(be.steps)!==parseInt(x.steps)||Math.abs(parseFloat(be.cfgScale)-parseFloat(x.cfgScale))>.05||be.sampler!==x.sampler||parseInt(be.threads)!==parseInt(x.threads)||!!be.useGpu!=(x.useGpu!==!1)||(be.backendType||(be.useGpu===!1?"cpu":"auto"))!==(x.backendType||(x.useGpu===!1?"cpu":"auto")))&&(L=!0)}else L=!0}catch{L=!0}if(L)try{console.log("Settings out of sync, restarting backend..."),await Vs(q,x);let ve=!1,be=null;for(let Be=0;Be<240;Be++){const xe=await Cl();if(xe.loading&&R({progress:xe.loading.progress||0,phase:xe.loading.phase||"Loading model...",speed:xe.loading.speed||"",current:xe.loading.current||0,total:xe.loading.total||0,backendMode:xe.loading.backendMode||xe.settings?.backendMode||"",device:xe.loading.device||xe.settings?.backendDevice||""}),xe.ready){ve=!0;break}if(xe.error){be=xe.error;break}if(!xe.running&&Be>3){be="The backend process terminated immediately on startup.";break}await new Promise(We=>setTimeout(We,500))}if(!ve)throw new Error(be||"Backend server failed to start after applying configuration changes.")}catch(ve){console.error("Failed to restart backend:",ve),V(ve.message||"Failed to sync settings with backend."),d(!1);return}d(!1),R(null),Je.current&&clearInterval(Je.current),A(!0),Q(0),ot(0),ct(0),Le(""),zt.current=!1,C(!1),se(!1),Ve(null),te(null);const he=(x.width>=1024?140:35)*.04,Re=x.width>=1024?140:35,gt=(x.backendType||(x.useGpu===!1?"cpu":"auto"))!=="cpu";let st=gt?he:Re,Pe=!1,qt=0,rt=0,pt=0;de(Math.round(x.steps*st)),Je.current=setInterval(async()=>{pt++,ot(pt);try{const ve=await eg();if(ve&&ve.active){const be=ve.decoding||!1;se(be);const Be=be?ve.steps||x.steps||20:ve.step||0,xe=ve.steps||x.steps||20,We=be?"":ve.speed||"";(be||ve.steps>0)&&(zt.current=!0,ct(Be)),Le(be?"decoding":We);let _e=0;be?_e=99:xe>0&&(_e=Math.round(Be/xe*100)),_e=Math.min(99,_e),be||ve.steps>0?Q(_e):Q(vt=>Math.max(vt,_e));let dt=0;if(be)dt=3;else{const vt=parseFloat(We);if(!isNaN(vt)&&vt>0){const cl=Math.max(0,xe-Be);We.includes("it/s")?dt=cl/vt:We.includes("s/it")&&(dt=cl*vt)}else{const cl=gt?he:Re;dt=Math.max(0,(xe-Be)*cl)}}if(gt&&!Pe){const vt=We.includes("it/s")&&numericSpeed<.2,cl=We.includes("s/it")&&numericSpeed>5;(vt||cl)&&(Pe=!0,C(!0))}de(Math.round(dt))}else Q(be=>Math.min(15,be+1)),de(be=>Math.max(1,be-1))}catch(ve){console.warn("Failed to get generation progress, running simulation fallback:",ve),gt&&!Pe&&pt>x.steps*he*1.3&&(Pe=!0,C(!0),st=Re,qt=pt,rt=Math.min(95,Math.round(pt/(x.steps*he)*100)));let be=Math.min(x.steps-1,Math.floor(pt/st)),Be=x.steps*st;if(pt>=Be){const _e=Math.ceil(pt/Math.max(.5,be));st=Math.max(st+15,_e+15),Be=x.steps*st,be=Math.min(x.steps-1,Math.floor(pt/st))}const xe=Math.max(1,Be-pt);de(Math.round(xe));let We=0;if(Pe){const _e=99-rt,dt=Be-qt;if(dt>0){const vt=Math.min(1,(pt-qt)/dt);if(vt<.85)We=Math.round(rt+vt*_e);else{const cl=(pt-(qt+dt*.85))/(st*2),ci=1-Math.exp(-cl);We=Math.min(99,Math.round(rt+.85*_e+.14*_e*ci))}}else We=99}else if(ptMath.max(_e,We))}},1e3);try{Ht.current=new AbortController;const ve=await K0(f,N,x,q,yt,_e=>Q(dt=>Math.max(dt,_e)),Ht.current.signal);Je.current&&clearInterval(Je.current),Q(100),ct(x.steps),de(0),Le("");const be={prompt:f,negativePrompt:N,seed:ve.seed,steps:x.steps,cfgScale:x.cfgScale,width:x.width,height:x.height,sampler:x.sampler,model:q,mode:yt?"img2img":"txt2img",denoisingStrength:yt?x.denoisingStrength:null,duration_sec:ve.duration_sec,timestamp:new Date().toLocaleTimeString()};let Be=null,xe=null;try{if(Be=await k0(ve.image,be),xe=Be.url,!xe)throw new Error("The local server saved the image but did not return a file URL.")}catch(_e){throw console.error("Generated image could not be saved:",_e),new Error(`Image generated but could not be saved to USB: ${_e.message||_e}`)}te(xe),Ae(ve.seed),Ve(ve.duration_sec);const We={...be,...Be||{},url:xe};w(_e=>[We,..._e])}catch(ve){ve.name==="AbortError"?console.log("Generation request was aborted by the user"):(console.error("Generation failed:",ve),V(ve.message||"Generation failed. Please try again.")),Je.current&&clearInterval(Je.current)}finally{A(!1),Ht.current=null}},B=async()=>{if(await $({title:"Stop Generation?",message:"The current image generation will be cancelled and the local model server may restart.",confirmLabel:"Stop",danger:!0})){Je.current&&clearInterval(Je.current),me(!0),Ht.current&&Ht.current.abort();try{console.log("Cancelling: Stopping server to kill generation threads..."),await Js(),ct(0),Le(""),se(!1),zt.current=!1,q&&(console.log("Cancelling: Restarting server for future generations..."),await Vs(q,x),await F0()||console.warn("Server failed to restart properly."))}catch(L){console.error("Failed to restart server on cancellation:",L)}finally{me(!1),A(!1),Q(0),de(0)}}},W=async()=>{if(fe)try{if(fe.startsWith("data:image")){const he=await(await fetch(fe)).blob();await navigator.clipboard.write([new ClipboardItem({[he.type]:he})])}else{const L=new Image;L.crossOrigin="anonymous",L.src=fe,L.onload=async()=>{const he=document.createElement("canvas");he.width=L.width,he.height=L.height,he.getContext("2d").drawImage(L,0,0),he.toBlob(async gt=>{await navigator.clipboard.write([new ClipboardItem({[gt.type]:gt})])},"image/png")}}ee(!0),setTimeout(()=>ee(!1),2e3)}catch(L){console.error("Failed to copy image:",L)}},ae=()=>{if(!fe)return;const L=document.createElement("a");L.href=fe,L.download=`diffusion-${lt||Date.now()}.png`,document.body.appendChild(L),L.click(),document.body.removeChild(L)},At=(L,he=`diffusion-${Date.now()}.png`)=>{if(!L)return;const Re=document.createElement("a");Re.href=L,Re.download=he,document.body.appendChild(Re),Re.click(),document.body.removeChild(Re)},Gt=U.useCallback(L=>{b(L.prompt),r(L.negativePrompt),E(he=>({...he,steps:L.steps,cfgScale:L.cfgScale,width:L.width,height:L.height,sampler:L.sampler,seed:L.seed})),te(L.url),Ae(L.seed),Ve(L.duration_sec??L.durationSec??null)},[b,r,E]),dl=U.useCallback((L,he)=>{const Re=he.ctrlKey||he.metaKey||he.shiftKey;Me(gt=>Re?gt.includes(L)?gt.filter(st=>st!==L):[...gt,L]:[L]),H[L]&&Gt(H[L])},[H,Gt]),we=Qe.map(L=>H[L]).filter(Boolean),al=()=>{Me([])},$e=()=>{we[0]&&Gt(we[0])},za=()=>{we.forEach((L,he)=>{const Re=L.seed??Date.now();At(L.url,`diffusion-${Re}-${he+1}.png`)})},Ta=async()=>{if(we.length===0)return;const L=we.length;if(await $({title:"Delete Selected Outputs?",message:`Delete ${L} selected output${L===1?"":"s"} from USB? This removes the image and metadata files.`,confirmLabel:"Delete",danger:!0}))try{await V0(we);const Re=new Set(Qe);w(gt=>gt.filter((st,Pe)=>!Re.has(Pe))),Me([]),we.some(gt=>gt.url===fe)&&(te(null),Ae(null),Ve(null))}catch(Re){console.error("Failed to delete selected outputs:",Re),J({title:"Delete Failed",message:Re.message||String(Re),danger:!0})}};return c.jsxs("div",{className:"workspace-area",children:[c.jsxs("div",{className:"workspace-title-section",children:[c.jsx("h2",{className:"workspace-title",children:"Generator Dashboard"}),c.jsx("p",{className:"workspace-subtitle",children:"Create high-fidelity images completely locally on your external drive."})]}),c.jsxs("div",{className:"generator-layout",children:[c.jsx("div",{children:c.jsxs("div",{className:"m3-card",children:[c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(Su,{size:18,style:{color:"var(--md-sys-color-primary)"}}),"Generation Workspace"]}),c.jsxs("div",{className:"m3-field-group",children:[c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Positive Prompt"}),c.jsx("textarea",{className:"m3-textarea",value:f,onChange:L=>b(L.target.value),placeholder:"Enter a descriptive prompt (e.g. 'A futuristic city in cyberpunk aesthetic, high details, cinematic lighting')...",disabled:h})]}),c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Negative Prompt (Optional)"}),c.jsx("input",{type:"text",className:"m3-input",value:N,onChange:L=>r(L.target.value),placeholder:"Items to avoid (e.g. 'blurry, low quality, deformed hands, texts')...",disabled:h})]}),c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Base Image (Optional)"}),yt?c.jsxs("div",{style:{display:"flex",gap:"12px",alignItems:"center",padding:"12px",background:"var(--md-sys-color-surface-variant)",borderRadius:"var(--md-shape-corner-medium)",border:"1px solid var(--md-sys-color-outline-variant)"},children:[c.jsx("img",{src:yt,alt:"Base",style:{width:"64px",height:"64px",objectFit:"cover",borderRadius:"8px",flexShrink:0}}),c.jsxs("div",{style:{flex:1,minWidth:0},children:[c.jsx("div",{style:{fontWeight:600,fontSize:"0.85rem"},children:"Base image ready"}),c.jsx("div",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)"},children:"The AI will redraw this image guided by your prompt."})]}),c.jsxs("button",{type:"button",className:"m3-btn m3-btn-error",style:{height:"34px",flexShrink:0},onClick:ie,disabled:h,children:[c.jsx(rn,{size:14}),c.jsx("span",{children:"Remove"})]})]}):c.jsxs("label",{className:"import-box",style:{margin:0,padding:"16px",cursor:h?"not-allowed":"pointer"},children:[c.jsx("input",{type:"file",accept:"image/png,image/jpeg,image/webp",style:{display:"none"},onChange:k,disabled:h}),c.jsx(i0,{className:"import-icon"}),c.jsx("span",{style:{fontWeight:600},children:"Upload a base image"}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",textAlign:"center"},children:"Optional. Generate a new image based on one of your own."})]}),yt&&c.jsxs("div",{style:{marginTop:"12px"},children:[c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",fontSize:"0.8rem",marginBottom:"4px"},children:[c.jsx("span",{children:"Transformation strength"}),c.jsx("span",{children:x.denoisingStrength.toFixed(2)})]}),c.jsx("input",{type:"range",min:"0.1",max:"1",step:"0.05",value:x.denoisingStrength,onChange:L=>E(he=>({...he,denoisingStrength:parseFloat(L.target.value)})),disabled:h,style:{width:"100%"}}),c.jsx("div",{style:{fontSize:"0.72rem",color:"var(--md-sys-color-outline)",marginTop:"2px"},children:"Lower = closer to your image; higher = more creative freedom. 0.7 is a good start."})]})]}),c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Active Image Constraints"}),c.jsxs("div",{className:"chips-container",children:[c.jsxs("div",{className:"status-chip",onClick:()=>P("constraints"),children:[c.jsx(Ks,{size:14}),c.jsxs("span",{children:["Resolution: ",x.width,"x",x.height]})]}),c.jsxs("div",{className:"status-chip",onClick:()=>P("constraints"),children:[c.jsx(aa,{size:14}),c.jsxs("span",{children:["Steps: ",x.steps]})]}),c.jsxs("div",{className:"status-chip",onClick:()=>P("constraints"),children:[c.jsx(Su,{size:14}),c.jsxs("span",{children:["Sampler: ",x.sampler]})]}),c.jsx("div",{className:"status-chip",onClick:()=>P("constraints"),children:c.jsxs("span",{children:["Seed: ",x.seed===-1?"Random":x.seed]})})]})]}),c.jsxs("button",{className:"m3-btn m3-btn-filled",style:{height:"48px",marginTop:"8px"},onClick:D,disabled:h||!f.trim()||!q,children:[c.jsx(Su,{size:18}),c.jsx("span",{children:q?yt?"Generate from Base Image":"Generate Local Image":"Select Model to Generate"})]})]})]})}),c.jsxs("div",{className:"m3-card output-panel",children:[c.jsx("div",{className:"output-image-container",children:h||Z||ce?c.jsx("div",{className:"progress-overlay",children:ce?c.jsxs("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",gap:"16px",padding:"20px"},children:[c.jsx("div",{className:"progress-spinner"}),c.jsx("div",{className:"progress-text",style:{fontSize:"1.1rem",fontWeight:700,textAlign:"center"},children:"Syncing Settings & Initializing..."}),j?c.jsxs("div",{style:{width:"min(360px, 90%)"},children:[c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:"12px",fontSize:"0.82rem",fontWeight:700,marginBottom:"6px"},children:[c.jsxs("span",{children:[j.phase,j.speed?` (${j.speed})`:""]}),c.jsx("span",{children:j.total>0?`Loaded ${j.current} / ${j.total} tensors`:`${Math.round(j.progress)}%`})]}),c.jsx("div",{className:"progress-bar-container",style:{margin:0},children:c.jsx("div",{className:"progress-bar-fill",style:{width:`${Math.min(100,Math.max(0,j.progress))}%`,transition:"width 0.25s ease"}})}),(j.backendMode||j.device)&&c.jsxs("div",{style:{fontSize:"0.78rem",opacity:.8,textAlign:"center",marginTop:"8px"},children:["Loading on ",j.backendMode||"backend",j.device?` • ${j.device}`:""]})]}):c.jsx("p",{style:{fontSize:"0.85rem",opacity:.8,textAlign:"center",maxWidth:"280px",margin:0},children:"Adjusting inference settings and restarting the local model server."})]}):Z?c.jsxs("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",gap:"16px",padding:"20px"},children:[c.jsx("div",{className:"progress-spinner"}),c.jsx("div",{className:"progress-text",style:{fontSize:"1.1rem",fontWeight:700,textAlign:"center"},children:"Cancelling Generation..."}),c.jsx("p",{style:{fontSize:"0.85rem",opacity:.8,textAlign:"center",maxWidth:"280px",margin:0},children:"Stopping CPU/GPU execution threads and reloading model weights. Please wait a moment."})]}):c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"progress-spinner"}),c.jsx("div",{className:"progress-text",style:{fontSize:"1.1rem",fontWeight:700},children:I?"Decoding Latents...":"Generating Locally..."}),_&&c.jsx("div",{style:{fontSize:"0.8rem",color:"var(--md-sys-color-primary)",fontWeight:600,background:"var(--md-sys-color-primary-container)",padding:"4px 12px",borderRadius:"12px",margin:"4px 0 8px 0",animation:"pulse 2s infinite"},children:"⚠️ CPU Fallback Detected (Running Slower)"}),c.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"16px",width:"80%",margin:"8px 0 0 0",fontSize:"0.85rem",opacity:.9},children:[c.jsxs("div",{style:{textAlign:"left"},children:[c.jsx("strong",{children:"Step:"})," ",Ze," / ",x.steps," (",Math.max(0,x.steps-Ze)," left)"]}),c.jsxs("div",{style:{textAlign:"right"},children:[c.jsx("strong",{children:"Elapsed:"})," ",Se,"s",Ke&&` (${Ke==="decoding"?"decoding":Ke})`]})]}),c.jsx("div",{className:"progress-bar-container",style:{margin:"4px 0 10px 0"},children:c.jsx("div",{className:"progress-bar-fill",style:{width:`${O}%`,transition:"width 1s linear"}})}),c.jsxs("div",{style:{fontSize:"0.85rem",opacity:.8,display:"flex",gap:"8px",alignItems:"center"},children:[c.jsxs("span",{children:["Progress: ",O,"%"]}),c.jsx("span",{children:"•"}),c.jsxs("span",{children:["Est. remaining: ",Mt,"s"]})]}),c.jsx("button",{className:"m3-btn m3-btn-error",style:{marginTop:"16px",height:"36px",padding:"0 24px"},onClick:B,children:"Stop Generating"})]})}):fe?c.jsx("img",{src:fe,className:"output-image",alt:"Generated Output"}):G?c.jsxs("div",{style:{textAlign:"center",padding:"24px",color:"var(--md-sys-color-error)",display:"flex",flexDirection:"column",alignItems:"center",gap:"16px"},children:[c.jsx("div",{style:{background:"rgba(239, 68, 68, 0.1)",padding:"16px",borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center"},children:c.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"48",height:"48",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2",children:c.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"})})}),c.jsx("h3",{style:{margin:0,fontSize:"1.2rem",fontWeight:"bold"},children:"Generation Failed"}),c.jsx("p",{style:{margin:0,fontSize:"0.95rem",opacity:.9,maxWidth:"320px",lineHeight:"1.4"},children:G}),c.jsxs("div",{style:{marginTop:"8px",fontSize:"0.85rem",opacity:.8,color:"var(--md-sys-color-on-surface-variant)"},children:["Hint: Try lowering the resolution in the ",c.jsx("strong",{children:"Image Constraints"})," settings (e.g., to 512x512)."]})]}):c.jsxs("div",{style:{textAlign:"center"},children:[c.jsx(Su,{className:"output-placeholder-icon"}),c.jsxs("p",{className:"output-placeholder-text",children:["Your generated image will appear here.",c.jsx("br",{}),"Enter a prompt on the left and click Generate."]})]})}),fe&&!h&&c.jsxs(c.Fragment,{children:[c.jsxs("div",{style:{width:"100%",display:"flex",justifyContent:"space-between",marginTop:"12px",fontSize:"0.8rem",color:"var(--md-sys-color-outline)"},children:[c.jsxs("span",{children:["Seed: ",lt]}),ke&&c.jsxs("span",{children:["Inference Time: ",ke.toFixed(1),"s"]})]}),c.jsxs("div",{className:"image-actions",children:[c.jsxs("button",{className:"m3-btn m3-btn-tonal",onClick:W,children:[De?c.jsx(Xs,{size:16}):c.jsx(Jy,{size:16}),c.jsx("span",{children:De?"Copied":"Copy"})]}),c.jsxs("button",{className:"m3-btn m3-btn-outlined",onClick:ae,children:[c.jsx(nm,{size:16}),c.jsx("span",{children:"Save to USB"})]})]})]})]})]}),c.jsxs("div",{className:"gallery-section",children:[c.jsxs("div",{className:"gallery-header",children:[c.jsx("h3",{className:"m3-card-title",style:{marginBottom:0},children:"Recent Outputs Gallery"}),we.length>0&&c.jsxs("div",{className:"gallery-selection-actions",children:[c.jsxs("span",{className:"gallery-selection-count",children:[we.length," selected"]}),c.jsxs("button",{className:"m3-btn m3-btn-tonal",onClick:$e,children:[c.jsx(Xs,{size:14}),c.jsx("span",{children:"Use"})]}),c.jsxs("button",{className:"m3-btn m3-btn-outlined",onClick:za,children:[c.jsx(nm,{size:14}),c.jsx("span",{children:"Download"})]}),c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Ta,children:[c.jsx(rn,{size:14}),c.jsx("span",{children:"Delete"})]}),c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:al,children:"Clear"})]})]}),H.length===0?c.jsx("div",{className:"m3-card gallery-empty",children:"No images generated in this session yet. Everything will be saved directly on your external drive."}):c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"gallery-hint",children:"Click to select one image. Ctrl-click or Shift-click to select multiple."}),c.jsx("div",{className:"gallery-grid",children:H.map((L,he)=>c.jsx(tg,{img:L,idx:he,isSelected:Qe.includes(he),onClick:dl},L.image||he))})]})]})]})}const ag=U.memo(lg),ng=[{group:"SDXL - Best Quality",items:[{name:"Juggernaut XL v9 Lightning",filename:"Juggernaut_RunDiffusionPhoto2_Lightning_4Steps.safetensors",format:"Safetensors",approxSize:"6.6 GB",resolution:"1024x1024",notes:"Photorealistic SDXL model tuned for 4-8 step Lightning generation.",url:"https://huggingface.co/RunDiffusion/Juggernaut-XL-Lightning/resolve/main/Juggernaut_RunDiffusionPhoto2_Lightning_4Steps.safetensors"},{name:"DreamShaper XL Lightning",filename:"DreamShaperXL_Lightning.safetensors",format:"Safetensors",approxSize:"6.6 GB",resolution:"1024x1024",notes:"All-around SDXL model for fantasy art, renders, illustration, and stylized images.",url:"https://huggingface.co/Lykon/dreamshaper-xl-lightning/resolve/main/DreamShaperXL_Lightning.safetensors"}]},{group:"SD 1.5 - Ultra Fast / Low VRAM",items:[{name:"DreamShaper 8",filename:"DreamShaper_8_pruned.safetensors",format:"Safetensors",approxSize:"2.1 GB",resolution:"512x512",notes:"General-purpose SD 1.5 model for illustration, anime, and semi-realistic portraits.",url:"https://huggingface.co/Lykon/DreamShaper/resolve/main/DreamShaper_8_pruned.safetensors"},{name:"CyberRealistic V8",filename:"CyberRealistic_V8_FP16.safetensors",format:"Safetensors",approxSize:"2.1 GB",resolution:"512x512",notes:"High-fidelity realism and photorealism in the SD 1.5 ecosystem.",url:"https://huggingface.co/cyberdelia/CyberRealistic/resolve/main/CyberRealistic_V8_FP16.safetensors"},{name:"ReV Animated v1.2.2",filename:"rev-animated-v1-2-2.safetensors",format:"Safetensors",approxSize:"2.1 GB",resolution:"512x512",notes:"Semi-realistic, 2.5D, anime, fantasy, and stylized digital art.",url:"https://huggingface.co/danbrown/RevAnimated-v1-2-2/resolve/main/rev-animated-v1-2-2.safetensors"}]}];function ig({activeModel:f,setActiveModel:b,serverRunning:N,setServerRunning:r,constraints:x,showAlert:E=async({message:H})=>window.alert(H),showConfirm:q=async({message:H})=>window.confirm(H)}){const[H,w]=U.useState([]),[h,A]=U.useState(null),[O,Q]=U.useState(0),[P,J]=U.useState(""),[$,fe]=U.useState(null),[te,lt]=U.useState(null),[Ae,ke]=U.useState(null),[Ve,De]=U.useState({filename:"",speed:"0.0",eta:0,status:""}),[ee,Se]=U.useState(""),[ot,Mt]=U.useState(null),[de,Ze]=U.useState(!1),[ct,Ke]=U.useState({progress:0,phase:""}),[Le,_]=U.useState(null),[C,Z]=U.useState({backendMode:"",backendBinary:"",backendDevice:""}),me=H.map(D=>ui(D).filename),ce=$!==null||de;U.useEffect(()=>{R(),d()},[]),U.useEffect(()=>{if(!N&&!$){Z({backendMode:"",backendBinary:"",backendDevice:""});return}let D=!1;const B=async()=>{try{const ae=await Cl();if(D)return;Z({backendMode:ae.settings?.backendMode||ae.loading?.backendMode||"",backendBinary:ae.settings?.backendBinary||ae.loading?.backendBinary||"",backendDevice:ae.settings?.backendDevice||ae.loading?.device||""})}catch{}};B();const W=setInterval(B,2e3);return()=>{D=!0,clearInterval(W)}},[N,$]);const d=async()=>{try{const D=await cm();D.active&&D.filename&&j(D.filename)}catch(D){console.error("Check active download failed:",D)}},j=D=>{A(D);const B=setInterval(async()=>{try{const W=await cm();W.active?(Q(W.progress===-1?0:W.progress),J(`${W.speed} • ETA ${W.eta}s`)):(clearInterval(B),A(null),W.error?String(W.error).toLowerCase().includes("cancelled")||E({title:"Download Failed",message:W.error,danger:!0}):R())}catch(W){console.error("Polling error:",W)}},1e3)},R=async()=>{try{const D=await X0();w(D.map(ui).filter(B=>B.filename))}catch(D){console.error(D)}},G=async(D,B)=>{if(h||me.includes(B)&&!await q({title:"Overwrite Model?",message:`"${B}" is already downloaded. Do you want to download and overwrite it?`,confirmLabel:"Overwrite"}))return;const W=typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0,ae=typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1");if(!W&&!ae){E({title:"Server Required",message:"Model download requires the local image generator server.",danger:!0});return}try{const At=await I0(D);At.ok?j(B):E({title:"Download Failed",message:At.error||"Unknown error",danger:!0})}catch(At){E({title:"Download Error",message:At.message,danger:!0})}},V=async D=>{if(D.preventDefault(),!ee.trim())return;let B="model.gguf";try{const W=new URL(ee.trim());if(W.protocol!=="http:"&&W.protocol!=="https:")throw new Error("URL protocol must be http or https");if(B=W.pathname.split("/").pop()||"",!B)throw new Error("Could not extract filename from URL");const ae=B.toLowerCase();if(!ae.endsWith(".gguf")&&!ae.endsWith(".safetensors")&&!ae.endsWith(".ckpt"))throw new Error("URL must point to a .gguf, .safetensors, or .ckpt weights file")}catch(W){E({title:"Invalid URL",message:W.message,danger:!0});return}await G(ee.trim(),B),Se("")},I=D=>{G(D.url,D.filename)},se=async()=>{await P0(),A(null),Q(0),J(""),await R()},Qe=async D=>{if(f&&f!==D&&N){_(D);return}await Me(D)},Me=async D=>{const B=typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0,W=typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"),At=(await Cl())?.port||8080;if(!B&&!W){const Gt=await hm(),dl=`.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${At} --model .\\models\\${D} --threads 8`;Gt?await q({title:"Web Browser Mode",message:`To load "${D}" on the active GPU server, restart your terminal backend with: - -${dl} - -Update the UI status anyway?`,confirmLabel:"Update UI"})&&(b(D),r(!0)):await q({title:"Backend Not Running",message:`C++ backend server is not running on port ${At}. - -To run locally, start: - -${dl} - -Proceed in Simulation Mode instead?`,confirmLabel:"Proceed"})&&(b(D),r(!1));return}fe(D),lt({progress:0,phase:"Starting backend...",speed:"",current:0,total:0,model:D,backendMode:"",backendBinary:"",device:""});try{const Gt=await Vs(D,x);console.log(Gt);let dl=!1,we=null;for(let al=0;al<240;al++){const $e=await Cl();if($e.loading&<({progress:$e.loading.progress||0,phase:$e.loading.phase||"Loading model...",speed:$e.loading.speed||"",current:$e.loading.current||0,total:$e.loading.total||0,model:$e.loading.model||D,backendMode:$e.loading.backendMode||$e.settings?.backendMode||"",backendBinary:$e.loading.backendBinary||$e.settings?.backendBinary||"",device:$e.loading.device||$e.settings?.backendDevice||""}),$e.ready){dl=!0;break}if($e.error){we=$e.error;break}if(!$e.running&&al>3){we="The backend process terminated immediately on startup.";break}await new Promise(za=>setTimeout(za,500))}if(dl)lt(al=>({...al||{},progress:100,phase:"Model ready",speed:"",current:al?.current||0,total:al?.total||0,model:D})),b(D),r(!0);else throw new Error(we||`Model server failed to respond on port ${At}.`)}catch(Gt){console.error("Failed to load model:",Gt),E({title:"Model Load Failed",message:Gt.message||String(Gt),danger:!0})}finally{setTimeout(()=>lt(null),800),fe(null)}},yt=async()=>{const D=Le;_(null),await Nt(),D&&await Me(D)},Nt=async()=>{if(de)return;Ze(!0),Ke({progress:10,phase:"Stopping backend process..."});const D=setInterval(async()=>{try{const B=await Cl();B.unloading?.active?Ke({progress:B.unloading.progress||50,phase:B.unloading.phase||"Unloading model..."}):Ke(W=>({progress:Math.min(95,W.progress+10),phase:W.phase||"Unloading model..."}))}catch{}},300);try{await Js(),Ke({progress:100,phase:"Model unloaded"}),b(null),_(null),r(!1)}catch(B){console.error(B),E({title:"Unload Failed",message:B.message||String(B),danger:!0})}finally{clearInterval(D),setTimeout(()=>{Ze(!1),Ke({progress:0,phase:""})},500)}},Je=async D=>{if(await q({title:"Delete Model?",message:`Delete "${D}" from your drive?`,confirmLabel:"Delete",danger:!0}))try{await W0(D),await R(),f===D&&await Nt()}catch(B){console.error("Failed to delete model:",B),E({title:"Delete Failed",message:B.message||String(B),danger:!0})}},Ht=async(D,B)=>{const W=new AbortController;Mt(W),ke(0),De({filename:B,speed:"0.0",eta:0,status:"Initiating file copy..."});try{await J0(D,ae=>{ke(Math.round(ae.progress)),De({filename:ae.filename,speed:ae.speed_mb_s.toFixed(1),eta:Math.round(ae.eta_secs),status:ae.status})},W.signal),await R()}catch(ae){console.error("Import failed:",ae),ae.name!=="AbortError"&&E({title:"Import Failed",message:ae.message||String(ae),danger:!0})}finally{ke(null),Mt(null)}},zt=()=>{ot&&ot.abort()},k=D=>{const B=D.target.files[0];B&&Ht(B,B.name),D.target.value=""},ie=!!h;return c.jsxs("div",{className:"workspace-area",children:[c.jsxs("div",{className:"workspace-title-section",children:[c.jsx("h2",{className:"workspace-title",children:"Model Manager"}),c.jsx("p",{className:"workspace-subtitle",children:"Manage local files and download recommended weights directly to your local models folder."})]}),f&&c.jsx("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-primary)",background:"var(--md-sys-color-primary-container)",color:"var(--md-sys-color-on-primary-container)"},children:c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[c.jsxs("div",{children:[c.jsxs("h4",{style:{fontWeight:700},children:["Active Model: ",f]}),c.jsx("p",{style:{fontSize:"0.85rem",marginTop:"2px",opacity:.9},children:"The local C++ stable-diffusion server is running. Real-time telemetry is displaying performance status in the top bar."}),(C.backendMode||C.backendDevice||C.backendBinary)&&c.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"8px",marginTop:"10px"},children:[C.backendMode&&c.jsxs("span",{className:"status-chip",style:{cursor:"default"},children:[c.jsx(xu,{size:14}),c.jsxs("span",{children:["Loaded on ",C.backendMode]})]}),C.backendDevice&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:C.backendDevice})}),C.backendBinary&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:C.backendBinary})})]})]}),c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Nt,disabled:de||$!==null,children:[de?c.jsx(aa,{className:"progress-spinner",size:16}):c.jsx(rn,{size:16}),c.jsx("span",{children:de?"Unloading":"Unload Server"})]})]})}),de&&c.jsxs("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-error)",marginTop:"16px"},children:[c.jsx("h4",{style:{fontWeight:700,marginBottom:"8px"},children:"Unloading Model"}),c.jsxs("div",{className:"model-progress-section",style:{margin:"8px 0 0 0"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsx("span",{children:ct.phase||"Unloading model..."}),c.jsxs("span",{children:[Math.round(ct.progress),"%"]})]}),c.jsx("div",{className:"model-progress-bar",children:c.jsx("div",{className:"model-progress-fill",style:{width:`${Math.min(100,Math.max(0,ct.progress))}%`,transition:"width 0.25s ease"}})})]})]}),te&&c.jsxs("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-secondary)",marginTop:"16px"},children:[c.jsxs("h4",{style:{fontWeight:700,marginBottom:"8px"},children:["Loading Model: ",te.model||$]}),(te.backendMode||te.backendBinary||te.device)&&c.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"8px",marginBottom:"10px"},children:[te.backendMode&&c.jsxs("span",{className:"status-chip",style:{cursor:"default"},children:[c.jsx(xu,{size:14}),c.jsxs("span",{children:["Loading on ",te.backendMode]})]}),te.device&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:te.device})}),te.backendBinary&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:te.backendBinary})})]}),c.jsxs("div",{className:"model-progress-section",style:{margin:"8px 0 0 0"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsxs("span",{children:[te.phase,te.speed?` (${te.speed})`:""]}),c.jsx("span",{children:te.total>0?`Loaded ${te.current} / ${te.total} tensors`:`${Math.round(te.progress)}%`})]}),c.jsx("div",{className:"model-progress-bar",children:c.jsx("div",{className:"model-progress-fill",style:{width:`${Math.min(100,Math.max(0,te.progress))}%`,transition:"width 0.25s ease"}})})]})]}),c.jsxs("div",{className:"m3-card",style:{marginTop:"24px"},children:[c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(Qs,{size:18,style:{color:"var(--md-sys-color-primary)"}}),"Local Models (",H.length,")"]}),H.length===0?c.jsx("p",{style:{fontSize:"0.9rem",color:"var(--md-sys-color-outline)",textAlign:"center",padding:"16px 0"},children:"No models detected in app/models/. Download from the library below or import a file."}):c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"10px"},children:H.map(D=>{const B=D.filename,W=f===B;return c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"12px 16px",background:W?"var(--md-sys-color-primary-container)":"var(--md-sys-color-surface-variant)",color:W?"var(--md-sys-color-on-primary-container)":"var(--md-sys-color-on-surface-variant)",borderRadius:"var(--md-shape-corner-medium)",border:"1px solid var(--md-sys-color-outline-variant)"},children:[c.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"2px"},children:[c.jsx("span",{style:{fontWeight:600,fontSize:"0.95rem"},children:B}),c.jsxs("span",{style:{fontSize:"0.75rem",opacity:.8},children:["Local Weights File • ",D.size||ii(D.sizeBytes)]})]}),c.jsx("div",{style:{display:"flex",gap:"8px"},children:W?c.jsxs("button",{className:"m3-btn m3-btn-error",style:{height:"36px",padding:"0 16px"},onClick:Nt,disabled:de,children:[de?c.jsx(aa,{className:"progress-spinner",size:14}):c.jsx(rn,{size:14}),c.jsx("span",{children:de?"Unloading":"Unload"})]}):c.jsxs(c.Fragment,{children:[c.jsxs("button",{className:"m3-btn m3-btn-filled",style:{height:"36px",padding:"0 16px"},onClick:()=>Qe(B),disabled:ce,children:[$===B?c.jsx(aa,{className:"progress-spinner",size:14}):c.jsx(Qs,{size:14}),c.jsx("span",{children:$===B?"Loading":"Load"})]}),c.jsx("button",{className:"m3-btn m3-btn-error",style:{height:"36px",width:"36px",padding:0,minWidth:"36px"},onClick:()=>Je(B),disabled:ce,title:"Delete from models folder",children:c.jsx(rn,{size:14})})]})})]},B)})})]}),c.jsx("div",{className:"workspace-title-section",style:{marginTop:"32px",marginBottom:"16px"},children:c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(r0,{size:20,style:{color:"var(--md-sys-color-primary)"}}),"Model Library"]})}),c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"16px"},children:ng.map(D=>c.jsxs("div",{className:"m3-card",style:{margin:0},children:[c.jsx("h4",{style:{fontWeight:700,marginBottom:"12px"},children:D.group}),c.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(280px, 1fr))",gap:"12px"},children:D.items.map(B=>{const W=me.includes(B.filename),ae=h===B.filename;return c.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"10px",padding:"14px",background:"var(--md-sys-color-surface-variant)",border:"1px solid var(--md-sys-color-outline-variant)",borderRadius:"var(--md-shape-corner-medium)"},children:[c.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"4px"},children:[c.jsx("span",{style:{fontWeight:700},children:B.name}),c.jsxs("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)"},children:[B.format," • approx. ",B.approxSize," • ",B.resolution]})]}),c.jsx("p",{style:{fontSize:"0.78rem",color:"var(--md-sys-color-on-surface-variant)",lineHeight:1.35,margin:0},children:B.notes}),ae&&c.jsxs("div",{className:"model-progress-section",style:{marginTop:"2px"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"8px"},children:[c.jsx("span",{className:"loading-dot"}),"Downloading"]}),c.jsx("span",{children:O>0?`${O}%`:"Preparing"})]}),c.jsx("div",{className:"model-progress-bar",children:c.jsx("div",{className:"model-progress-fill",style:{width:`${Math.min(100,Math.max(0,O))}%`,transition:"width 0.2s ease"}})}),P&&c.jsx("div",{style:{fontSize:"0.7rem",color:"var(--md-sys-color-outline)",marginTop:"4px"},children:P})]}),c.jsxs("button",{className:W?"m3-btn m3-btn-tonal":"m3-btn m3-btn-filled",style:{height:"36px",marginTop:"auto"},onClick:()=>I(B),disabled:W||h!==null,children:[W?c.jsx(xu,{size:14}):ae?c.jsx(aa,{className:"progress-spinner",size:14}):c.jsx(am,{size:14}),c.jsx("span",{children:W?"Downloaded":ae?"Downloading":"Download"})]}),c.jsx("a",{href:B.url,target:"_blank",rel:"noreferrer",style:{alignSelf:"center",color:"var(--md-sys-color-primary)",fontSize:"0.75rem",fontWeight:600,lineHeight:1.2,textDecoration:"none"},children:"Save to Downloads folder"})]},B.filename)})})]},D.group))}),c.jsx("div",{className:"workspace-title-section",style:{marginTop:"32px",marginBottom:"16px"},children:c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(im,{size:20,style:{color:"var(--md-sys-color-primary)"}}),"Local Model Import"]})}),Ae!==null&&c.jsxs("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-primary)",marginTop:"24px"},children:[c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"12px"},children:[c.jsxs("h4",{style:{fontWeight:600},children:["Copying Model to local folder: ",Ve.filename]}),c.jsxs("button",{className:"m3-btn m3-btn-error",style:{height:"34px",padding:"0 14px"},onClick:zt,children:[c.jsx(ks,{size:14}),c.jsx("span",{children:"Stop Import"})]})]}),c.jsxs("div",{className:"model-progress-section",style:{margin:"12px 0 6px 0"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsxs("span",{children:[Ve.status," (",Ve.speed," MB/s)"]}),c.jsxs("span",{children:[Ae,"% (",Ve.eta,"s remaining)"]})]}),c.jsx("div",{className:"model-progress-bar",children:c.jsx("div",{className:"model-progress-fill",style:{width:`${Ae}%`,transition:"width 0.15s ease"}})})]})]}),c.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(300px, 1fr))",gap:"16px",marginTop:"16px"},children:[c.jsxs("label",{className:"import-box",style:{margin:0,height:"100%",justifyContent:"center"},children:[c.jsx("input",{type:"file",style:{display:"none"},accept:".gguf,.safetensors,.ckpt",onChange:k,disabled:Ae!==null}),c.jsx(im,{className:"import-icon"}),c.jsx("span",{style:{fontWeight:600},children:"Choose weights file"}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",textAlign:"center"},children:"Select `.gguf`, `.safetensors` or `.ckpt` weights."})]}),c.jsxs("div",{className:"m3-card",style:{display:"flex",flexDirection:"column",justifyContent:"center",margin:0,padding:"20px"},children:[c.jsxs("h4",{style:{fontWeight:600,marginBottom:"8px",display:"flex",alignItems:"center",gap:"8px"},children:[c.jsx(am,{size:16}),"Download Model from URL"]}),c.jsx("p",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",marginBottom:"12px"},children:"Download any GGUF or Safetensors model from Hugging Face or other sites directly to your models folder."}),ie?c.jsxs("div",{className:"model-progress-section",style:{marginTop:"0px"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsx("span",{style:{fontSize:"0.75rem",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:"200px"},title:h,children:h}),c.jsxs("span",{children:[O,"%"]})]}),c.jsx("div",{className:"model-progress-bar",style:{margin:"4px 0"},children:c.jsx("div",{className:"model-progress-fill",style:{width:`${O}%`,transition:"width 0.15s ease"}})}),c.jsx("div",{style:{fontSize:"0.7rem",color:"var(--md-sys-color-outline)",marginTop:"2px"},children:P}),c.jsxs("button",{className:"m3-btn m3-btn-error",style:{height:"34px",padding:"0 14px",marginTop:"10px"},onClick:se,children:[c.jsx(ks,{size:14}),c.jsx("span",{children:"Stop Download"})]})]}):c.jsxs("form",{onSubmit:V,style:{display:"flex",gap:"8px"},children:[c.jsx("input",{type:"text",className:"m3-input",style:{flex:1,height:"40px"},placeholder:"e.g. https://huggingface.co/...",value:ee,onChange:D=>Se(D.target.value),disabled:h!==null}),c.jsx("button",{type:"submit",className:"m3-btn m3-btn-filled",style:{height:"40px"},disabled:h!==null,children:"Download"})]})]})]}),Le&&c.jsx("div",{style:{position:"fixed",inset:0,zIndex:50,background:"rgba(0,0,0,0.55)",display:"flex",alignItems:"center",justifyContent:"center",padding:"24px"},children:c.jsxs("div",{className:"m3-card",style:{maxWidth:"460px",width:"100%",margin:0,border:"1px solid var(--md-sys-color-outline-variant)"},children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",marginBottom:"10px"},children:[c.jsx(M0,{size:22,style:{color:"var(--md-sys-color-error)"}}),c.jsx("h3",{style:{fontSize:"1.05rem",fontWeight:700},children:"Unload Current Model?"})]}),c.jsxs("p",{style:{fontSize:"0.9rem",color:"var(--md-sys-color-on-surface-variant)",lineHeight:1.45,marginBottom:"16px"},children:['"',f,'" is already loaded. Unload it before loading "',Le,'".']}),c.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"10px"},children:[c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:()=>_(null),disabled:de,children:"Cancel"}),c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Nt,disabled:de,children:[c.jsx(rn,{size:14}),c.jsx("span",{children:"Unload Only"})]}),c.jsxs("button",{className:"m3-btn m3-btn-filled",onClick:yt,disabled:de,children:[de?c.jsx(aa,{className:"progress-spinner",size:14}):c.jsx(aa,{size:14}),c.jsx("span",{children:"Unload and Load"})]})]})]})})]})}const ug=U.memo(ig),sm=[{id:"1:1",label:"1:1 Square",width:512,height:512,sdxl_width:1024,sdxl_height:1024,desc:"Social posts & avatars"},{id:"4:3",label:"4:3 Photo",width:640,height:480,sdxl_width:1152,sdxl_height:864,desc:"Classic photo look"},{id:"16:9",label:"16:9 Landscape",width:768,height:432,sdxl_width:1216,sdxl_height:684,desc:"Widescreen landscape"},{id:"9:16",label:"9:16 Portrait",width:432,height:768,sdxl_width:684,sdxl_height:1216,desc:"Tall phone screen"}],cg=f=>{if(!f)return!0;const b=f.toLowerCase();return!(b.includes("flux")||b.includes("schnell")||b.includes("sdxl")||b.includes("lightning")||b.includes("turbo")||b.includes("sd3"))};function sg({constraints:f,setConstraints:b,activeModel:N,specs:r,backendOptions:x}){const E=N?cg(N):!1,q=x?.options?.length?x.options:[{id:"cpu",label:"CPU",available:!0}],H=(A,O)=>{b(Q=>({...Q,[A]:O}))},w=(A,O)=>{const Q=O==="sdxl"&&!E,P=sm.find(J=>J.id===A);if(P){let J=Q?P.sdxl_width:P.width,$=Q?P.sdxl_height:P.height;E&&(J>$?($=Math.round($*512/J),J=512):(J=Math.round(J*512/$),$=512),J=Math.round(J/64)*64,$=Math.round($/64)*64),H("width",J),H("height",$)}},h=A=>{b(O=>({...O,backendType:A,useGpu:A!=="cpu"}))};return c.jsxs("div",{className:"workspace-area",children:[c.jsxs("div",{className:"workspace-title-section",children:[c.jsx("h2",{className:"workspace-title",children:"Image Constraints"}),c.jsx("p",{className:"workspace-subtitle",children:"Configure size, quality, and performance controls for image generation."})]}),c.jsxs("div",{className:"generator-layout",children:[c.jsxs("div",{children:[c.jsxs("div",{className:"m3-card",children:[c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(Fy,{size:18,style:{color:"var(--md-sys-color-primary)"}}),"1. Picture Size & Shape"]}),E&&c.jsxs("div",{style:{background:"rgba(251, 191, 36, 0.1)",border:"1px solid rgb(251, 191, 36)",color:"var(--md-sys-color-on-surface)",padding:"12px",borderRadius:"8px",marginBottom:"16px",fontSize:"0.85rem",display:"flex",gap:"8px",alignItems:"flex-start"},children:[c.jsx(f0,{size:16,style:{color:"rgb(251, 191, 36)",flexShrink:0,marginTop:"2px"}}),c.jsxs("div",{children:[c.jsx("strong",{children:"Low-VRAM Protection Active:"})," Capped at 512x512 for the loaded SD 1.x model (",c.jsx("code",{children:N}),"). Generating at larger sizes (like 1024x1024) is disabled to prevent out-of-memory crashes on your ",r&&r.gpu_name&&!r.gpu_name.includes("Loading")?r.gpu_name:"graphics processor","."]})]}),c.jsxs("div",{className:"m3-field-group",children:[c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"AI Generation Engine Optimization"}),c.jsxs("div",{className:"m3-segmented-button",children:[E?c.jsx("div",{className:"m3-segment-item disabled",style:{opacity:.5,cursor:"not-allowed",textDecoration:"line-through",backgroundColor:"var(--md-sys-color-surface-variant)"},title:"High Quality Mode is disabled for SD 1.x models.",children:"High Quality Mode (1024px)"}):c.jsx("div",{className:`m3-segment-item ${f.width>=1024?"active":""}`,onClick:()=>{H("width",1024),H("height",1024)},children:"High Quality Mode (1024px)"}),c.jsx("div",{className:`m3-segment-item ${f.width<1024||E?"active":""}`,onClick:()=>{H("width",512),H("height",512)},children:"Standard Speed Mode (512px)"})]}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",marginTop:"4px"},children:E?"*Standard Speed Mode (512px) is forced for SD 1.5 / custom models to ensure stable local generations.":"*Use High Quality (1024px) for Flux or SDXL models. Use Standard (512px) for SD 1.5."})]}),c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Select Shape (Aspect Ratio)"}),c.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"8px"},children:sm.map(A=>{const O=Math.abs(f.width/f.height-A.width/A.height)<.1;return c.jsxs("button",{className:`m3-btn m3-btn-outlined aspect-ratio-btn ${O?"active":""}`,style:{height:"86px",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",padding:"8px",borderRadius:"var(--md-shape-corner-medium)",borderColor:"var(--md-sys-color-outline-variant)"},onClick:()=>w(A.id,E?"sd15":f.width>=1024?"sdxl":"sd15"),children:[c.jsx("div",{className:`aspect-ratio-preview ratio-${A.id.replace(":","-")}`}),c.jsx("span",{style:{fontWeight:600,fontSize:"0.85rem",marginTop:"2px"},children:A.label}),c.jsx("span",{style:{fontSize:"0.68rem",opacity:.7},children:E?`Max: ${A.id==="1:1"?"512x512":A.id==="4:3"?"512x384":A.id==="16:9"?"512x288":"288x512"}`:A.desc})]},A.id)})})]})]})]}),c.jsxs("div",{className:"m3-card",children:[c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(Ks,{size:18,style:{color:"var(--md-sys-color-primary)"}}),"2. Quality, Speed & Logic"]}),c.jsxs("div",{className:"m3-field-group",children:[c.jsxs("div",{className:"m3-slider-group",children:[c.jsxs("div",{className:"m3-slider-header",children:[c.jsx("span",{className:"m3-slider-label",children:"Detail Steps (Inference speed)"}),c.jsxs("span",{className:"m3-slider-value",children:[f.steps," steps"]})]}),c.jsx("input",{type:"range",className:"m3-slider",value:f.steps,onChange:A=>H("steps",parseInt(A.target.value)),min:"1",max:"60"}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",lineHeight:"1.3"},children:"The number of times the AI cleans up the image. More steps = sharper details, but takes longer."})]}),c.jsxs("div",{className:"m3-text-field",style:{marginTop:"8px"},children:[c.jsx("label",{className:"m3-text-field-label",children:"Image Blueprint (DNA Seed)"}),c.jsxs("div",{style:{display:"flex",gap:"8px"},children:[c.jsx("input",{type:"number",className:"m3-input",value:f.seed,onChange:A=>H("seed",parseInt(A.target.value)||-1),placeholder:"-1 for a brand new image...",style:{flex:1,height:"40px"}}),c.jsx("button",{className:"m3-btn m3-btn-tonal",onClick:()=>H("seed",-1),style:{height:"40px"},children:"Random (-1)"})]})]})]})]})]}),c.jsx("div",{children:c.jsxs("div",{className:"m3-card",children:[c.jsxs("h3",{className:"m3-card-title",children:[c.jsx(rm,{size:18,style:{color:"var(--md-sys-color-primary)"}}),"3. System Settings"]}),c.jsxs("div",{className:"m3-field-group",children:[c.jsxs("div",{className:"m3-text-field",children:[c.jsx("label",{className:"m3-text-field-label",children:"Generation Backend"}),c.jsx("div",{className:"m3-segmented-button",children:q.map(A=>c.jsx("div",{className:`m3-segment-item ${f.backendType===A.id||!f.backendType&&A.id==="cpu"?"active":""}`,onClick:()=>h(A.id),title:A.id==="cuda"?"CUDA appears only when an NVIDIA CUDA backend is available.":void 0,children:A.label},A.id))}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",marginTop:"4px",lineHeight:1.35},children:"CPU is slow but safest. Vulkan works on supported GPUs. CUDA is shown only when NVIDIA CUDA support is available."}),f.backendType==="cuda"&&r?.gpu_name&&String(r.gpu_name).toLowerCase().includes("gtx")&&c.jsxs("div",{style:{marginTop:"12px",padding:"10px 14px",background:"rgba(239, 68, 68, 0.08)",border:"1px dashed rgb(239, 68, 68)",borderRadius:"8px",fontSize:"0.75rem",color:"var(--md-sys-color-on-surface)",lineHeight:"1.45"},children:[c.jsx("strong",{children:"Performance Alert:"})," Your graphics card (",c.jsx("code",{children:r.gpu_name}),") is a GTX-series GPU which lacks hardware ",c.jsx("strong",{children:"Tensor Cores"}),". Running in CUDA mode will be up to 3x slower. We strongly recommend switching to ",c.jsx("strong",{children:"Vulkan GPU"})," for optimal generation speed."]}),x?.unavailable?.length>0&&c.jsx("div",{style:{marginTop:"8px",display:"flex",flexDirection:"column",gap:"4px"},children:x.unavailable.map(A=>c.jsxs("span",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-error)",lineHeight:1.35},children:[A.label," unavailable: ",A.reason]},A.id))})]}),c.jsxs("div",{className:"m3-text-field",style:{marginTop:"20px"},children:[c.jsx("label",{className:"m3-text-field-label",children:"Memory Optimization (GPU VRAM)"}),c.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"14px",marginTop:"10px"},children:[c.jsxs("label",{style:{display:"flex",alignItems:"flex-start",gap:"10px",cursor:"pointer",fontSize:"0.85rem"},children:[c.jsx("input",{type:"checkbox",checked:f.vaeTiling!==!1,onChange:A=>H("vaeTiling",A.target.checked),style:{width:"16px",height:"16px",marginTop:"3px",accentColor:"var(--md-sys-color-primary)",cursor:"pointer"}}),c.jsxs("div",{children:[c.jsx("strong",{style:{color:"var(--md-sys-color-on-surface)"},children:"Enable VAE Tiling"}),c.jsx("div",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",marginTop:"2px",lineHeight:1.35},children:"Processes VAE decoding in smaller tiles. Drastically reduces VRAM usage (from 2GB+ down to ~100MB) with no speed loss. Highly recommended for GPUs with 4GB-6GB VRAM."})]})]}),c.jsxs("label",{style:{display:"flex",alignItems:"flex-start",gap:"10px",cursor:"pointer",fontSize:"0.85rem"},children:[c.jsx("input",{type:"checkbox",checked:f.vaeOnCpu===!0,onChange:A=>H("vaeOnCpu",A.target.checked),style:{width:"16px",height:"16px",marginTop:"3px",accentColor:"var(--md-sys-color-primary)",cursor:"pointer"}}),c.jsxs("div",{children:[c.jsx("strong",{style:{color:"var(--md-sys-color-on-surface)"},children:"Run VAE on CPU"}),c.jsx("div",{style:{fontSize:"0.75rem",color:"var(--md-sys-color-outline)",marginTop:"2px",lineHeight:1.35},children:"Offloads the heavy VAE decoder computation from GPU VRAM to system memory (RAM). Saves ~2GB of VRAM, but makes the final decoding stage slightly slower."})]})]})]})]})]})]})})]})]})}const fg=U.memo(sg);function og(){const f=U.useRef(null),[b,N]=U.useState(null),r=U.useCallback(k=>{const ie=f.current;f.current=null,N(null),ie&&ie(k)},[]),x=U.useCallback(({title:k="Confirm Action",message:ie,confirmLabel:D="OK",cancelLabel:B="Cancel",danger:W=!1})=>new Promise(ae=>{f.current=ae,N({type:"confirm",title:k,message:ie,confirmLabel:D,cancelLabel:B,danger:W})}),[]),E=U.useCallback(({title:k="Notice",message:ie,confirmLabel:D="OK",danger:B=!1})=>new Promise(W=>{f.current=W,N({type:"alert",title:k,message:ie,confirmLabel:D,danger:B})}),[]),[q,H]=U.useState(()=>{const k=localStorage.getItem("theme");return k||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")});U.useEffect(()=>{document.documentElement.setAttribute("data-theme",q),localStorage.setItem("theme",q)},[q]);const[w,h]=U.useState("generator"),[A,O]=U.useState(""),[Q,P]=U.useState(""),[J,$]=U.useState(null),[fe,te]=U.useState(!1),[lt,Ae]=U.useState(!1),[ke,Ve]=U.useState({options:[{id:"cpu",label:"CPU",available:!0}],cudaAvailable:!1,vulkanAvailable:!1,defaultBackendType:"cpu"}),[De,ee]=U.useState(null),[Se,ot]=U.useState([]),[Mt,de]=U.useState(!1),[Ze,ct]=U.useState(!1),[Ke,Le]=U.useState([]),[_,C]=U.useState(!1),[Z,me]=U.useState(0),[ce,d]=U.useState({os_name:"Loading Specs...",cpu_name:"Loading Specs...",cpu_cores_physical:4,cpu_cores_logical:8,ram_total_gb:0,gpu_name:"Loading Specs..."}),[j,R]=U.useState({cpu_usage:0,ram_used_gb:0,ram_total_gb:0,gpu_name:"Detecting...",vram_used_gb:0,vram_total_gb:0}),[G,V]=U.useState({width:1024,height:1024,steps:4,cfgScale:1,sampler:"euler_a",seed:-1,denoisingStrength:.7,useGpu:!0,useTaesd:!0,useFlashAttn:!0,useTiling:!1,vaeTiling:!0,vaeOnCpu:!1,threads:4,backendType:"auto"});U.useEffect(()=>{async function k(){try{const ie=await q0(),D=await Y0();Ve(D),d(ie),V(B=>({...B,threads:ie.cpu_cores_physical||4,backendType:B.backendType==="auto"?D.defaultBackendType||"cpu":B.backendType,useGpu:(B.backendType==="auto"?D.defaultBackendType:B.backendType)!=="cpu"}))}catch(ie){console.error("Error fetching hardware specs:",ie)}}k()},[]);const I=U.useCallback(async()=>{de(!0);try{const[k,ie]=await Promise.all([R0(),H0().catch(()=>[])]);ee(k),ot(ie)}finally{de(!1)}},[]);U.useEffect(()=>{I()},[I]);const se=U.useCallback(async()=>{try{const k=await B0();await navigator.clipboard.writeText(JSON.stringify(k,null,2)),ct(!0),setTimeout(()=>ct(!1),1800)}catch(k){E({title:"Diagnostics Failed",message:k.message||String(k),danger:!0})}},[E]),Qe=U.useCallback(async()=>{if(Se.length===0)return;const k=Se.reduce((D,B)=>D+Number(B.sizeBytes||0),0);if(await x({title:"Clean Safe Files?",message:`Delete ${Se.length} temporary/cache item${Se.length===1?"":"s"} and free about ${ii(k)}? - -Models and generated outputs are not included.`,confirmLabel:"Clean",danger:!0})){de(!0);try{await G0(Se.map(D=>D.id)),await I()}catch(D){E({title:"Cleanup Failed",message:D.message||String(D),danger:!0})}finally{de(!1)}}},[Se,I,E,x]);U.useEffect(()=>{async function k(){const ie=await Q0();Le(ie)}k()},[]),U.useEffect(()=>{J?localStorage.setItem("active-model",J):localStorage.removeItem("active-model")},[J]),U.useEffect(()=>{let k=null;async function ie(){const D=await Cl();if(!!D.ready){k&&clearInterval(k);const W=localStorage.getItem("active-model");if(W)$(W),te(!0);else{try{if(D&&D.settings&&D.settings.model){const ae=D.settings.model.split(/[\\/]/).pop();$(ae)}}catch{}te(!0)}}else if(!(typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0)){const ae=localStorage.getItem("active-model");ae&&($(ae),te(!1))}}return ie(),k=setInterval(ie,2e3),()=>{k&&clearInterval(k)}},[]),U.useEffect(()=>{async function k(){try{const D=await L0();R(B=>B.cpu_usage===D.cpu_usage&&B.ram_used_gb===D.ram_used_gb&&B.ram_total_gb===D.ram_total_gb&&B.gpu_name===D.gpu_name&&B.vram_used_gb===D.vram_used_gb&&B.vram_total_gb===D.vram_total_gb?B:D)}catch{}}k();const ie=setInterval(k,1500);return()=>clearInterval(ie)},[]),U.useEffect(()=>{if(J){const k=J.toLowerCase();k.includes("flux")||k.includes("schnell")?V(ie=>({...ie,steps:4,cfgScale:1,width:1024,height:1024})):k.includes("lightning")||k.includes("turbo")?V(ie=>({...ie,steps:4,cfgScale:1.5,width:1024,height:1024})):k.includes("sd15")?V(ie=>({...ie,steps:25,cfgScale:7,width:512,height:512})):k.includes("sd35")?V(ie=>({...ie,steps:20,cfgScale:4.5,width:1024,height:1024})):V(ie=>({...ie,steps:20,cfgScale:7,width:512,height:512}))}},[J]);const Me=U.useMemo(()=>{switch(w){case"generator":return c.jsx(ag,{prompt:A,setPrompt:O,negativePrompt:Q,setNegativePrompt:P,constraints:G,setConstraints:V,activeModel:J,generatedImages:Ke,setGeneratedImages:Le,isGenerating:_,setIsGenerating:C,generationProgress:Z,setGenerationProgress:me,setActiveTab:h,showAlert:E,showConfirm:x});case"models":return c.jsx(ug,{activeModel:J,setActiveModel:$,serverRunning:fe,setServerRunning:te,constraints:G,showAlert:E,showConfirm:x});case"constraints":return c.jsx(fg,{constraints:G,setConstraints:V,activeModel:J,specs:ce,backendOptions:ke});default:return null}},[w,A,Q,G,J,Ke,_,Z,fe,ce,ke,E,x]),yt=U.useMemo(()=>c.jsx(E0,{activeTab:w,setActiveTab:h,specs:ce}),[w,ce]),Nt=U.useCallback(async()=>{if(!(!fe||lt)){Ae(!0);try{await Js(),te(!1),$(null)}catch(k){console.error("Failed to stop server:",k),E({title:"Stop Server Failed",message:k.message||String(k),danger:!0})}finally{Ae(!1)}}},[fe,lt,E]),Je=[...De?.stale?["Restart Local AI Image Generator so the local server loads the latest API."]:[],...De?.issues||[]],Ht=Se.reduce((k,ie)=>k+Number(ie.sizeBytes||0),0),zt=!!(De&&(Je.length>0||Se.length>0));return c.jsxs("div",{className:"app-container",children:[yt,c.jsxs("div",{className:"main-content",children:[c.jsx(D0,{telemetry:j,serverRunning:fe,activeModel:J,onStopServer:Nt,isStoppingServer:lt,theme:q,setTheme:H}),zt&&c.jsxs("div",{className:`m3-card readiness-card ${De?.stale||Je.length>0?"readiness-card-warning":""}`,children:[c.jsxs("div",{className:"readiness-header",children:[c.jsxs("div",{children:[c.jsx("h3",{className:"m3-card-title",style:{marginBottom:"4px"},children:De?.stale?"Restart Required":Je.length>0?"System Readiness":"Safe Cleanup Available"}),c.jsx("p",{className:"m3-card-subtitle",style:{margin:0},children:De?.stale?`Running server build: ${De.build||"unknown"}`:Je.length>0?"Local AI Image Generator found setup items that may need attention.":`${Se.length} temporary item${Se.length===1?"":"s"} can be cleaned (${ii(Ht)}).`})]}),c.jsxs("div",{className:"readiness-actions",children:[c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:I,disabled:Mt,children:Mt?"Checking":"Refresh"}),c.jsx("button",{className:"m3-btn m3-btn-tonal",onClick:se,children:Ze?"Copied":"Copy Diagnostics"}),Se.length>0&&c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Qe,disabled:Mt,children:["Clean ",ii(Ht)]})]})]}),Je.length>0&&c.jsx("div",{className:"readiness-issues",children:Je.slice(0,4).map(k=>c.jsx("span",{children:k},k))}),Se.length>0&&c.jsx("div",{className:"readiness-cleanup-list",children:Se.slice(0,3).map(k=>c.jsxs("span",{title:k.path,children:[k.name," · ",k.size," · ",k.reason]},k.id))})]}),Me]}),b&&c.jsx("div",{className:"app-dialog-backdrop",role:"presentation",children:c.jsxs("div",{className:"app-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"app-dialog-title",children:[c.jsx("h3",{id:"app-dialog-title",className:"app-dialog-title",children:b.title}),c.jsx("p",{className:"app-dialog-message",children:b.message}),c.jsxs("div",{className:"app-dialog-actions",children:[b.type==="confirm"&&c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:()=>r(!1),children:b.cancelLabel}),c.jsx("button",{className:`m3-btn ${b.danger?"m3-btn-error":"m3-btn-filled"}`,onClick:()=>r(!0),children:b.confirmLabel})]})]})})]})}Gy.createRoot(document.getElementById("root")).render(c.jsx(Dy.StrictMode,{children:c.jsx(og,{})}));export{ja as i,rg as t}; diff --git a/app/dist/assets/index-DW6G7wc2.css b/app/dist/assets/index-DW6G7wc2.css deleted file mode 100644 index d6e6769..0000000 --- a/app/dist/assets/index-DW6G7wc2.css +++ /dev/null @@ -1 +0,0 @@ -:root{font-family:Outfit,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-shape-corner-small: 8px;--md-shape-corner-medium: 12px;--md-shape-corner-large: 16px;--md-shape-corner-extra-large: 22px;--md-shape-corner-full: 9999px;--md-transition-duration: .25s;--md-transition-easing: cubic-bezier(.4, 0, .2, 1);--md-sys-color-primary: #4f46e5;--md-sys-color-on-primary: #ffffff;--md-sys-color-primary-container: rgba(79, 70, 229, .06);--md-sys-color-on-primary-container: #4338ca;--md-sys-color-secondary: #0ea5e9;--md-sys-color-on-secondary: #ffffff;--md-sys-color-secondary-container: rgba(14, 165, 233, .06);--md-sys-color-on-secondary-container: #0369a1;--md-sys-color-tertiary: #d946ef;--md-sys-color-on-tertiary: #ffffff;--md-sys-color-background: #f8fafc;--md-sys-color-on-background: #0f172a;--md-sys-color-surface: rgba(255, 255, 255, .75);--md-sys-color-on-surface: #0f172a;--md-sys-color-surface-variant: rgba(241, 245, 249, .8);--md-sys-color-on-surface-variant: #475569;--md-sys-color-outline: #64748b;--md-sys-color-outline-variant: #94a3b8;--border-color: rgba(15, 23, 42, .08);--border-color-muted: rgba(15, 23, 42, .05);--md-sys-color-error: #ef4444;--md-sys-color-on-error: #ffffff;--md-sys-color-success: #10b981;--md-sys-color-on-success: #ffffff;--md-sys-color-card-border: rgba(15, 23, 42, .06);--glass-shadow: 0 8px 32px 0 rgba(15, 23, 42, .04);--glass-blur: blur(16px)}html[data-theme=dark]{--md-sys-color-primary: #6366f1;--md-sys-color-on-primary: #ffffff;--md-sys-color-primary-container: rgba(99, 102, 241, .12);--md-sys-color-on-primary-container: #c7d2fe;--md-sys-color-secondary: #38bdf8;--md-sys-color-on-secondary: #0f172a;--md-sys-color-secondary-container: rgba(56, 189, 248, .08);--md-sys-color-on-secondary-container: #bae6fd;--md-sys-color-tertiary: #f472b6;--md-sys-color-on-tertiary: #0f172a;--md-sys-color-background: #090a0f;--md-sys-color-on-background: #f4f4f5;--md-sys-color-surface: rgba(18, 20, 29, .65);--md-sys-color-on-surface: #f4f4f5;--md-sys-color-surface-variant: rgba(26, 29, 41, .55);--md-sys-color-on-surface-variant: #a1a1aa;--md-sys-color-outline: #a1a1aa;--md-sys-color-outline-variant: #71717a;--border-color: rgba(255, 255, 255, .09);--border-color-muted: rgba(255, 255, 255, .05);--md-sys-color-error: #f87171;--md-sys-color-on-error: #ffffff;--md-sys-color-success: #34d399;--md-sys-color-on-success: #090a0f;--md-sys-color-card-border: rgba(255, 255, 255, .04);--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, .3);--glass-blur: blur(16px)}*{box-sizing:border-box;margin:0;padding:0}body{background-color:var(--md-sys-color-background);background-image:radial-gradient(at 0% 0%,var(--md-sys-color-primary-container) 0px,transparent 50%),radial-gradient(at 100% 0%,var(--md-sys-color-secondary-container) 0px,transparent 40%);color:var(--md-sys-color-on-background);overflow:hidden;height:100vh;width:100vw;font-size:16px;transition:background-color var(--md-transition-duration) ease}.app-container{display:flex;flex-direction:row;height:100vh;width:100vw;overflow:hidden}.sidebar{width:280px;background-color:var(--md-sys-color-surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);border-right:1px solid var(--border-color);display:flex;flex-direction:column;padding:28px 18px;justify-content:space-between;box-shadow:var(--glass-shadow);z-index:11;transition:all var(--md-transition-duration) var(--md-transition-easing)}.sidebar-logo{display:flex;align-items:center;gap:12px;padding:0 12px 28px}.sidebar-logo-icon{color:var(--md-sys-color-primary);width:26px;height:26px;filter:drop-shadow(0 2px 8px rgba(99,102,241,.3))}.sidebar-logo-text{font-size:1.15rem;font-weight:700;letter-spacing:-.02em;color:var(--md-sys-color-on-background)}.nav-list{display:flex;flex-direction:column;gap:6px;margin-bottom:auto}.nav-item{display:flex;align-items:center;gap:12px;padding:10px 16px;height:48px;border-radius:var(--md-shape-corner-medium);color:var(--md-sys-color-on-surface-variant);font-size:.9rem;font-weight:500;cursor:pointer;transition:all var(--md-transition-duration) var(--md-transition-easing);-webkit-user-select:none;user-select:none;text-decoration:none;position:relative}.nav-item:hover{background-color:var(--md-sys-color-surface-variant);color:var(--md-sys-color-primary);transform:translate(2px)}.nav-item.active{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-primary);font-weight:600}.nav-item.active:before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:4px;background-color:var(--md-sys-color-primary);border-radius:0 4px 4px 0}.sidebar-footer{padding:18px 12px 0;border-top:1px solid var(--border-color);font-size:.78rem;color:var(--md-sys-color-on-surface-variant);display:flex;flex-direction:column;gap:6px;opacity:.95}.main-content{flex:1;display:flex;flex-direction:column;background-color:transparent;overflow:hidden}.top-status-bar{height:64px;background-color:var(--md-sys-color-surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;padding:0 24px;z-index:10;box-shadow:var(--glass-shadow)}.current-model-info{display:flex;align-items:center;gap:10px;font-size:.88rem}.status-indicator{width:8px;height:8px;border-radius:50%;background-color:var(--md-sys-color-success);box-shadow:0 0 8px var(--md-sys-color-success)}.status-indicator.busy{background-color:var(--md-sys-color-primary);box-shadow:0 0 8px var(--md-sys-color-primary);animation:pulse 1.8s infinite}.status-indicator.offline{background-color:var(--md-sys-color-on-surface-variant);box-shadow:none;opacity:.6}.telemetry-group{display:flex;align-items:center;gap:8px}.telemetry-chip{display:flex;align-items:center;gap:6px;background-color:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);border-radius:var(--md-shape-corner-medium);padding:6px 12px;font-size:.8rem;font-weight:500;color:var(--md-sys-color-on-surface-variant);transition:all var(--md-transition-duration)}.telemetry-chip-icon{width:14px;height:14px}.readiness-card{margin:0 0 18px;border-left:4px solid var(--md-sys-color-primary)}.readiness-card-warning{border-left-color:var(--md-sys-color-error)}.readiness-header{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.readiness-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}.readiness-actions .m3-btn{height:34px;padding:0 14px}.readiness-issues,.readiness-cleanup-list{display:flex;flex-direction:column;gap:6px;margin-top:12px;font-size:.82rem;color:var(--md-sys-color-on-surface-variant)}.readiness-issues span{color:var(--md-sys-color-error)}.theme-toggle-btn{background:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);color:var(--md-sys-color-on-surface-variant);cursor:pointer;display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--md-shape-corner-medium);transition:all var(--md-transition-duration) var(--md-transition-easing);margin-right:6px}.theme-toggle-btn:hover{color:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary);background-color:var(--md-sys-color-primary-container);transform:scale(1.05)}.workspace-area{flex:1;overflow-y:auto;padding:28px}.workspace-title-section{margin-bottom:24px}.workspace-title{font-size:1.6rem;font-weight:700;color:var(--md-sys-color-on-background);letter-spacing:-.02em}.workspace-subtitle{font-size:.9rem;color:var(--md-sys-color-on-surface-variant);margin-top:4px;opacity:.8}.m3-card{background-color:var(--md-sys-color-surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);border:1px solid var(--md-sys-color-card-border);border-radius:var(--md-shape-corner-large);padding:24px;margin-bottom:24px;box-shadow:var(--glass-shadow);transition:all var(--md-transition-duration) var(--md-transition-easing)}.m3-card:hover{border-color:#6366f126}.m3-card-title{font-size:1.05rem;font-weight:600;margin-bottom:20px;color:var(--md-sys-color-on-surface);display:flex;align-items:center;gap:8px}.m3-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:38px;padding:0 20px;border-radius:var(--md-shape-corner-medium);font-size:.88rem;font-weight:550;font-family:inherit;border:none;cursor:pointer;transition:all var(--md-transition-duration) var(--md-transition-easing);-webkit-user-select:none;user-select:none;line-height:1;white-space:nowrap}.m3-btn svg{flex:0 0 auto}.m3-btn .progress-spinner{width:14px;height:14px;border-width:2px;border-color:currentColor;border-right-color:transparent;border-bottom-color:transparent}.m3-btn:disabled{opacity:.45;cursor:not-allowed}.m3-btn-filled{background:linear-gradient(135deg,var(--md-sys-color-primary),#8b5cf6);color:var(--md-sys-color-on-primary);box-shadow:0 4px 12px #6366f133}.m3-btn-filled:hover:not(:disabled){box-shadow:0 6px 20px #6366f159;transform:translateY(-1px);filter:brightness(1.04)}.m3-btn-tonal{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.m3-btn-tonal:hover:not(:disabled){background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);transform:translateY(-1px)}.m3-btn-outlined{background-color:transparent;border:1px solid var(--border-color);color:var(--md-sys-color-on-surface)}.m3-btn-outlined:hover:not(:disabled){background-color:var(--md-sys-color-surface-variant);border-color:var(--md-sys-color-primary);color:var(--md-sys-color-primary)}.m3-btn-error{background-color:#f43f5e14;border:1px solid rgba(244,63,94,.18);color:#e11d48;box-shadow:0 2px 8px #f43f5e0d}.m3-btn-error:hover:not(:disabled){background-color:#f43f5e26;border-color:#f43f5e4d;transform:translateY(-1px)}html[data-theme=dark] .m3-btn-error{background-color:#fb718517;border:1px solid rgba(251,113,133,.22);color:#fda4af;box-shadow:0 2px 8px #fb71850a}html[data-theme=dark] .m3-btn-error:hover:not(:disabled){background-color:#fb718529;border-color:#fb718559}.m3-field-group{display:flex;flex-direction:column;gap:18px}.m3-text-field{display:flex;flex-direction:column;gap:6px}.m3-text-field-label{font-size:.82rem;font-weight:600;letter-spacing:.01em;color:var(--md-sys-color-on-surface-variant)}.m3-input,.m3-textarea{width:100%;background-color:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);border-radius:var(--md-shape-corner-medium);padding:10px 14px;font-family:inherit;font-size:.92rem;color:var(--md-sys-color-on-surface);transition:all var(--md-transition-duration) var(--md-transition-easing);outline:none}.m3-input{height:42px}.m3-textarea{min-height:90px;resize:vertical}.m3-input:focus,.m3-textarea:focus{border-color:var(--md-sys-color-primary);background-color:var(--md-sys-color-surface);box-shadow:0 0 0 3px #6366f11f}.m3-slider-group{display:flex;flex-direction:column;gap:4px}.m3-slider-header{display:flex;justify-content:space-between;align-items:center}.m3-slider-label{font-size:.82rem;font-weight:600;color:var(--md-sys-color-on-surface-variant)}.m3-slider-value{font-size:.78rem;font-weight:700;color:var(--md-sys-color-primary);background-color:var(--md-sys-color-primary-container);padding:2px 8px;border-radius:var(--md-shape-corner-full)}.m3-slider{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:var(--md-shape-corner-full);background:var(--border-color);outline:none;margin:12px 0;transition:background .25s}.m3-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:var(--md-sys-color-primary);border:2px solid var(--md-sys-color-surface);box-shadow:0 0 8px #6366f159;cursor:pointer;transition:all var(--md-transition-duration) var(--md-transition-easing)}.m3-slider::-webkit-slider-thumb:hover{transform:scale(1.25);box-shadow:0 0 12px #6366f199}.m3-switch-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0}.m3-switch-info{display:flex;flex-direction:column;gap:2px}.m3-switch-title{font-size:.9rem;font-weight:600;color:var(--md-sys-color-on-surface)}.m3-switch-description{font-size:.78rem;color:var(--md-sys-color-on-surface-variant);opacity:.8}.m3-switch{position:relative;display:inline-block;width:46px;height:26px}.m3-switch input{opacity:0;width:0;height:0}.m3-switch-slider{position:absolute;cursor:pointer;inset:0;background-color:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);border-radius:26px;transition:.25s}.m3-switch-slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--md-sys-color-on-surface-variant);border-radius:50%;transition:.25s}.m3-switch input:checked+.m3-switch-slider{background-color:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary)}.m3-switch input:checked+.m3-switch-slider:before{transform:translate(20px);background-color:var(--md-sys-color-on-primary)}.m3-segmented-button{display:flex;background-color:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);border-radius:var(--md-shape-corner-medium);padding:3px;overflow:hidden;width:100%}.m3-segment-item{flex:1;text-align:center;padding:8px 0;font-size:.82rem;font-weight:600;cursor:pointer;background-color:transparent;color:var(--md-sys-color-on-surface-variant);transition:all var(--md-transition-duration) var(--md-transition-easing);border-radius:calc(var(--md-shape-corner-medium) - 2px);-webkit-user-select:none;user-select:none;border:none}.m3-segment-item:hover:not(.active):not(.disabled){color:var(--md-sys-color-on-surface);background-color:#00000008}html[data-theme=dark] .m3-segment-item:hover:not(.active):not(.disabled){background-color:#ffffff0a}.m3-segment-item.active{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);box-shadow:0 2px 8px #6366f133}.m3-segment-item.disabled{opacity:.45;cursor:not-allowed;text-decoration:line-through}.generator-layout{display:grid;grid-template-columns:1fr;gap:24px}@media(min-width:1000px){.generator-layout{grid-template-columns:1.15fr .85fr}}.chips-container{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.status-chip{display:inline-flex;align-items:center;gap:6px;background-color:var(--md-sys-color-surface-variant);border:1px solid var(--border-color);color:var(--md-sys-color-on-surface-variant);padding:5px 12px;border-radius:var(--md-shape-corner-full);font-size:.78rem;font-weight:550;cursor:pointer;transition:all var(--md-transition-duration)}.status-chip:hover{background-color:var(--md-sys-color-primary-container);border-color:var(--md-sys-color-primary);color:var(--md-sys-color-primary)}.prompt-actions{display:flex;justify-content:space-between;align-items:center;margin-top:16px}.aspect-ratio-btn{position:relative;transition:all var(--md-transition-duration) var(--md-transition-easing)}.aspect-ratio-btn.active{background-color:var(--md-sys-color-primary-container)!important;border-color:var(--md-sys-color-primary)!important;color:var(--md-sys-color-primary)!important;box-shadow:0 0 0 2px #6366f126}.aspect-ratio-preview{border:1.5px solid currentColor;border-radius:2px;opacity:.6;background-color:transparent;transition:all .2s ease;margin-bottom:6px}.aspect-ratio-btn:hover .aspect-ratio-preview,.aspect-ratio-btn.active .aspect-ratio-preview{opacity:1;background-color:#6366f10f}.ratio-1-1{width:16px;height:16px}.ratio-4-3{width:20px;height:15px}.ratio-16-9{width:24px;height:13.5px}.ratio-9-16{width:13.5px;height:24px}.output-panel{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:380px}.output-image-container{width:100%;aspect-ratio:1;background-color:var(--md-sys-color-surface-variant);border-radius:var(--md-shape-corner-large);border:1px solid var(--border-color);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;box-shadow:inset 0 2px 8px #0000000d}.output-image{width:100%;height:100%;object-fit:contain;transition:transform .3s}.output-image:hover{transform:scale(1.015)}.progress-overlay{position:absolute;inset:0;background-color:#090a0fd9;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;gap:14px;padding:24px;z-index:5}.progress-spinner{width:44px;height:44px;border:3px solid rgba(255,255,255,.15);border-top-color:var(--md-sys-color-primary);border-radius:50%;animation:spin .9s cubic-bezier(.5,.1,.4,.9) infinite;box-shadow:0 0 12px #6366f133}.loading-dot{width:8px;height:8px;border-radius:50%;background:var(--md-sys-color-primary);opacity:.9;animation:pulse 1.2s ease-in-out infinite;box-shadow:0 0 8px var(--md-sys-color-primary)}.progress-text{font-size:1.05rem;font-weight:700;letter-spacing:-.01em}.progress-bar-container{width:80%;height:5px;background-color:#ffffff1f;border-radius:var(--md-shape-corner-full);overflow:hidden;margin:4px 0}.progress-bar-fill{height:100%;background:linear-gradient(90deg,var(--md-sys-color-primary),#d946ef);width:0%;transition:width .4s cubic-bezier(.1,.8,.25,1);box-shadow:0 0 8px #6366f166}.output-placeholder-icon{width:48px;height:48px;color:var(--md-sys-color-on-surface-variant);opacity:.5;filter:drop-shadow(0 2px 10px rgba(0,0,0,.05))}.output-placeholder-text{font-size:.88rem;color:var(--md-sys-color-on-surface-variant);margin-top:12px;text-align:center;line-height:1.5;opacity:.8}.image-actions{display:flex;gap:10px;margin-top:16px;width:100%}.image-actions button{flex:1}.models-grid{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:800px){.models-grid{grid-template-columns:1fr 1fr}}.model-card{display:flex;flex-direction:column;justify-content:space-between;border:1px solid var(--border-color-muted)}.model-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}.model-name{font-size:1.05rem;font-weight:600}.model-badge{font-size:.72rem;font-weight:700;padding:3px 8px;border-radius:var(--md-shape-corner-full)}.model-badge-gguf{background-color:#f59e0b26;color:#d97706}.model-badge-safetensors{background-color:#0ea5e926;color:#0284c7}.model-description{font-size:.82rem;color:var(--md-sys-color-on-surface-variant);margin-bottom:16px;line-height:1.45;opacity:.9}.model-status-info{margin-top:auto;padding-top:12px;border-top:1px solid var(--border-color-muted);display:flex;align-items:center;justify-content:space-between}.model-size{font-size:.8rem;font-weight:550;color:var(--md-sys-color-on-surface-variant)}.model-progress-section{width:100%;margin-top:12px}.model-progress-label{display:flex;justify-content:space-between;font-size:.78rem;font-weight:600;margin-bottom:4px}.model-progress-bar{width:100%;height:6px;background-color:var(--border-color-muted);border-radius:var(--md-shape-corner-full);overflow:hidden}.model-progress-fill{height:100%;background:linear-gradient(90deg,var(--md-sys-color-primary),var(--md-sys-color-secondary));width:0%;transition:width .25s ease}.import-box{border:2px dashed var(--border-color);border-radius:var(--md-shape-corner-large);padding:32px;text-align:center;cursor:pointer;transition:all var(--md-transition-duration);background-color:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface-variant);display:flex;flex-direction:column;align-items:center;gap:10px}.import-box:hover{background-color:var(--md-sys-color-primary-container);border-color:var(--md-sys-color-primary);color:var(--md-sys-color-primary)}.import-icon{width:40px;height:40px;color:var(--md-sys-color-primary);filter:drop-shadow(0 2px 6px rgba(99,102,241,.15))}.gallery-section{margin-top:36px}.gallery-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.gallery-selection-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.gallery-selection-actions .m3-btn{height:34px;padding:0 14px}.gallery-selection-count{font-size:.82rem;font-weight:700;color:var(--md-sys-color-primary);padding:7px 10px;border-radius:var(--md-shape-corner-full);background:var(--md-sys-color-primary-container)}.gallery-hint{margin-top:8px;font-size:.78rem;color:var(--md-sys-color-outline)}.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;margin-top:16px}.gallery-item{aspect-ratio:1;border-radius:var(--md-shape-corner-medium);overflow:hidden;border:1px solid var(--border-color-muted);cursor:pointer;position:relative;contain:layout paint style;transition:border-color var(--md-transition-duration),box-shadow var(--md-transition-duration),transform var(--md-transition-duration)}.gallery-item.selected{border-color:var(--md-sys-color-primary);box-shadow:0 0 0 3px #4f46e540}.gallery-thumb{width:100%;height:100%;object-fit:cover;transition:transform var(--md-transition-duration) var(--md-transition-easing)}.gallery-item:hover .gallery-thumb{transform:scale(1.05)}.gallery-select-badge{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;color:var(--md-sys-color-on-primary);background:var(--md-sys-color-primary);box-shadow:0 2px 8px #00000047}.app-dialog-backdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:#080c18a3;-webkit-backdrop-filter:blur(4px) saturate(.86);backdrop-filter:blur(4px) saturate(.86)}.app-dialog{width:min(460px,100%);background:color-mix(in srgb,var(--md-sys-color-surface) 96%,white 4%);color:var(--md-sys-color-on-surface);border:1px solid color-mix(in srgb,var(--md-sys-color-outline) 55%,transparent);border-radius:var(--md-shape-corner-large);box-shadow:0 22px 60px #0000006b,0 0 0 1px #ffffff14 inset;padding:24px}.app-dialog-title{font-size:1.15rem;font-weight:800;margin:0 0 10px}.app-dialog-message{font-size:.92rem;line-height:1.45;color:var(--md-sys-color-on-surface-variant);margin:0;white-space:pre-wrap}.app-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:22px;flex-wrap:wrap}.gallery-empty{text-align:center;padding:36px;color:var(--md-sys-color-on-surface-variant);font-size:.85rem;opacity:.8}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(1.06)}}.sidebar-specs-header{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-weight:600;color:var(--md-sys-color-on-background)}.sidebar-specs-item{text-overflow:ellipsis;overflow:hidden;white-space:nowrap;opacity:.85;margin-bottom:2px;color:var(--md-sys-color-on-background)}.sidebar-specs-os{color:var(--md-sys-color-primary);font-weight:600;margin-top:4px} diff --git a/app/dist/index.html b/app/dist/index.html deleted file mode 100644 index eee907d..0000000 --- a/app/dist/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - Local AI Image Generator - - - - - -
- - From d4cdc6ae284ce00e8d1682978fb01e23ebc6a5ff Mon Sep 17 00:00:00 2001 From: techjarves Date: Sat, 13 Jun 2026 15:36:43 +0530 Subject: [PATCH 4/8] feat: add OpenVINO NPU worker support and frontend UI components for model management and image generation --- README.md | 5 + app/frontend/src/App.jsx | 26 +- app/frontend/src/components/Generator.jsx | 5 +- .../src/components/ImageConstraints.jsx | 130 ++++- app/frontend/src/components/ModelManager.jsx | 66 ++- app/frontend/src/services/api.js | 97 +++- linux.sh | 21 +- .../openvino_npu_worker.cpython-313.pyc | Bin 0 -> 12022 bytes scripts/openvino_npu_worker.py | 187 +++++++ scripts/serve.cjs | 491 +++++++++++++++++- scripts/setup-openvino-npu.ps1 | 46 ++ scripts/setup-openvino-npu.sh | 85 +++ scripts/setup.ps1 | 14 +- 13 files changed, 1125 insertions(+), 48 deletions(-) create mode 100644 scripts/__pycache__/openvino_npu_worker.cpython-313.pyc create mode 100644 scripts/openvino_npu_worker.py create mode 100644 scripts/setup-openvino-npu.ps1 create mode 100644 scripts/setup-openvino-npu.sh diff --git a/README.md b/README.md index 4b69955..91bd63b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ 1. **Check compatibility:** Prebuilt Linux backends are built on Ubuntu 24.04 and require **glibc 2.38+**. Run `ldd --version` to verify. 2. **Launch:** Open a terminal in the project folder and run **`./linux.sh`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run). - For maximum AMD GPU performance, use **`./linux.sh --max-perf`** on first setup (adds the ROCm backend). + - For Intel Core Ultra NPU support, install the Intel Linux NPU driver, then run **`./linux.sh --setup-openvino`**. 3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). 4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. @@ -100,6 +101,7 @@ local-ai-image-generator/ | **NVIDIA** | Vulkan | CPU | No reliable prebuilt Linux CUDA binary is currently available. NVIDIA GPUs use Vulkan. | | **AMD Radeon** | ROCm | Vulkan | ROCm provides best AMD performance when host ROCm drivers are available. | | **Intel Arc / integrated** | Vulkan | CPU | Cross-vendor Vulkan support. | +| **Intel Core Ultra NPU** | OpenVINO NPU | CPU | Requires the Intel Linux NPU driver, kernel 6.6+, Python 3, and `./linux.sh --setup-openvino`. | | **Integrated / None** | CPU | — | Runs on logical CPU threads (slow). | ### macOS @@ -110,13 +112,16 @@ local-ai-image-generator/ | **Intel Mac** | Source build required | CPU | Official prebuilt macOS backend is Apple Silicon only. | **System requirements:** +- **64-bit Windows 10 or Windows 11** is required for the portable Node.js 22 runtime used by the Windows launcher. - **glibc 2.38 or newer** is required for the prebuilt Linux backends (Ubuntu 24.04, Fedora 40+, etc.). - The setup script will warn you if your glibc is older. You can still run setup, but the prebuilt backends will not start. +- **Linux OpenVINO NPU:** Intel Core Ultra, x86_64 Linux, kernel 6.6+, a working `/dev/accel/accel0` device, Python 3 with `venv`, and the Intel Linux NPU driver are required. - **Apple Silicon (M1 or newer)** is required for the prebuilt macOS Metal backend. **Linux setup modes:** - **Default (`./linux.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB). - **Maximum Performance (`./linux.sh --max-perf`)**: Also downloads the ROCm backend. Total download ~1.3 GB. +- **Intel NPU (`./linux.sh --setup-openvino`)**: Creates a local OpenVINO Python environment and verifies that the Intel NPU driver exposes an `NPU` device. --- diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx index 817bfb1..5063f1f 100644 --- a/app/frontend/src/App.jsx +++ b/app/frontend/src/App.jsx @@ -97,6 +97,8 @@ function App() { width: 1024, height: 1024, steps: 4, // Recommended default steps for Flux.1 Schnell/Lightning + npuSteps: 4, + standardSteps: 20, cfgScale: 1.0, // Recommended default CFG for Flux.1 Schnell sampler: "euler_a", seed: -1, @@ -277,10 +279,22 @@ function App() { useEffect(() => { if (activeModel) { const name = activeModel.toLowerCase(); - if (name.includes("flux") || name.includes("schnell")) { + if (name.includes("lcm-dreamshaper-v7-fp16")) { setConstraints((prev) => ({ ...prev, steps: 4, + npuSteps: 4, + cfgScale: 1.0, + width: 512, + height: 512, + backendType: "openvino-npu", + useGpu: true, + })); + } else if (name.includes("flux") || name.includes("schnell")) { + setConstraints((prev) => ({ + ...prev, + steps: 4, + standardSteps: 4, cfgScale: 1.0, width: 1024, height: 1024, @@ -289,6 +303,7 @@ function App() { setConstraints((prev) => ({ ...prev, steps: 4, + standardSteps: 4, cfgScale: 1.5, width: 1024, height: 1024, @@ -297,6 +312,7 @@ function App() { setConstraints((prev) => ({ ...prev, steps: 25, + standardSteps: 25, cfgScale: 7.0, width: 512, height: 512, @@ -305,6 +321,7 @@ function App() { setConstraints((prev) => ({ ...prev, steps: 20, + standardSteps: 20, cfgScale: 4.5, width: 1024, height: 1024, @@ -314,6 +331,7 @@ function App() { setConstraints((prev) => ({ ...prev, steps: 20, + standardSteps: 20, cfgScale: 7.0, width: 512, height: 512, @@ -353,6 +371,7 @@ function App() { serverRunning={serverRunning} setServerRunning={setServerRunning} constraints={constraints} + backendOptions={backendOptions} showAlert={showAlert} showConfirm={showConfirm} /> @@ -365,6 +384,11 @@ function App() { activeModel={activeModel} specs={specs} backendOptions={backendOptions} + serverRunning={serverRunning} + setServerRunning={setServerRunning} + setActiveModel={setActiveModel} + showAlert={showAlert} + showConfirm={showConfirm} /> ); default: diff --git a/app/frontend/src/components/Generator.jsx b/app/frontend/src/components/Generator.jsx index e9e8e68..8fc9048 100644 --- a/app/frontend/src/components/Generator.jsx +++ b/app/frontend/src/components/Generator.jsx @@ -98,6 +98,8 @@ function Generator({ settings.sampler !== constraints.sampler || parseInt(settings.threads) !== parseInt(constraints.threads) || Boolean(settings.useGpu) !== (constraints.useGpu !== false) || + parseInt(settings.width || 512) !== parseInt(constraints.width || 512) || + parseInt(settings.height || 512) !== parseInt(constraints.height || 512) || (settings.backendType || (settings.useGpu === false ? "cpu" : "auto")) !== (constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"))) { needsRestart = true; } @@ -115,7 +117,8 @@ function Generator({ let isReady = false; let crashError = null; - for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms) + const maxStartupPolls = constraints.backendType === "openvino-npu" ? 1200 : 240; + for (let i = 0; i < maxStartupPolls; i++) { const status = await getBackendStatus(); if (status.loading) { setRestartLoadProgress({ diff --git a/app/frontend/src/components/ImageConstraints.jsx b/app/frontend/src/components/ImageConstraints.jsx index 8633db3..5f23b0d 100644 --- a/app/frontend/src/components/ImageConstraints.jsx +++ b/app/frontend/src/components/ImageConstraints.jsx @@ -1,5 +1,6 @@ -import React, { memo } from "react"; +import React, { memo, useEffect } from "react"; import { Crop, Sliders, Cpu, Info } from "lucide-react"; +import { stopServer } from "../services/api"; const ASPECT_RATIOS = [ { id: "1:1", label: "1:1 Square", width: 512, height: 512, sdxl_width: 1024, sdxl_height: 1024, desc: "Social posts & avatars" }, @@ -17,26 +18,55 @@ const isSD15OrCustomModel = (modelName) => { return true; }; -function ImageConstraints({ constraints, setConstraints, activeModel, specs, backendOptions }) { +function ImageConstraints({ + constraints, + setConstraints, + activeModel, + specs, + backendOptions, + serverRunning, + setServerRunning, + setActiveModel, + showAlert = async ({ message }) => window.alert(message), + showConfirm = async ({ message }) => window.confirm(message), +}) { const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false; + const isOpenVinoNpu = constraints.backendType === "openvino-npu"; + const forceStandardMode = isSD15OrCustom && !isOpenVinoNpu; const availableBackends = backendOptions?.options?.length ? backendOptions.options : [{ id: "cpu", label: "CPU", available: true }]; + useEffect(() => { + if (isOpenVinoNpu && constraints.steps > 8) { + setConstraints((prev) => ({ ...prev, steps: 8, npuSteps: 8 })); + } + }, [constraints.steps, isOpenVinoNpu, setConstraints]); + const updateConstraint = (key, value) => { setConstraints((prev) => ({ ...prev, - [key]: value + [key]: value, + ...(key === "steps" + ? isOpenVinoNpu + ? { npuSteps: value } + : { standardSteps: value } + : {}), })); }; const handleAspectRatioChange = (ratio, sizeType) => { + if (isOpenVinoNpu && ratio !== "1:1") return; const isSDXL = sizeType === "sdxl" && !isSD15OrCustom; const selected = ASPECT_RATIOS.find((r) => r.id === ratio); if (selected) { let w = isSDXL ? selected.sdxl_width : selected.width; let h = isSDXL ? selected.sdxl_height : selected.height; - if (isSD15OrCustom) { + if (isOpenVinoNpu) { + const size = constraints.width >= 1024 ? 1024 : 512; + w = size; + h = size; + } else if (isSD15OrCustom) { if (w > h) { h = Math.round((h * 512) / w); w = 512; @@ -52,11 +82,55 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac } }; - const handleBackendChange = (backendType) => { + const handleBackendChange = async (backendType) => { + const currentBackend = constraints.backendType || "cpu"; + if (backendType === currentBackend) return; + + const isGpuBackend = (type) => !["cpu", "openvino-npu"].includes(type); + const switchesAccelerator = + (currentBackend === "openvino-npu" && isGpuBackend(backendType)) || + (isGpuBackend(currentBackend) && backendType === "openvino-npu"); + + if (serverRunning && switchesAccelerator) { + const leavingNpu = currentBackend === "openvino-npu"; + const confirmed = await showConfirm({ + title: leavingNpu ? "Unload NPU Model?" : "Unload GPU Model?", + message: leavingNpu + ? "The OpenVINO NPU model must be unloaded before switching to the GPU backend." + : "The GPU model must be unloaded before switching to the OpenVINO NPU backend.", + confirmLabel: "Unload", + cancelLabel: "Cancel", + danger: true, + }); + if (!confirmed) return; + + try { + await stopServer(); + setServerRunning(false); + setActiveModel(null); + } catch (err) { + await showAlert({ + title: "Unload Failed", + message: err.message || String(err), + danger: true, + }); + return; + } + } + setConstraints((prev) => ({ ...prev, backendType, useGpu: backendType !== "cpu", + steps: backendType === "openvino-npu" + ? Math.max(1, Math.min(8, prev.npuSteps || 4)) + : Math.max(1, Math.min(60, prev.standardSteps || 20)), + ...(backendType === "openvino-npu" + ? { + width: prev.width >= 1024 ? 1024 : 512, + height: prev.width >= 1024 ? 1024 : 512, + } + : {}), })); }; @@ -79,7 +153,25 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac 1. Picture Size & Shape - {isSD15OrCustom && ( + {isOpenVinoNpu ? ( +
+ +
+ OpenVINO NPU Resolution: The NPU generates at its stable 512x512 resolution. HD mode produces a 1024x1024 output using high-quality Lanczos upscaling without recompiling the model. +
+
+ ) : isSD15OrCustom && (
- {isSD15OrCustom ? ( + {forceStandardMode ? (
- High Quality Mode (1024px) + {isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
) : (
- High Quality Mode (1024px) + {isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
)}
{ updateConstraint("width", 512); updateConstraint("height", 512); @@ -139,7 +231,9 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
- {isSD15OrCustom + {isOpenVinoNpu + ? "*HD mode generates at 512x512 on the NPU, then upscales to 1024x1024. It improves output size, but does not add native diffusion detail." + : isSD15OrCustom ? "*Standard Speed Mode (512px) is forced for SD 1.5 / custom models to ensure stable local generations." : "*Use High Quality (1024px) for Flux or SDXL models. Use Standard (512px) for SD 1.5."} @@ -151,10 +245,12 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
{ASPECT_RATIOS.map((ratio) => { const isActive = Math.abs((constraints.width / constraints.height) - (ratio.width / ratio.height)) < 0.1; + const isUnsupportedOpenVinoShape = isOpenVinoNpu && ratio.id !== "1:1"; return (
diff --git a/app/frontend/src/components/ModelManager.jsx b/app/frontend/src/components/ModelManager.jsx index fa20bc8..bb78cc1 100644 --- a/app/frontend/src/components/ModelManager.jsx +++ b/app/frontend/src/components/ModelManager.jsx @@ -1,6 +1,6 @@ import React, { memo, useState, useEffect } from "react"; import { FolderOpen, DownloadCloud, RefreshCw, Database, Trash2, Square, HardDrive, Library, AlertTriangle } from "lucide-react"; -import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api"; +import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, downloadOpenVinoModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api"; const MODEL_LIBRARY = [ { @@ -60,7 +60,26 @@ const MODEL_LIBRARY = [ }, ]; -function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) { +const OPENVINO_MODEL_LIBRARY = [ + { + group: "Intel NPU - OpenVINO Test", + items: [ + { + id: "lcm-dreamshaper-v7-fp16", + name: "LCM DreamShaper v7 FP16", + filename: "lcm-dreamshaper-v7-fp16", + format: "OpenVINO", + backendType: "openvino-npu", + approxSize: "2.0 GB", + resolution: "512x512", + notes: "NPU test model for Windows and Linux. Runs the text encoder on CPU, UNet on Intel NPU, and VAE decoder on Intel GPU or CPU fallback.", + url: "https://huggingface.co/OpenVINO/LCM_Dreamshaper_v7-fp16-ov", + }, + ], + }, +]; + +function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) { const [localModels, setLocalModels] = useState([]); const [downloadingModelId, setDownloadingModelId] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); @@ -78,6 +97,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun const modelNames = localModels.map((model) => normalizeModel(model).filename); const isBusy = loadingModelId !== null || isUnloading; + const openvinoSupported = Boolean(backendOptions?.openvinoNpu?.supported); + const visibleModelLibrary = openvinoSupported ? [...MODEL_LIBRARY, ...OPENVINO_MODEL_LIBRARY] : MODEL_LIBRARY; + const getLocalModelInfo = (modelId) => localModels.map(normalizeModel).find((model) => model.filename === modelId); useEffect(() => { fetchModels(); @@ -217,7 +239,21 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun setDownloadUrl(""); }; - const handleLibraryDownload = (model) => { + const handleLibraryDownload = async (model) => { + if (model.backendType === "openvino-npu") { + if (downloadingModelId) return; + try { + const res = await downloadOpenVinoModel(model.id); + if (res.ok) { + startProgressPolling(model.filename); + } else { + showAlert({ title: "Download Failed", message: res.error || "Unknown error", danger: true }); + } + } catch (err) { + showAlert({ title: "Download Error", message: err.message, danger: true }); + } + return; + } downloadByUrl(model.url, model.filename); }; @@ -246,7 +282,11 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun if (!isTauriDesktop && !isLocalServerMode) { const isAlreadyRunning = await pingServer(); - const cmd = `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`; + const modelInfo = getLocalModelInfo(modelId); + const isOpenVinoModel = modelInfo?.backendType === "openvino-npu"; + const cmd = isOpenVinoModel + ? `Run the platform launcher and load "${modelId}" from Model Manager on a configured Intel NPU machine.` + : `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`; if (isAlreadyRunning) { if (await showConfirm({ title: "Web Browser Mode", message: `To load "${modelId}" on the active GPU server, restart your terminal backend with:\n\n${cmd}\n\nUpdate the UI status anyway?`, confirmLabel: "Update UI" })) { @@ -275,7 +315,19 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun device: "", }); try { - const response = await startServer(modelId, constraints); + const modelInfo = getLocalModelInfo(modelId); + const loadConstraints = modelInfo?.backendType === "openvino-npu" + ? { + ...constraints, + backendType: "openvino-npu", + useGpu: true, + width: constraints.width >= 1024 ? 1024 : 512, + height: constraints.height >= 1024 ? 1024 : 512, + steps: Math.max(1, Math.min(8, constraints.steps || 4)), + cfgScale: constraints.cfgScale || 1, + } + : constraints; + const response = await startServer(modelId, loadConstraints); console.log(response); let isReady = false; @@ -601,7 +653,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
{filename} - Local Weights File • {model.size || formatBytes(model.sizeBytes)} + {model.backendType === "openvino-npu" ? "OpenVINO NPU Model" : model.format || "Local Weights File"} • {model.size || formatBytes(model.sizeBytes)}
@@ -654,7 +706,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
- {MODEL_LIBRARY.map((section) => ( + {visibleModelLibrary.map((section) => (

{section.group}

diff --git a/app/frontend/src/services/api.js b/app/frontend/src/services/api.js index 035982b..88ea70e 100644 --- a/app/frontend/src/services/api.js +++ b/app/frontend/src/services/api.js @@ -48,8 +48,12 @@ export function normalizeModel(model) { } return { filename: model?.filename || model?.name || "", + name: model?.name || model?.filename || "", sizeBytes: Number(model?.sizeBytes || 0), size: model?.size || (model?.sizeBytes ? formatBytes(model.sizeBytes) : "Unknown"), + format: model?.format || "Local Weights File", + backendType: model?.backendType || "", + resolution: model?.resolution || "", }; } @@ -254,6 +258,8 @@ export async function startServer(modelPath, constraints) { threads: constraints.threads || 8, use_gpu: constraints.useGpu !== false, backend_type: constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"), + width: constraints.width || 512, + height: constraints.height || 512, vae_tiling: constraints.vaeTiling !== false, vae_on_cpu: constraints.vaeOnCpu === true, }), @@ -330,14 +336,34 @@ export async function deleteGeneratedOutputs(outputs) { // List model files from the models folder (via management API in web mode) export async function listModelsFromDisk() { try { - const r = await fetch("/api/models"); - const data = await r.json(); - return (data.models || []).map(normalizeModel); + const [modelRes, openvino] = await Promise.all([ + fetch("/api/models"), + listOpenVinoModels().catch(() => ({ supported: false, models: [] })), + ]); + const data = await modelRes.json(); + const normalModels = (data.models || []).map(normalizeModel); + const openvinoModels = openvino.supported + ? (openvino.models || []).filter((model) => model.installed).map((model) => normalizeModel({ + filename: model.id, + name: model.name, + sizeBytes: model.sizeBytes, + size: model.size, + format: "OpenVINO", + backendType: "openvino-npu", + resolution: model.resolution, + })) + : []; + return [...normalModels, ...openvinoModels]; } catch (_) { return []; } } +export async function listOpenVinoModels() { + const res = await fetch("/api/openvino-models"); + return await readJsonResponse(res, "The local server returned invalid OpenVINO model data."); +} + // Generate image (T2I / I2I) // Handles API calls to sd-server. If the server is unreachable or returns an error, // we surface that error to the UI instead of silently returning a fake placeholder. @@ -345,8 +371,6 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM console.log("Initiating image generation:", { prompt, negativePrompt, constraints, activeModelName }); const startTime = Date.now(); - const baseUrl = await getBackendBaseUrl(); - // Prepare payload based on standard stable-diffusion.cpp REST endpoint schemas const payload = { prompt: prompt, @@ -361,6 +385,45 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM denoising_strength: constraints.denoisingStrength || 0.7, }; + if (constraints.backendType === "openvino-npu") { + if (inputImageBase64) { + throw new Error("OpenVINO NPU test mode currently supports text-to-image only."); + } + const response = await fetch("/api/openvino-generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + signal, + body: JSON.stringify({ + prompt: payload.prompt, + negative_prompt: payload.negative_prompt, + steps: payload.steps, + cfg_scale: payload.cfg_scale, + seed: payload.seed, + width: payload.width, + height: payload.height, + }), + }); + const data = await readJsonResponse(response, "The local server returned an invalid OpenVINO generation response."); + const imgB64 = data?.data?.[0]?.b64_json; + if (!imgB64) throw new Error("OpenVINO generation did not return an image."); + const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, ""); + const header = atob(normalizedB64.slice(0, 24)); + const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG"; + const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff; + const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP"; + if (!isPng && !isJpeg && !isWebp) { + throw new Error("OpenVINO generation returned an invalid image payload instead of a real PNG/JPEG/WebP."); + } + const durationSec = Number(data.duration_sec) || parseFloat(((Date.now() - startTime) / 1000).toFixed(1)); + return { + image: `data:image/png;base64,${normalizedB64}`, + seed: data.data?.[0]?.seed ?? payload.seed, + duration_sec: durationSec, + }; + } + + const baseUrl = await getBackendBaseUrl(); + // txt2img uses /v1/images/generations; img2img uses /sdapi/v1/img2img. const isImg2Img = !!payload.image; @@ -416,9 +479,17 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM // Response: { data: [{ b64_json: "..." }] } const imgB64 = data?.data?.[0]?.b64_json ?? data?.images?.[0]; if (imgB64) { + const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, ""); + const header = atob(normalizedB64.slice(0, 24)); + const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG"; + const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff; + const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP"; + if (!isPng && !isJpeg && !isWebp) { + throw new Error("Generation returned an invalid image payload instead of a real PNG/JPEG/WebP."); + } const durationSec = parseFloat(((Date.now() - startTime) / 1000).toFixed(1)); return { - image: `data:image/png;base64,${imgB64}`, + image: `data:image/png;base64,${normalizedB64}`, seed: data.data?.[0]?.seed ?? payload.seed, duration_sec: durationSec, }; @@ -654,6 +725,20 @@ export async function downloadModel(url) { } } +export async function downloadOpenVinoModel(modelId) { + try { + const res = await fetch("/api/download-openvino-model", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model_id: modelId }) + }); + return await readJsonResponse(res, "The local server returned an invalid OpenVINO download response."); + } catch (e) { + console.error("Failed to start OpenVINO model download:", e); + return { ok: false, error: e.message }; + } +} + export async function cancelModelDownload() { try { const res = await fetch("/api/cancel-download", { method: "POST" }); diff --git a/linux.sh b/linux.sh index 5eccac2..f7ee797 100755 --- a/linux.sh +++ b/linux.sh @@ -30,10 +30,27 @@ FRONTEND_PORT="${FRONTEND_PORT:-1420}" SETUP_REASON="" SETUP_MODE="Repair" MAX_PERF_FLAG="" +SETUP_OPENVINO=0 # Parse args -if [[ "${1:-}" == "--max-perf" ]]; then - MAX_PERF_FLAG="--max-perf" +for arg in "$@"; do + case "$arg" in + --max-perf) + MAX_PERF_FLAG="--max-perf" + ;; + --setup-openvino) + SETUP_OPENVINO=1 + ;; + *) + echo "[ERROR] Unknown option: $arg" >&2 + echo "Usage: ./linux.sh [--max-perf] [--setup-openvino]" >&2 + exit 1 + ;; + esac +done + +if [[ $SETUP_OPENVINO -eq 1 ]]; then + bash "$SCRIPT_DIR/scripts/setup-openvino-npu.sh" fi # ── Symlink node_modules to avoid OS conflicts ────────────────────────────── diff --git a/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc b/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ca397fd11e61ed30690c4a1619fbd5210c8cf94 GIT binary patch literal 12022 zcma(%YiwKBdH3>OzDd0$OR^r6EZL@HN`A;M`6=sRTTS4`Z<2}@H~6fNpfJD2i9 zX#%%cI|oZqZEFy_9bkD2V5u=+c`X*D3l^{FFc&CL(o{h1M(qN$*x-M1?ZKHlVEevv zc}be4;~a_SoO{l9p5J%A*EtXEb_)Yx%H}%D)iccR@k0xID)Vp!GVd`0Bd`MuQB#(% z^jk-CH020KQ=agU>IU@v24d(p5~Bm*xdBtZnV4ywAF%XWiIwKr0UNQ?b_JvmQvEB7coaITWU=u7fS9ZEwuy&qe7@vt@PM0+@ zyHnG4Fab8eI_>xId&sF%DvtSN@x;B&7P2)4qV{PYpm57?g&G`yhFbInCh4z^1UUA-dDNz@63$ zJpA;h`3@5<2_|n4EP{#VjDne%0!F7*vA!B6Qz8)}zKD3g5c*Qg)8TP36})=03J1bKpV4G9$=ihp+2XDSfPa2 zFtTG*0Fk0cY+O|IA|YX-m}Y`AVkjtyig8>#7xas=!bgJBqGFLFJ`xefy}pQ3ukf-M zn#k9kjEit!Q0inq3C=|1_VA1-oeN4~uQW63Jx@2jZRVn4@_K_(Fyi$pWm(v0(@Jr5 z@)3wGG0CdNcMFo`)$dx86*cc#wa?2u6qHxdq_PGQdvXJQ+19G-t~r0?{Bi4L{`-Xw zp$lfnpGF9L;l@zZeF3O>kEvtE5QYYK8d+LN_NKbi2+Pz;ETED-0R8)F8D=bvC_yK1 z?lcNA@E&$|YAyxLE}fPaS%HUM0L%w75cpwM&`UZ&4}I$dLmQV(9%G0+8U&+JfLd#{+MrN!^`^rBvZM|A5 zaURaSQEQV)1sk-l$$V=$D6P}-nN+avPGQEuKz|LH8ZDRF$%3cmmuL0bytcDK;eOu1 zoZ22LPWNE~ER<+pS^HX~?R9`@V8%9U5Jtv#kYOY~YfN72A9_mfWL8hbu)eKY`%H>h z8*9s`#PRHmHaP^7MtUNh)GzT(0X!a0? zVAyRA9Oe*Adz&L`s6!xPj59tWN)cHxi5G%$#2cQvk4Njj)NNTmqL{n8x=-|adrrDR zoEZFJX(AYyE6W+#-P_gkjk$`P(q6ZF*xl)XQTT}<$gNG#sdIy(3!hW??l2LR;`Hvp z>Qzo@bI(f1EPE*{<ej zoNye7ISxc^waLP=XpJjg=vu32OjaDf%m2pwD|6KCkDnNi9v7o!6AwAvhLUwgS6s4g zU^Z=ywjPW(9=uw9r}=XK7mns+Wo@$F`M}866kP6kckq#gv6n>4b}n~(boln+=+0x& zlJ2`5_iQ6;w&HaiTYMmSpm$~4I>#2DV85uS{-cq%7py~@;vWpXGnBA7Vm8NO*$N-U z+@ZD7%0y}Fz0%g3-OKz(1-A?0+m6Ibk6!Lc+KPTK_|D*!;wwGZ2Cfb)mffmes$LdW zZTnL-$)a-lU!APn^nn2NbxU>e=6%r(``68kz3Ks@hhc3@ZNs}SC97&-;{7ncXvx-P zK5pCn2-0x+_ATJEBCKk^^kX(sqCi2MRBeY2o>*Xx*+Ay1C{nm zI>kW5$SjeDoMx1}l5biBxoh!y;b6{&FmLmE|7_M5O4Ss4y%RwqM^F<0!R;lejVK%v zG0uw|!q0NA7nFiX&<{F6gapsbf&%CDE-|DUL6DXR%0C*RjwK^!`yNn_S-*LqX`O*& zQKrdCH%;#LJi@}q6^;55wG(Ww=y4SBDG82R29@B*3f}-3jw-B&=>S;_nx09}WhDA; zgo7Y~lx||2+>l@WR%1=edN>K+huP<;l!Cs)kX9${f-$8|+Uby}T$(`9GzscA>dg3U z7A&VN0uOC@!Ae_L1skyi^v(jMcG@>3dM8z#xb0vl?DvJ_!)+-g`FL8YaO&P*T{ICk0yjK~V~Tfn!m4Y^UhQXQyX?VXIeM0D~h$A1Z6$q+W+&xFS>%BUcrUam5Ey_b%? z=_|>wjRGBmhVpkHy2PxNZg{`*uRE^<;-#AtrLI`1YZ0=0C>u@K>SDIKE0I-O6R^&N zttMuxx$;KTb!63cG+hv_?Of@8y1;c){@JeO$WQlw`1Y#pa2j&eR)-w8|DFC+SEHZV z8YxqFiV@(d=n?n_$h?PSi5fso$dEQAgzO`ff_#Q>^TFg2Zax@8#)hV2q6G3X z4c-4W%z?Nu5+ItZ$xB8jC;`ai<>!OEBt|B~A!Bs1ltg6QsuVp~#>3texU%)Di6R3z0PW=wh`>Zclv4tbS8bcpSj>a(11oQ= z+6J>A4nz+Pui8e^C7;>qp2aijJ`~Sy!=#Z<1E!%UHwY~CO}x)weLno?M%jEu+#y!e zx_zKRIT(P_BtzB^8cZwJBjx3U{H?~)Mj+Hv|IgFSG-d|1xXPR`mS$^T*YbycPzT5f zE*Lq(oCUtkkYU1s=k1nW0D2ZoZMtk&r+H}3*bclW6q&q#7lt8d@i+~j8&6S%RMkTD zROeUqU&}10r=W7kqW}I60fBu_iagSk6h1FGfRD|9o*Y@ykpT9E8Z5HL8SnuSFP0$R zR+SuNf2tCVqyV0a@T@dW(2kY1}G#e`5lP9 z1gb79{lUd|E?zkqFKkX0mA!BIE6bH#$?E!RtyfzU)%zF4TT@F@H`|l7jn~Gmj-`^d zTE{~9jr!~L@!B29`lf4dU43htx>-Z+z$Ye=Y05`&mG4gaL@*3n;SAGZyZnteoABRY%;0p6rf)RJ zHJW{91haPHBo=!sXjsuwaE~-=BRpr#rCQspUctlw`I>(gWqIIL+A7y-B>fB5QlY_SjSAEU zvZ2(C3@;~W&{CG_`keV~&|vfQtRW|>cMV!Ho(h0RzLL5cpT=F41_QXuxQWuaOlKMY zuw?R>rb~qCv9v@IHfH5(#CUq6)QnY)Mg=%2F-b=sVTdb%RO z=nHX@Zlkpwc4XRQt-MW}dnV1STyVI6cWFI%%tAw(L9)E??DR*zNN zARp|xM=z9j{t$RG=n>fjYe0PGh&e{p!!Ehdp`KbV5r2HTBv-$B*zzEy& z?>ab*IgbJBJw}=@7(<1fhOD#lm0ZGOqI#oC+hL*YX}uBL-Gf@6;O+(lOX$MRr+K#v zT-F&&ONytZeLcFbi@S;gsWi)ty?toT+z$Ryk$^LZoP|;Q$P~mR1X1uY3#L=rhfST_ z>P?g8>!!C{WE5Z(b~{0%Wls0zTZ&7m6ZY+aD9H)xyeG|3;ACx#F z-$$iiDG1C4$9<9?^jYxQh;TtT5m2)PPjTjg&8c(h?lVA~WDd$P5ADgDke}<)-0gI# z^6bn^m_%g9?j1WXz|WCdC-@c|VdB`aeP_o7j2tI=9J4cY)Hb+-gDELyX?7aia4J%O++slthJ%N9fshi6g>ZYc3vONo3m&$B)oB|F{O{6(doP_CIWku{_Vj(hhc*3I}EeI8|VWyyLS#JFusUE#!mi0$S#a$jEciGoqOb#Xu7QaKOl3KyT=b&p)Lc zeWB%m26x7f_e>9tp>(MuS(UWJQX9Cg6$z)yQAN5>$#kz^9X`prj zUd@W*aNm3X{~)CAn|a(FP+!!WU!@eOCMZpaaDReyOzGzWR4XPT0Wm`tiYdMNLP-q3 z#+?(r>S>wVkb}0K-tOU^lS9W<%n)S93O6!z9B*AF!erVPQS?699}JQSsDe8c)0sUT z)VV~EG!^#3eQX0$1W5r7t8rh%2gfXR*vGwca1K0S;n~Q{ET9cPOsQiquGloJqIDU3 z1bo1`g-k;?S=J-k!>S3{i~Sol#~Bw8BgBJ6dUUaZCyh`~8jsvUF{Ey_P#z%zfZJ@v z@`^7st6uPsZ(}((4c6Z~%BkYc2kuLPG=+IlFl5+k-YPXN>L5#qZ@ zeu&@Trl-rZxJB7F4Gu;rpcn_chPq!H76=|VK(Ftds2I+` z&d@C&|0Qs!|H+u>1M$KwX%l^6HoEs@%+bGEG>|N+NS0Q9nF-LH<=q$khp}g`+*|nOhHHj)`tjZZ}0iR{O>S&_s_?_-}`H9PY z>$*<+F&1ALwEU=LVf4mp*I&Egz3yFpCEk26(cB$t?!I$8-aMFS9)(+-c(rHVi1$CQ zqK{y)GG4ngQM+&XOuTA;qUuPj>PWn*Yu@m?4Yh04P0{B4@#@ZKS?5~i#w%TaD=+N$ zz`wZVR_jvhavu*MPb;q~#B(?N` zl{Gr*ji2}?d`8Cu@rKFy0_^TMz27p%Yqlq9jx6_nG;n($UU?Kc$(h=*W$~lX?NGd` zd)|;NtC~L&ZRv@Z_R!f)#;b$TvS5aR8I$#!=WXk}xwzpb-r*ECe4-}59({~o>y=F1 zCh8l3By=^DY~7yR+>t!k{prBR19uJagTv8Reer|754eTnHwLZ`EF0oYdsiL%Rs&~O zr^1P;^Y^CC$EPl?GtAp;x9$PMgxDV4Lx!1Tdv!_24(dFaw?8UkHf>%A+?cvP72Vkl z8%n;Cp?Q6Bqhp~L zrySeZp4fPDdC!XH)6*ZHj_o=*Z~9&7#$-*ywZW@{i_MGmpEM^r`(vH`iB5N{(;cr7 z=6lyFYX2rU&%wo_>QY*mowvk_Tx->iM0Lx(>XyY_w+=2H{O~XkL!!DXR^4@HOT4;& z-k7G`WOdyaO|41CmK)aV*2TWKW7h*cS68vlaJ3si7PPeA+PJiF(Z2jL5cs!le`{Ha zZthKPZe4hFv1@T+@lzT9fl;(=F#uoFBHX7-KC*?i9zHx*wEU8cP0~u@skn z`KW>^t9{6@K#B{`uJYEaP%+QJQ5M?=NCcwAVAh4%O&_JUvRp35}Y7LB&AIiD7 zcTGkjGp{>TkpmuWssV7S2XFswpI|i;)9G_qj zb)|kYwS%_oO(UN|^-%<=fe194$GUM=-_5R;Y<44%7M|wB} zRPmH%3g5XQOEX1>9MVos5LmeL1*%a4Pz?v>Xnsf@AWdHjQlA9}j_|MO)AY3=&6!zd zy%kG42h*c~?wlJmlT%`|?nK#q@4q~3zL#B4n&)BwN>MoH1noVQqyys=&uLE3<^u1_ z(#}r73VneaCpUmepLR)R?x54w1efK6wB_$1#S84n3-Z73%FG8Y)e45@tS%S5e{jJw zn^9CaHJc8eGvf8F!R11WYtHC$(bt|S3yu5&CXY`aCw#M^$ehXLQXhe0fAkqB_BU4v zcfaisQ0_8s8`au5^NyW++qT2sjyYG3jywo2ltc*>ItLha$h$h*+tbjU)TD71D<)uC z@Tv(eeha!tV0IcTm=XLWip@6;k1tbY^gV{kQY;i))zHx3%l9gizSkgNd;(X*Wz&O8 zIVN5=!wXz5Xz3z8#O1*vrsI*HV_(Pw;3*M(q?>bt%uoB^dDE`};3x2t{|FJVM|*j~ z-gM926t{1_)R*Kf34TM2-*Cmg%zx4m#r)Asp=q(>XMS4PnJF~iAgF8k9X5)RmS+r&-iM}*YHIRZU#J)q5Qvs{1P^Ue25Vm7X;mGs!3o) z#J8#YNg1t*6kRZ^YWMKiBQhBZo}u_vOqy{_TA>G}K>PH9UhWH$omj|?oE)UKg!B1vm4hP3~#$U7w4+}oi}~A KZ=GXO+w=db+?{m* literal 0 HcmV?d00001 diff --git a/scripts/openvino_npu_worker.py b/scripts/openvino_npu_worker.py new file mode 100644 index 0000000..e5a0309 --- /dev/null +++ b/scripts/openvino_npu_worker.py @@ -0,0 +1,187 @@ +import argparse +import base64 +import json +import sys +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from io import BytesIO +from pathlib import Path + +import openvino as ov +import openvino_genai as genai +from PIL import Image + + +class WorkerState: + def __init__(self, model_dir: Path, width: int, height: int, cache_dir: Path): + self.model_dir = model_dir + self.width = width + self.height = height + self.cache_dir = cache_dir + self.ready = False + self.error = None + self.pipeline = None + self.devices = {} + self.started_at = time.time() + + def load(self) -> None: + try: + available_devices = ov.Core().available_devices + if "NPU" not in available_devices: + raise RuntimeError( + f"Intel NPU is not available to OpenVINO. Devices: {', '.join(available_devices)}" + ) + vae_device = "GPU" if "GPU" in available_devices else "CPU" + self.devices = { + "text_encoder": "CPU", + "unet": "NPU", + "vae_decoder": vae_device, + } + print(f"[openvino-npu] Loading model: {self.model_dir}", flush=True) + print( + "[openvino-npu] Devices: " + f"text_encoder=CPU, unet=NPU, vae_decoder={vae_device}", + flush=True, + ) + self.cache_dir.mkdir(parents=True, exist_ok=True) + pipe = genai.Text2ImagePipeline(str(self.model_dir)) + pipe.reshape(1, self.height, self.width, 1.0) + pipe.compile( + "CPU", + "NPU", + vae_device, + config={"CACHE_DIR": str(self.cache_dir)}, + ) + self.pipeline = pipe + self.ready = True + print("[openvino-npu] READY", flush=True) + except Exception as exc: + self.error = str(exc) + print(f"[openvino-npu] ERROR: {self.error}", file=sys.stderr, flush=True) + raise + + +def make_handler(state: WorkerState): + class Handler(BaseHTTPRequestHandler): + def _json(self, code: int, payload: dict) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_OPTIONS(self) -> None: + self.send_response(204) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS") + self.end_headers() + + def do_GET(self) -> None: + if self.path == "/health": + self._json(200, { + "ok": state.ready and not state.error, + "ready": state.ready, + "error": state.error, + "model": str(state.model_dir), + "devices": state.devices, + "uptime_sec": round(time.time() - state.started_at, 1), + }) + return + self._json(404, {"ok": False, "error": "Unknown endpoint"}) + + def do_POST(self) -> None: + if self.path != "/generate": + self._json(404, {"ok": False, "error": "Unknown endpoint"}) + return + if not state.ready or state.pipeline is None: + self._json(503, {"ok": False, "error": state.error or "OpenVINO NPU worker is not ready"}) + return + + length = int(self.headers.get("Content-Length", "0") or "0") + try: + request = json.loads(self.rfile.read(length).decode("utf-8") or "{}") + prompt = str(request.get("prompt") or "").strip() + if not prompt: + raise ValueError("Prompt is required") + steps = max(1, min(8, int(request.get("steps") or 4))) + guidance_scale = float(request.get("guidance_scale") or request.get("cfg_scale") or 1.0) + width = int(request.get("width") or state.width) + height = int(request.get("height") or state.height) + if (width, height) not in ((512, 512), (1024, 1024)): + raise ValueError("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale.") + + started = time.time() + last_step_at = started + + def progress_callback(step: int, num_steps: int, _latent) -> bool: + nonlocal last_step_at + now = time.time() + step_duration = max(0.001, now - last_step_at) + last_step_at = now + print( + f"[openvino-npu] PROGRESS {step + 1}/{num_steps} {1.0 / step_duration:.2f} it/s", + flush=True, + ) + return False + + generate_args = { + "width": state.width, + "height": state.height, + "num_inference_steps": steps, + "guidance_scale": guidance_scale, + "rng_seed": int(request.get("seed")) if request.get("seed") not in (None, -1) else int(time.time_ns() % (2**32)), + "callback": progress_callback, + } + negative_prompt = str(request.get("negative_prompt") or "").strip() + if negative_prompt and guidance_scale > 1.0: + generate_args["negative_prompt"] = negative_prompt + image_tensor = state.pipeline.generate(prompt, **generate_args) + print("[openvino-npu] DECODING", flush=True) + image = Image.fromarray(image_tensor.data[0]) + upscaled = width != state.width or height != state.height + if upscaled: + image = image.resize((width, height), Image.Resampling.LANCZOS) + buf = BytesIO() + image.save(buf, format="PNG") + encoded = base64.b64encode(buf.getvalue()).decode("ascii") + self._json(200, { + "ok": True, + "data": [{ + "b64_json": encoded, + "seed": request.get("seed"), + }], + "generated_size": f"{state.width}x{state.height}", + "output_size": f"{width}x{height}", + "upscaled": upscaled, + "duration_sec": round(time.time() - started, 2), + }) + except Exception as exc: + self._json(500, {"ok": False, "error": str(exc)}) + + def log_message(self, fmt: str, *args) -> None: + print(f"[openvino-npu-http] {fmt % args}", flush=True) + + return Handler + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--model-dir", required=True, type=Path) + parser.add_argument("--port", required=True, type=int) + parser.add_argument("--width", type=int, default=512) + parser.add_argument("--height", type=int, default=512) + parser.add_argument("--cache-dir", type=Path, default=Path("app/tools/openvino-cache")) + args = parser.parse_args() + + state = WorkerState(args.model_dir, args.width, args.height, args.cache_dir) + state.load() + server = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(state)) + print(f"[openvino-npu] Listening on http://127.0.0.1:{args.port}", flush=True) + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/scripts/serve.cjs b/scripts/serve.cjs index c8b901a..6b74f06 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -68,15 +68,38 @@ const MODELS = path.join(ROOT, "app", "models"); if (!fs.existsSync(MODELS)) { fs.mkdirSync(MODELS, { recursive: true }); } +const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models"); +if (!fs.existsSync(OPENVINO_MODELS)) { + fs.mkdirSync(OPENVINO_MODELS, { recursive: true }); +} const OUTPUTS = path.join(ROOT, "app", "outputs"); if (!fs.existsSync(OUTPUTS)) { fs.mkdirSync(OUTPUTS, { recursive: true }); } +const OPENVINO_NPU_MODELS = [ + { + id: "lcm-dreamshaper-v7-fp16", + name: "LCM DreamShaper v7 FP16", + repo: "OpenVINO/LCM_Dreamshaper_v7-fp16-ov", + folder: "LCM_Dreamshaper_v7-fp16-ov", + approxSize: "2.0 GB", + resolution: "512x512", + notes: "OpenVINO LCM image model. Uses CPU text encoder, NPU UNet, and GPU or CPU VAE decoder.", + }, +]; + // ── Backend process state ───────────────────────────────────────────────────── let backendProc = null; let backendReady = false; let backendError = null; +let openvinoProc = null; +let openvinoReady = false; +let openvinoError = null; +let openvinoPort = null; +let openvinoModel = null; +let openvinoWidth = null; +let openvinoHeight = null; let backendLoadState = { active: false, phase: "", @@ -104,6 +127,8 @@ let currentSettings = { backendType: "auto", vaeTiling: true, vaeOnCpu: false, + width: 512, + height: 512, }; let lastCpuSample = null; @@ -360,6 +385,98 @@ function getPathSize(targetPath) { } } +function getOpenVinoPythonCandidates() { + const candidates = []; + if (process.env.OPENVINO_PYTHON) candidates.push(process.env.OPENVINO_PYTHON); + if (osPlatform === "win32") { + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "Scripts", "python.exe")); + candidates.push("C:\\tmp\\npu-test-venv\\Scripts\\python.exe"); + candidates.push("python"); + } else { + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python")); + candidates.push("python3"); + candidates.push("python"); + } + return [...new Set(candidates)]; +} + +function getOpenVinoPython() { + for (const candidate of getOpenVinoPythonCandidates()) { + try { + const result = spawnSync(candidate, [ + "-c", + "import openvino, openvino_genai, PIL; print(openvino.__version__)", + ], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] }); + if (result.status === 0) return candidate; + } catch (_) {} + } + return null; +} + +function getOpenVinoNpuInfo() { + if (osPlatform !== "win32" && osPlatform !== "linux") { + return { supported: false, reason: "OpenVINO NPU backend is supported on Windows and Linux Intel Core Ultra systems." }; + } + const python = getOpenVinoPython(); + if (!python) { + const setupScript = osPlatform === "win32" + ? "scripts/setup-openvino-npu.ps1" + : "bash scripts/setup-openvino-npu.sh"; + return { + supported: false, + reason: `OpenVINO GenAI runtime is not installed. Run ${setupScript} first.`, + }; + } + try { + const script = [ + "import json, openvino as ov", + "core=ov.Core()", + "devices=core.available_devices", + "info={'devices':devices,'npu':None}", + "if 'NPU' in devices:", + " info['npu']={'name':core.get_property('NPU','FULL_DEVICE_NAME'),'capabilities':core.get_property('NPU','OPTIMIZATION_CAPABILITIES')}", + "print(json.dumps(info))", + ].join("\n"); + const result = spawnSync(python, ["-c", script], { encoding: "utf8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] }); + if (result.status !== 0) { + return { supported: false, python, reason: result.stderr.trim() || "OpenVINO NPU probe failed." }; + } + const info = JSON.parse(result.stdout.trim()); + if (!info.devices.includes("NPU")) { + return { supported: false, python, reason: `OpenVINO is installed, but NPU is not available. Devices: ${info.devices.join(", ")}` }; + } + return { supported: true, platform: osPlatform, python, devices: info.devices, npu: info.npu }; + } catch (err) { + return { supported: false, python, reason: err.message || String(err) }; + } +} + +function getOpenVinoModelInfo() { + return OPENVINO_NPU_MODELS.map((model) => { + const modelPath = path.join(OPENVINO_MODELS, model.folder); + const requiredFiles = [ + path.join(modelPath, "model_index.json"), + path.join(modelPath, "text_encoder", "openvino_model.xml"), + path.join(modelPath, "unet", "openvino_model.xml"), + path.join(modelPath, "vae_decoder", "openvino_model.xml"), + ]; + const installed = requiredFiles.every((file) => fs.existsSync(file)); + return { + ...model, + filename: model.id, + path: modelPath, + installed, + sizeBytes: getPathSize(modelPath), + size: formatBytes(getPathSize(modelPath)), + format: "OpenVINO", + }; + }); +} + +function findOpenVinoModel(modelId) { + return getOpenVinoModelInfo().find((model) => model.id === modelId || model.folder === modelId || model.name === modelId); +} + function pathInside(childPath, parentPath) { const relative = path.relative(path.resolve(parentPath), path.resolve(childPath)); return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative)); @@ -468,7 +585,7 @@ async function getHealth() { const ports = { frontend: { ...(await checkPort(PORT_FRONTEND)), expectedInUse: true }, - backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null }, + backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null || openvinoProc !== null }, }; ports.frontend.ok = !ports.frontend.available; ports.backend.preferred = PREFERRED_BACKEND_PORT; @@ -489,8 +606,8 @@ async function getHealth() { checks, ports, backend: { - ready: backendReady, - running: backendProc !== null, + ready: backendReady || openvinoReady, + running: backendProc !== null || openvinoProc !== null, error: backendError, settings: currentSettings, options: getBackendOptions(), @@ -609,12 +726,16 @@ function getBackendOptions() { // as Metal-capable so the UI does not default to CPU if a probe flag changes // upstream. const metalAvailable = metalInstalled; + const openvinoNpu = getOpenVinoNpuInfo(); + const openvinoModels = getOpenVinoModelInfo(); + const openvinoNpuAvailable = openvinoNpu.supported && openvinoModels.some((model) => model.installed); const options = [{ id: "cpu", label: "CPU", available: true }]; if (metalAvailable) options.push({ id: "metal", label: "Metal GPU", available: true }); if (vulkanAvailable) options.push({ id: "vulkan", label: "Vulkan GPU", available: true }); if (rocmAvailable) options.push({ id: "rocm", label: "ROCm GPU (AMD)", available: true }); if (cudaAvailable) options.push({ id: "cuda", label: "CUDA GPU", available: true }); + if (openvinoNpuAvailable) options.push({ id: "openvino-npu", label: "NPU (OpenVINO)", available: true }); const unavailable = []; if (vulkanInstalled && !vulkanAvailable) { @@ -629,6 +750,11 @@ function getBackendOptions() { if (metalInstalled && !metalAvailable) { unavailable.push({ id: "metal", label: "Metal GPU", reason: "Installed, but Metal backend validation failed." }); } + if (openvinoNpu.supported && !openvinoNpuAvailable) { + unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: "Runtime is ready, but no OpenVINO NPU model is downloaded." }); + } else if (!openvinoNpu.supported && (osPlatform === "win32" || osPlatform === "linux")) { + unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: openvinoNpu.reason }); + } let defaultBackend = "cpu"; if (metalAvailable) { @@ -654,6 +780,9 @@ function getBackendOptions() { vulkanAvailable, rocmAvailable, metalAvailable, + openvinoNpuAvailable, + openvinoNpu, + openvinoModels, defaultBackendType: defaultBackend, }; return cachedBackendOptions; @@ -795,6 +924,208 @@ function pingBackendReady() { }); } +function requestJson(url, payload = null, timeoutMs = 120000) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const data = payload ? Buffer.from(JSON.stringify(payload), "utf8") : null; + const req = http.request({ + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname + parsed.search, + method: data ? "POST" : "GET", + headers: data ? { + "Content-Type": "application/json", + "Content-Length": data.length, + } : {}, + timeout: timeoutMs, + }, (res) => { + let body = ""; + res.setEncoding("utf8"); + res.on("data", (chunk) => { body += chunk; }); + res.on("end", () => { + try { + const jsonBody = JSON.parse(body || "{}"); + if (res.statusCode < 200 || res.statusCode >= 300 || jsonBody.ok === false) { + reject(new Error(jsonBody.error || `HTTP ${res.statusCode}`)); + return; + } + resolve(jsonBody); + } catch (err) { + reject(err); + } + }); + }); + req.on("error", reject); + req.on("timeout", () => req.destroy(new Error("Request timed out"))); + if (data) req.write(data); + req.end(); + }); +} + +async function killOpenVinoWorker() { + if (!openvinoProc) { + openvinoReady = false; + return; + } + console.log(" [openvino-npu] Stopping worker..."); + try { openvinoProc.kill("SIGTERM"); } catch (_) {} + await new Promise((resolve) => setTimeout(resolve, 800)); + try { openvinoProc.kill("SIGKILL"); } catch (_) {} + openvinoProc = null; + openvinoReady = false; +} + +function getOpenVinoResolution(settings = {}) { + const width = Number(settings.width) || 512; + const height = Number(settings.height) || 512; + const supported = (width === 512 && height === 512) || (width === 1024 && height === 1024); + if (!supported) { + throw new Error("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale."); + } + return { width, height }; +} + +async function startOpenVinoWorker(settings = {}) { + const npuInfo = getOpenVinoNpuInfo(); + if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU is not available."); + const model = findOpenVinoModel(settings.model) || getOpenVinoModelInfo().find((item) => item.installed); + if (!model || !model.installed) { + throw new Error("OpenVINO NPU model is not downloaded. Download an OpenVINO NPU model from Model Manager first."); + } + const { width, height } = getOpenVinoResolution(settings); + if (openvinoProc && + openvinoReady && + openvinoModel === model.id) { + currentSettings = { ...currentSettings, ...settings, model: model.id, width, height }; + return; + } + + await killBackend(); + await killOpenVinoWorker(); + openvinoError = null; + openvinoReady = false; + openvinoModel = model.id; + openvinoWidth = 512; + openvinoHeight = 512; + openvinoPort = await findAvailableBackendPort(); + PORT_BACKEND = openvinoPort; + + currentSettings = { ...currentSettings, ...settings }; + currentSettings.backendType = "openvino-npu"; + currentSettings.useGpu = true; + currentSettings.backendMode = "NPU (OpenVINO)"; + currentSettings.backendBinary = "openvino_npu_worker.py"; + currentSettings.backendDevice = npuInfo.npu?.name || "Intel AI Boost"; + currentSettings.model = model.id; + currentSettings.width = width; + currentSettings.height = height; + + backendError = null; + backendLoadState = { + active: true, + phase: "Compiling OpenVINO NPU pipeline (512x512)...", + progress: 5, + current: 0, + total: 0, + speed: "", + model: model.name, + backendMode: "NPU (OpenVINO)", + backendBinary: "openvino_npu_worker.py", + device: currentSettings.backendDevice, + }; + + const workerPath = path.join(ROOT, "scripts", "openvino_npu_worker.py"); + const cacheDir = path.join(ROOT, "app", "tools", "openvino-cache", "512x512"); + console.log(` [openvino-npu] Starting 512x512 worker on port ${openvinoPort}`); + openvinoProc = spawn(npuInfo.python, [ + workerPath, + "--model-dir", model.path, + "--port", String(openvinoPort), + "--width", "512", + "--height", "512", + "--cache-dir", cacheDir, + ], { stdio: "pipe" }); + + let openvinoStdoutBuffer = ""; + openvinoProc.stdout.on("data", (data) => { + const text = data.toString(); + process.stdout.write(" " + text); + openvinoStdoutBuffer += text; + const lines = openvinoStdoutBuffer.split(/\r?\n/); + openvinoStdoutBuffer = lines.pop() || ""; + for (const line of lines) { + if (line.includes("READY")) { + openvinoReady = true; + backendReady = true; + backendLoadState = { ...backendLoadState, active: false, phase: "OpenVINO NPU model ready", progress: 100 }; + } + const progressMatch = line.match(/PROGRESS\s+(\d+)\/(\d+)\s+(.+)$/); + if (progressMatch) { + generationState.active = true; + generationState.step = Number(progressMatch[1]); + generationState.steps = Number(progressMatch[2]); + generationState.speed = progressMatch[3].trim(); + generationState.decoding = false; + } else if (line.includes("DECODING") && generationState.active) { + generationState.step = generationState.steps; + generationState.decoding = true; + generationState.speed = ""; + } + } + }); + openvinoProc.stderr.on("data", (data) => { + const text = data.toString(); + process.stderr.write(" " + text); + const cleanText = stripAnsi(text).trim(); + if (cleanText && (cleanText.includes("ERROR") || cleanText.includes("Traceback") || cleanText.includes("Exception"))) { + openvinoError = cleanText; + backendError = openvinoError; + } + }); + openvinoProc.on("exit", (code) => { + console.log(" [openvino-npu] worker exited with code", code); + openvinoProc = null; + openvinoReady = false; + backendReady = false; + if (code !== 0 && code !== null && !backendError) backendError = `OpenVINO NPU worker exited with code ${code}`; + backendLoadState.active = false; + }); + + const startedAt = Date.now(); + while (Date.now() - startedAt < 360000) { + if (openvinoReady) return; + if (backendError) throw new Error(backendError); + if (!openvinoProc) throw new Error("OpenVINO NPU worker exited during startup."); + const elapsed = Math.round((Date.now() - startedAt) / 1000); + backendLoadState.progress = Math.min(95, 5 + Math.round(elapsed / 2)); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + await killOpenVinoWorker(); + throw new Error("OpenVINO NPU 512x512 compile timed out after 6 minutes."); +} + +async function generateWithOpenVino(body) { + if (!openvinoReady || !openvinoPort) throw new Error("OpenVINO NPU worker is not ready. Load an OpenVINO NPU model first."); + const steps = Math.max(1, Math.min(8, Number(body.steps) || 4)); + generationState = { active: true, step: 0, steps, speed: "", decoding: false }; + try { + const result = await requestJson(`http://127.0.0.1:${openvinoPort}/generate`, { + prompt: body.prompt, + negative_prompt: body.negative_prompt || "", + width: Number(body.width) || currentSettings.width || 512, + height: Number(body.height) || currentSettings.height || 512, + steps, + cfg_scale: Number(body.cfg_scale) || 1.0, + seed: body.seed, + }, 300000); + resetGenerationState(); + return result; + } catch (err) { + resetGenerationState(); + throw err; + } +} + function startBackendReadyPoll() { let attempts = 0; const interval = setInterval(async () => { @@ -839,6 +1170,11 @@ function killBackend() { } async function startBackend(settings = {}) { + if (settings.backendType === "openvino-npu") { + await startOpenVinoWorker(settings); + return; + } + await killOpenVinoWorker(); backendError = null; currentSettings = { ...currentSettings, ...settings }; if (!currentSettings.model) currentSettings.model = getDefaultModel(); @@ -1118,6 +1454,54 @@ let downloadState = { }; let activeDownload = null; +function startOpenVinoModelDownload(modelId) { + if (downloadState.active) return; + const npuInfo = getOpenVinoNpuInfo(); + if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU runtime is not available."); + const model = OPENVINO_NPU_MODELS.find((item) => item.id === modelId); + if (!model) throw new Error("Unknown OpenVINO model."); + + const destDir = path.join(OPENVINO_MODELS, model.folder); + fs.mkdirSync(destDir, { recursive: true }); + downloadState = { + active: true, + filename: model.name, + progress: -1, + speed: "Downloading snapshot", + eta: 0, + totalBytes: 0, + downloadedBytes: 0, + error: null, + }; + + const script = [ + "from huggingface_hub import snapshot_download", + `snapshot_download(repo_id=${JSON.stringify(model.repo)}, local_dir=${JSON.stringify(destDir)}, ignore_patterns=['safety_checker/*','feature_extractor/*','*.safetensors'])`, + "print('DONE')", + ].join("\n"); + console.log(` [openvino-download] Downloading ${model.repo} -> ${destDir}`); + const proc = spawn(npuInfo.python, ["-c", script], { stdio: "pipe" }); + activeDownload = { process: proc, destPath: destDir, openvino: true }; + proc.stdout.on("data", (data) => process.stdout.write(" [openvino-download] " + data.toString())); + proc.stderr.on("data", (data) => process.stderr.write(" [openvino-download] " + data.toString())); + proc.on("exit", (code) => { + activeDownload = null; + if (String(downloadState.error || "").toLowerCase().includes("cancelled")) { + return; + } + downloadState.active = false; + if (code === 0) { + downloadState.progress = 100; + downloadState.downloadedBytes = getPathSize(destDir); + downloadState.totalBytes = downloadState.downloadedBytes; + downloadState.speed = "Complete"; + cachedBackendOptions = null; + } else { + downloadState.error = `OpenVINO model download failed with code ${code}`; + } + }); +} + // ── Generation State (Real-time progress parser) ───────────────────────────── let generationState = { active: false, @@ -1342,9 +1726,14 @@ function cancelModelDownload() { } const filename = downloadState.filename; if (activeDownload) { - try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {} - try { activeDownload.fileStream.destroy(); } catch (_) {} - try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {} + if (activeDownload.process) { + try { activeDownload.process.kill("SIGTERM"); } catch (_) {} + setTimeout(() => { try { activeDownload?.process?.kill("SIGKILL"); } catch (_) {} }, 1000); + } else { + try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {} + try { activeDownload.fileStream.destroy(); } catch (_) {} + try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {} + } } else if (filename) { try { fs.unlinkSync(`${path.join(MODELS, filename)}.part`); } catch (_) {} } @@ -1514,9 +1903,9 @@ function safeOutputName(value) { } function saveGeneratedOutput(imageDataUrl, metadata = {}) { - const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp|svg\+xml));base64,(.+)$/); + const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/); if (!match) { - throw new Error("Expected a base64 image data URL"); + throw new Error("Expected a real base64 PNG, JPEG, or WebP image data URL"); } const mime = match[1]; @@ -1525,9 +1914,26 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) { "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/webp": ".webp", - "image/svg+xml": ".svg", }; const ext = extByMime[mime] || ".png"; + const imageBuffer = Buffer.from(match[2], "base64"); + const isPng = imageBuffer.length > 8 && + imageBuffer[0] === 0x89 && + imageBuffer[1] === 0x50 && + imageBuffer[2] === 0x4e && + imageBuffer[3] === 0x47; + const isJpeg = imageBuffer.length > 3 && + imageBuffer[0] === 0xff && + imageBuffer[1] === 0xd8 && + imageBuffer[2] === 0xff; + const isWebp = imageBuffer.length > 12 && + imageBuffer.toString("ascii", 0, 4) === "RIFF" && + imageBuffer.toString("ascii", 8, 12) === "WEBP"; + if ((mime === "image/png" && !isPng) || + ((mime === "image/jpeg" || mime === "image/jpg") && !isJpeg) || + (mime === "image/webp" && !isWebp)) { + throw new Error("Generated image payload did not match its declared image format."); + } const createdAt = new Date().toISOString(); const stamp = createdAt.replace(/[:.]/g, "-"); const seed = metadata.seed !== undefined && metadata.seed !== null ? `-${safeOutputName(metadata.seed)}` : ""; @@ -1537,7 +1943,7 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) { const imagePath = path.join(OUTPUTS, imageFilename); const metadataPath = path.join(OUTPUTS, metadataFilename); - fs.writeFileSync(imagePath, Buffer.from(match[2], "base64")); + fs.writeFileSync(imagePath, imageBuffer); const savedMetadata = { ...metadata, createdAt, @@ -1703,9 +2109,9 @@ const server = http.createServer(async (req, res) => { // GET /api/backend-status if (req.url === "/api/backend-status" && req.method === "GET") { - return json(res, 200, { - ready: backendReady, - running: backendProc !== null, + return json(res, 200, { + ready: backendReady || openvinoReady, + running: backendProc !== null || openvinoProc !== null, port: PORT_BACKEND, preferredPort: PREFERRED_BACKEND_PORT, error: backendError, @@ -1742,6 +2148,17 @@ const server = http.createServer(async (req, res) => { } catch (_) { return json(res, 200, { models: [] }); } } + if (req.url === "/api/openvino-models" && req.method === "GET") { + const npuInfo = getOpenVinoNpuInfo(); + return json(res, 200, { + supported: npuInfo.supported, + reason: npuInfo.reason || "", + npu: npuInfo.npu || null, + python: npuInfo.python || "", + models: npuInfo.supported ? getOpenVinoModelInfo() : [], + }); + } + // POST /api/restart-backend — restart with new settings if (req.url === "/api/restart-backend" && req.method === "POST") { const body = await readJsonBody(req, res); @@ -1750,11 +2167,13 @@ const server = http.createServer(async (req, res) => { await killBackend(); await new Promise(r => setTimeout(r, 500)); const newSettings = {}; - if (body.model) newSettings.model = path.join(MODELS, body.model); + if (body.model) newSettings.model = body.backend_type === "openvino-npu" ? String(body.model) : path.join(MODELS, body.model); if (body.steps) newSettings.steps = parseInt(body.steps); if (body.cfgScale) newSettings.cfgScale = parseFloat(body.cfgScale); if (body.sampler) newSettings.sampler = body.sampler; if (body.threads) newSettings.threads = parseInt(body.threads); + if (body.width) newSettings.width = parseInt(body.width); + if (body.height) newSettings.height = parseInt(body.height); if (typeof body.use_gpu === "boolean") newSettings.useGpu = body.use_gpu; if (body.backend_type) { newSettings.backendType = String(body.backend_type); @@ -1763,6 +2182,18 @@ const server = http.createServer(async (req, res) => { if (typeof body.vae_tiling === "boolean") newSettings.vaeTiling = body.vae_tiling; if (typeof body.vae_on_cpu === "boolean") newSettings.vaeOnCpu = body.vae_on_cpu; try { + if (newSettings.backendType === "openvino-npu") { + startBackend(newSettings).catch((err) => { + backendError = err.message || String(err); + backendLoadState = { + ...backendLoadState, + active: false, + phase: "OpenVINO NPU model load failed", + }; + console.error(" [openvino-npu] Startup failed:", backendError); + }); + return json(res, 200, { ok: true, message: "OpenVINO NPU backend starting...", settings: currentSettings, port: PORT_BACKEND }); + } await startBackend(newSettings); return json(res, 200, { ok: true, message: "Backend restarting...", settings: currentSettings, port: PORT_BACKEND }); } catch (err) { @@ -1774,6 +2205,7 @@ const server = http.createServer(async (req, res) => { // POST /api/stop-backend if (req.url === "/api/stop-backend" && req.method === "POST") { await killBackend(); + await killOpenVinoWorker(); return json(res, 200, { ok: true }); } @@ -1788,6 +2220,19 @@ const server = http.createServer(async (req, res) => { return json(res, 200, { ok: true, message: "Download started" }); } + if (req.url === "/api/download-openvino-model" && req.method === "POST") { + const body = await readJsonBody(req, res); + if (!body) return; + const modelId = String(body.model_id || body.modelId || ""); + if (!modelId) return json(res, 400, { ok: false, error: "model_id is required" }); + try { + startOpenVinoModelDownload(modelId); + return json(res, 200, { ok: true, message: "OpenVINO model download started" }); + } catch (err) { + return json(res, 500, { ok: false, error: err.message || String(err) }); + } + } + // GET /api/download-progress if (req.url === "/api/download-progress" && req.method === "GET") { return json(res, 200, downloadState); @@ -1804,6 +2249,16 @@ const server = http.createServer(async (req, res) => { return json(res, 200, generationState); } + if (req.url === "/api/openvino-generate" && req.method === "POST") { + const body = await readJsonBody(req, res); + if (!body) return; + try { + return json(res, 200, await generateWithOpenVino(body)); + } catch (err) { + return json(res, 500, { ok: false, error: err.message || String(err) }); + } + } + // GET /api/outputs if (req.url === "/api/outputs" && req.method === "GET") { return json(res, 200, { outputs: listGeneratedOutputs() }); @@ -1871,11 +2326,17 @@ const server = http.createServer(async (req, res) => { const { filename } = body; if (!filename) return json(res, 400, { error: "Filename is required" }); + const openvinoModel = findOpenVinoModel(filename); const safeFilename = path.basename(filename); const modelPath = path.join(MODELS, safeFilename); try { - if (fs.existsSync(modelPath)) { + if (openvinoModel?.installed && openvinoModel.path && pathInside(openvinoModel.path, OPENVINO_MODELS)) { + fs.rmSync(openvinoModel.path, { recursive: true, force: true }); + cachedBackendOptions = null; + console.log(` [api] Deleted OpenVINO model: ${openvinoModel.id}`); + return json(res, 200, { ok: true, message: `Deleted ${openvinoModel.name}` }); + } else if (fs.existsSync(modelPath)) { fs.unlinkSync(modelPath); console.log(` [api] Deleted model file: ${safeFilename}`); return json(res, 200, { ok: true, message: `Deleted ${safeFilename}` }); diff --git a/scripts/setup-openvino-npu.ps1 b/scripts/setup-openvino-npu.ps1 new file mode 100644 index 0000000..da5f70e --- /dev/null +++ b/scripts/setup-openvino-npu.ps1 @@ -0,0 +1,46 @@ +# Optional OpenVINO NPU setup for Intel AI Boost systems. +# This does not modify the existing stable-diffusion.cpp backends. + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$rootDir = Split-Path -Parent $scriptDir +$venvDir = Join-Path $rootDir "app\tools\openvino-venv" +$pythonExe = Join-Path $venvDir "Scripts\python.exe" + +Write-Host "" +Write-Host " ============================================================" +Write-Host " Local AI Image Generator - OpenVINO NPU Setup" +Write-Host " ============================================================" +Write-Host "" + +$npu = Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*Intel(R) AI Boost*" -or ($_.Name -match "NPU" -and $_.PNPClass -eq "ComputeAccelerator") } | + Select-Object -First 1 + +if (-not $npu) { + Write-Host " [ERROR] Intel AI Boost NPU was not detected on this system." -ForegroundColor Red + exit 1 +} + +Write-Host " Detected: $($npu.Name)" + +if (-not (Test-Path $pythonExe)) { + Write-Host " Creating Python environment: $venvDir" + python -m venv $venvDir +} + +Write-Host " Installing OpenVINO GenAI runtime..." +& $pythonExe -m pip install --upgrade pip +& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub + +Write-Host "" +Write-Host " Verifying OpenVINO NPU device..." +& $pythonExe -c "import openvino as ov; core=ov.Core(); print('Available devices:', core.available_devices); raise SystemExit(0 if 'NPU' in core.available_devices else 2)" + +Write-Host "" +Write-Host " ============================================================" +Write-Host " OpenVINO NPU setup complete." +Write-Host " Restart windows.bat, then download an OpenVINO NPU model." +Write-Host " ============================================================" +Write-Host "" diff --git a/scripts/setup-openvino-npu.sh b/scripts/setup-openvino-npu.sh new file mode 100644 index 0000000..fc74d9d --- /dev/null +++ b/scripts/setup-openvino-npu.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Optional OpenVINO NPU setup for Linux Intel Core Ultra systems. +# The Intel NPU kernel/user-mode driver must already be installed. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +VENV_DIR="$ROOT_DIR/app/tools/openvino-venv" +PYTHON_BIN="$VENV_DIR/bin/python" + +echo "" +echo " ============================================================" +echo " Local AI Image Generator - Linux OpenVINO NPU Setup" +echo " ============================================================" +echo "" + +if [[ "$(uname -s)" != "Linux" ]]; then + echo " [ERROR] This setup script is for Linux only." >&2 + exit 1 +fi + +if [[ "$(uname -m)" != "x86_64" ]]; then + echo " [ERROR] OpenVINO NPU support requires x86_64 Linux." >&2 + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo " [ERROR] python3 is required. Install Python 3 and the venv module." >&2 + exit 1 +fi + +KERNEL_VERSION="$(uname -r)" +KERNEL_MAJOR="${KERNEL_VERSION%%.*}" +KERNEL_REST="${KERNEL_VERSION#*.}" +KERNEL_MINOR="${KERNEL_REST%%.*}" +if (( KERNEL_MAJOR < 6 || (KERNEL_MAJOR == 6 && KERNEL_MINOR < 6) )); then + echo " [ERROR] Intel NPU support requires Linux kernel 6.6 or newer." + echo " Current kernel: $KERNEL_VERSION" + exit 1 +fi + +if [[ ! -e /dev/accel/accel0 ]]; then + echo " [ERROR] Intel NPU device /dev/accel/accel0 was not found." + echo " Install the Intel Linux NPU driver and reboot first." + echo " Driver: https://github.com/intel/linux-npu-driver" + exit 1 +fi + +if [[ ! -x "$PYTHON_BIN" ]]; then + echo " Creating Python environment: $VENV_DIR" + if ! python3 -m venv "$VENV_DIR"; then + echo " [ERROR] Could not create the virtual environment." + echo " On Ubuntu, install python3-venv and retry." + exit 1 + fi +fi + +echo " Installing OpenVINO GenAI runtime..." +"$PYTHON_BIN" -m pip install --upgrade pip +"$PYTHON_BIN" -m pip install openvino openvino-genai pillow huggingface-hub + +echo "" +echo " Verifying OpenVINO NPU device..." +"$PYTHON_BIN" - <<'PY' +import openvino as ov + +core = ov.Core() +devices = core.available_devices +print(" Available devices:", ", ".join(devices)) +if "NPU" not in devices: + raise SystemExit( + " [ERROR] OpenVINO is installed, but NPU is unavailable. " + "Verify the Intel NPU driver and device permissions." + ) +print(" NPU:", core.get_property("NPU", "FULL_DEVICE_NAME")) +PY + +echo "" +echo " ============================================================" +echo " OpenVINO NPU setup complete." +echo " Run ./linux.sh, then download an OpenVINO NPU model." +echo " ============================================================" +echo "" diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index cfa6836..feabd06 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -47,6 +47,18 @@ function Format-Speed { return "{0:N0} KB/s" -f ($bps / 1KB) } +function Enable-Tls12 { + try { + # Use the protocol's numeric value because older .NET enum definitions + # do not expose the Tls12 member even when Windows supports TLS 1.2. + $tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072) + [Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor $tls12 + } catch { + throw "TLS 1.2 could not be enabled. This setup requires 64-bit Windows 10 or Windows 11 with current system updates. $($_.Exception.Message)" + } +} + function Invoke-RichDownload { param([string]$Url, [string]$Dest, [string]$Label) Print-Info "Downloading: $Label" @@ -57,7 +69,7 @@ function Invoke-RichDownload { $lastTime = [DateTime]::Now try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Enable-Tls12 $req = [System.Net.HttpWebRequest]::Create($Url) $req.UserAgent = "Mozilla/5.0" $resp = $req.GetResponse() From 0beaeab414a74f2a9c003740dbd4c8d569c23d1d Mon Sep 17 00:00:00 2001 From: techjarves Date: Sat, 13 Jun 2026 15:45:16 +0530 Subject: [PATCH 5/8] fix: resolve OpenVINO NPU and GPU backend switching/loading transition issues --- app/frontend/src/components/Generator.jsx | 2 +- .../src/components/ImageConstraints.jsx | 11 +++++------ app/frontend/src/components/ModelManager.jsx | 6 ++++-- .../openvino_npu_worker.cpython-313.pyc | Bin 12022 -> 12022 bytes 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/frontend/src/components/Generator.jsx b/app/frontend/src/components/Generator.jsx index 8fc9048..c1b2811 100644 --- a/app/frontend/src/components/Generator.jsx +++ b/app/frontend/src/components/Generator.jsx @@ -139,7 +139,7 @@ function Generator({ crashError = status.error; break; } - if (!status.running && i > 3) { + if (!status.running && !status.loading?.active && i > 3) { crashError = "The backend process terminated immediately on startup."; break; } diff --git a/app/frontend/src/components/ImageConstraints.jsx b/app/frontend/src/components/ImageConstraints.jsx index 5f23b0d..ecb9ce9 100644 --- a/app/frontend/src/components/ImageConstraints.jsx +++ b/app/frontend/src/components/ImageConstraints.jsx @@ -86,18 +86,17 @@ function ImageConstraints({ const currentBackend = constraints.backendType || "cpu"; if (backendType === currentBackend) return; - const isGpuBackend = (type) => !["cpu", "openvino-npu"].includes(type); const switchesAccelerator = - (currentBackend === "openvino-npu" && isGpuBackend(backendType)) || - (isGpuBackend(currentBackend) && backendType === "openvino-npu"); + (currentBackend === "openvino-npu" && backendType !== "openvino-npu") || + (currentBackend !== "openvino-npu" && backendType === "openvino-npu"); if (serverRunning && switchesAccelerator) { const leavingNpu = currentBackend === "openvino-npu"; const confirmed = await showConfirm({ - title: leavingNpu ? "Unload NPU Model?" : "Unload GPU Model?", + title: leavingNpu ? "Unload NPU Model?" : "Unload Model?", message: leavingNpu - ? "The OpenVINO NPU model must be unloaded before switching to the GPU backend." - : "The GPU model must be unloaded before switching to the OpenVINO NPU backend.", + ? "The OpenVINO NPU model must be unloaded before switching to the standard backend." + : "The active model must be unloaded before switching to the OpenVINO NPU backend.", confirmLabel: "Unload", cancelLabel: "Cancel", danger: true, diff --git a/app/frontend/src/components/ModelManager.jsx b/app/frontend/src/components/ModelManager.jsx index bb78cc1..179de5b 100644 --- a/app/frontend/src/components/ModelManager.jsx +++ b/app/frontend/src/components/ModelManager.jsx @@ -332,7 +332,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun let isReady = false; let crashError = null; - for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms) + const isOpenVinoModel = modelInfo?.backendType === "openvino-npu"; + const maxStartupPolls = isOpenVinoModel ? 1200 : 240; + for (let i = 0; i < maxStartupPolls; i++) { const status = await getBackendStatus(); if (status.loading) { setModelLoadProgress({ @@ -355,7 +357,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun crashError = status.error; break; } - if (!status.running && i > 3) { + if (!status.running && !status.loading?.active && i > 3) { crashError = "The backend process terminated immediately on startup."; break; } diff --git a/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc b/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc index 4ca397fd11e61ed30690c4a1619fbd5210c8cf94..0d8ccd3e0d2d901e1c5bfbc4d9864deb539f07cc 100644 GIT binary patch delta 22 ccmews`z@CDGcPX}0}$xz=w_vCl*b%7 From 35b5aa9da9dd2671bdcc10164d6a62ef7bdd2e42 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sat, 13 Jun 2026 15:57:30 +0530 Subject: [PATCH 6/8] perf: cache getOpenVinoNpuInfo() results to avoid slow synchronous hardware probes on every models list request --- scripts/serve.cjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/serve.cjs b/scripts/serve.cjs index 6b74f06..9915f9c 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -413,19 +413,25 @@ function getOpenVinoPython() { return null; } +let cachedOpenVinoNpuInfo = null; + function getOpenVinoNpuInfo() { + if (cachedOpenVinoNpuInfo) return cachedOpenVinoNpuInfo; + if (osPlatform !== "win32" && osPlatform !== "linux") { - return { supported: false, reason: "OpenVINO NPU backend is supported on Windows and Linux Intel Core Ultra systems." }; + cachedOpenVinoNpuInfo = { supported: false, reason: "OpenVINO NPU backend is supported on Windows and Linux Intel Core Ultra systems." }; + return cachedOpenVinoNpuInfo; } const python = getOpenVinoPython(); if (!python) { const setupScript = osPlatform === "win32" ? "scripts/setup-openvino-npu.ps1" : "bash scripts/setup-openvino-npu.sh"; - return { + cachedOpenVinoNpuInfo = { supported: false, reason: `OpenVINO GenAI runtime is not installed. Run ${setupScript} first.`, }; + return cachedOpenVinoNpuInfo; } try { const script = [ @@ -439,15 +445,19 @@ function getOpenVinoNpuInfo() { ].join("\n"); const result = spawnSync(python, ["-c", script], { encoding: "utf8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] }); if (result.status !== 0) { - return { supported: false, python, reason: result.stderr.trim() || "OpenVINO NPU probe failed." }; + cachedOpenVinoNpuInfo = { supported: false, python, reason: result.stderr.trim() || "OpenVINO NPU probe failed." }; + return cachedOpenVinoNpuInfo; } const info = JSON.parse(result.stdout.trim()); if (!info.devices.includes("NPU")) { - return { supported: false, python, reason: `OpenVINO is installed, but NPU is not available. Devices: ${info.devices.join(", ")}` }; + cachedOpenVinoNpuInfo = { supported: false, python, reason: `OpenVINO is installed, but NPU is not available. Devices: ${info.devices.join(", ")}` }; + return cachedOpenVinoNpuInfo; } - return { supported: true, platform: osPlatform, python, devices: info.devices, npu: info.npu }; + cachedOpenVinoNpuInfo = { supported: true, platform: osPlatform, python, devices: info.devices, npu: info.npu }; + return cachedOpenVinoNpuInfo; } catch (err) { - return { supported: false, python, reason: err.message || String(err) }; + cachedOpenVinoNpuInfo = { supported: false, python, reason: err.message || String(err) }; + return cachedOpenVinoNpuInfo; } } From 7d83de23f4f0f727c52dfd3f10829e308c986126 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sat, 13 Jun 2026 16:29:04 +0530 Subject: [PATCH 7/8] feat: add ModelManager component for local model discovery, downloading, and backend status tracking --- app/frontend/src/components/ModelManager.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/frontend/src/components/ModelManager.jsx b/app/frontend/src/components/ModelManager.jsx index 179de5b..6b858fd 100644 --- a/app/frontend/src/components/ModelManager.jsx +++ b/app/frontend/src/components/ModelManager.jsx @@ -81,6 +81,7 @@ const OPENVINO_MODEL_LIBRARY = [ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) { const [localModels, setLocalModels] = useState([]); + const [isLoadingModels, setIsLoadingModels] = useState(true); const [downloadingModelId, setDownloadingModelId] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); const [downloadSpeed, setDownloadSpeed] = useState(""); @@ -171,11 +172,14 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun }; const fetchModels = async () => { + setIsLoadingModels(true); try { const list = await listLocalModels(); setLocalModels(list.map(normalizeModel).filter((model) => model.filename)); } catch (e) { console.error(e); + } finally { + setIsLoadingModels(false); } }; @@ -628,7 +632,12 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun Local Models ({localModels.length}) - {localModels.length === 0 ? ( + {isLoadingModels ? ( +
+ + Scanning models folder... +
+ ) : localModels.length === 0 ? (

No models detected in app/models/. Download from the library below or import a file.

From 6cf13a52d6aa9620dad25dc5cd52bbb34c2cef95 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sun, 14 Jun 2026 06:54:51 +0530 Subject: [PATCH 8/8] feat: introduce server process manager and OpenVINO NPU setup scripts for cross-platform model hosting --- scripts/serve.cjs | 34 +++++++++++++++++++++++++++++++--- scripts/setup-openvino-npu.ps1 | 2 +- scripts/setup-openvino-npu.sh | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/scripts/serve.cjs b/scripts/serve.cjs index 9915f9c..5c9b341 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -389,11 +389,13 @@ function getOpenVinoPythonCandidates() { const candidates = []; if (process.env.OPENVINO_PYTHON) candidates.push(process.env.OPENVINO_PYTHON); if (osPlatform === "win32") { - candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "Scripts", "python.exe")); + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-win", "Scripts", "python.exe")); + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "Scripts", "python.exe")); // legacy fallback candidates.push("C:\\tmp\\npu-test-venv\\Scripts\\python.exe"); candidates.push("python"); } else { - candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python")); + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-linux", "bin", "python")); + candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python")); // legacy fallback candidates.push("python3"); candidates.push("python"); } @@ -713,6 +715,32 @@ function hasAmdGpu() { return info.includes("amd") || info.includes("advanced micro devices") || info.includes("radeon"); } +// Detect whether we are running inside WSL2 (Windows Subsystem for Linux). +// WSL2's GPU paravirtualization (GPU-PV) only exposes a D3D12/DirectX interface +// to Linux. Vulkan hardware passthrough is only supported for NVIDIA (via CUDA). +// Intel Arc and AMD GPUs in WSL2 fall back to llvmpipe (CPU software rendering). +function isRunningInWSL() { + if (osPlatform !== "linux") return false; + try { + const procVersion = fs.readFileSync("/proc/version", "utf8").toLowerCase(); + return procVersion.includes("microsoft") || procVersion.includes("wsl"); + } catch (_) { + return false; + } +} + +function getVulkanUnavailableReason() { + if (osPlatform === "linux" && isRunningInWSL()) { + const gpuName = getGpuInfo().name; + const lowerGpu = gpuName.toLowerCase(); + if (lowerGpu.includes("intel") || lowerGpu.includes("arc")) { + return `Vulkan GPU is not available in WSL2 for Intel ${gpuName}. WSL2's GPU paravirtualization only exposes a DirectX interface — Intel Arc Vulkan requires running natively on Windows.`; + } + return "Vulkan GPU is not available in WSL2. WSL2's GPU paravirtualization does not support hardware Vulkan for this GPU. Run natively on Windows or Linux for GPU acceleration."; + } + return "Installed, but this binary did not register a Vulkan backend on this machine."; +} + function getBackendOptions() { if (cachedBackendOptions) return cachedBackendOptions; @@ -749,7 +777,7 @@ function getBackendOptions() { const unavailable = []; if (vulkanInstalled && !vulkanAvailable) { - unavailable.push({ id: "vulkan", label: "Vulkan GPU", reason: "Installed, but this binary did not register a Vulkan backend on this machine." }); + unavailable.push({ id: "vulkan", label: "Vulkan GPU", reason: getVulkanUnavailableReason() }); } if (cudaInstalled && !cudaAvailable) { unavailable.push({ id: "cuda", label: "CUDA GPU", reason: "Installed, but CUDA backend validation failed." }); diff --git a/scripts/setup-openvino-npu.ps1 b/scripts/setup-openvino-npu.ps1 index da5f70e..2fafae4 100644 --- a/scripts/setup-openvino-npu.ps1 +++ b/scripts/setup-openvino-npu.ps1 @@ -5,7 +5,7 @@ $ErrorActionPreference = "Stop" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $rootDir = Split-Path -Parent $scriptDir -$venvDir = Join-Path $rootDir "app\tools\openvino-venv" +$venvDir = Join-Path $rootDir "app\tools\openvino-venv-win" $pythonExe = Join-Path $venvDir "Scripts\python.exe" Write-Host "" diff --git a/scripts/setup-openvino-npu.sh b/scripts/setup-openvino-npu.sh index fc74d9d..36629a9 100644 --- a/scripts/setup-openvino-npu.sh +++ b/scripts/setup-openvino-npu.sh @@ -7,7 +7,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" -VENV_DIR="$ROOT_DIR/app/tools/openvino-venv" +VENV_DIR="$ROOT_DIR/app/tools/openvino-venv-linux" PYTHON_BIN="$VENV_DIR/bin/python" echo ""