Merge pull request #26 from techjarves/text_image

Text image
This commit is contained in:
Tech Jarves authored and GitHub committed 2026-06-25 20:25:11 +05:30
commit 06a46852e3
55 files changed
+20603 -3191

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.sh text eol=lf
+41 -20
View File
@@ -1,27 +1,48 @@
# Ignore large Stable Diffusion model weights (exceeds GitHub file limits)
app/models/*.safetensors
app/models/*.gguf
app/models/*.ckpt
app/models
app/outputs/
# Downloaded AI models
app/models/
app/openvino-models/
app/llm-models/
app/speech-models/
app/tts-models/
# Ignore portable Node.js binaries and packages
app/tools/
**/node_modules/
node_modules/
# Ignore backend folder completely (contains massive DLLs, EXEs, and ZIPs >1GB)
# Downloaded portable runtimes/backends
app/backend/
app/llm-backend/
app/speech-backend/
app/tts-runtime/
app/tools/
# Ignore local IDE and OS temp files
# Generated files
app/config/
app/cache/
app/dist/
app/outputs/
app/transcriptions/
app/tts-outputs/
app/tts-cache/
# Dependencies and platform-specific copies
**/node_modules/
**/node_modules_*/
**/node_modules_windows/
**/node_modules_linux/
**/node_modules_mac/
**/.active_modules_os
# Partial downloads and archives
*.part
*.tmp
app/tools/*.zip
app/tools/*.tar.gz
app/tools/*.tar.xz
# Logs and caches
*.log
__pycache__/
*.pyc
# OS/editor files
.vscode/
.idea/
.DS_Store
Thumbs.db
*.log
*.tmp
# output images folder
/app/outputs
+223 -49
View File
@@ -1,68 +1,146 @@
# 🖼️ Local AI Image Generator
# 🖼️ Uncensored AI Studio
### An easy, zero-setup Stable Diffusion GUI for Windows. Run GGUF & Safetensors models offline without Python configuration.
<p align="center">
<strong>A premium, zero-configuration local AI studio and offline GUI for Stable Diffusion (Image Generation), LLMs (Chat), Whisper (Speech-to-Text), and Kokoro (Text-to-Speech). Powered by hardware-accelerated GPU and NPU execution on Windows, Linux, and macOS.</strong>
</p>
<p align="center">
<img src="https://img.shields.io/badge/Offline-100%25-green?style=for-the-badge&logo=offline" alt="100% Offline" />
<img src="https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=for-the-badge" alt="Platforms" />
<img src="https://img.shields.io/badge/License-MIT-orange?style=for-the-badge" alt="License" />
</p>
<p align="center">
🎥 <strong>Watch the Setup & Demo Video:</strong> <a href="https://youtu.be/ESELhY-G_9w">https://youtu.be/ESELhY-G_9w</a>
</p>
| **Generation Workspace** | **Model Library** | **Image Constraints** |
| :---: | :---: | :---: |
| <img src="assets/dashboard.png" width="100%" style="border-radius: 6px;"> | <img src="assets/models.png" width="100%" style="border-radius: 6px;"> | <img src="assets/settings.png" width="100%" style="border-radius: 6px;"> |
---
</div>
<div align="center">
<p>🎥 <b>Watch the Setup & Demo Video:</b> <a href="https://youtu.be/ESELhY-G_9w">https://youtu.be/ESELhY-G_9w</a></p>
<p align="center">
<a href="https://youtu.be/ESELhY-G_9w">
<img src="https://img.youtube.com/vi/ESELhY-G_9w/maxresdefault.jpg" alt="Local AI Image Generator Video Tutorial" style="width:100%; max-width:800px; border-radius: 8px; margin-top: 10px;">
<img src="https://img.youtube.com/vi/ESELhY-G_9w/maxresdefault.jpg" alt="Uncensored AI Studio Video Tutorial" width="800" style="border-radius: 8px;" />
</a>
</div>
</p>
---
## 📖 Overview
**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows. Double-clicking `start.bat` automatically handles dependency setup, GPU backend matching (CUDA/Vulkan), and launches a high-performance local web workspace.
## 📖 Table of Contents
* [What is Uncensored AI Studio?](#what-is-uncensored-ai-studio)
* [Key Features](#key-features)
* [Workspace & Engine Architecture](#workspace-architecture)
* [Folder Architecture](#folder-architecture)
* [Getting Started](#getting-started)
* [Windows Setup](#windows-setup)
* [Linux Setup](#linux-setup)
* [macOS Setup](#macos-setup)
* [Hardware Compatibility & Acceleration](#hardware-compatibility-acceleration)
* [Troubleshooting & FAQ](#troubleshooting-faq)
* [Building From Source](#building-from-source)
* [Licensing](#licensing)
---
## ⚡ Quick Start
1. **Launch:** Double-click **`start.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.
## <a id="what-is-uncensored-ai-studio"></a>📖 What is Uncensored AI Studio?
**Uncensored AI Studio** is a completely offline, zero-setup, self-contained AI studio for Windows, Linux, and macOS. Unlike cloud-based AI systems, it runs entirely on your own hardware with no censorship, tracking, subscriptions, or login requirements.
It unifies four major local AI capabilities into one high-performance desktop interface:
1. **🎨 Image Generation (Stable Diffusion):** Generate and edit high-quality images offline using `.safetensors`, `.gguf`, or `.ckpt` model weights.
2. **💬 Text Chat (LLMs):** Converse privately with open-source language models (GGUF format) powered by official, high-performance `llama.cpp` backends.
3. **🎙️ Speech-to-Text (Whisper):** Transcribe voice recordings and speech to text in real-time with an integrated `whisper.cpp` engine.
4. **🗣️ Text-to-Speech (Kokoro TTS):** Convert text outputs into highly natural, lifelike vocal audio offline using the `Kokoro-82M` ONNX model.
---
## ✨ Features
* **100% Offline & Private:** Inference runs completely locally on your hardware.
* **Auto-Detected GPU Acceleration:** Configures **CUDA** for Nvidia cards, and **Vulkan** for AMD or Intel Arc GPUs.
* **Zero System Footprint:** Node.js is sandboxed inside the folder. No global environment paths are altered.
* **Integrated Model Manager:** Paste a Hugging Face URL to download weights directly, or drag-and-drop local weight files to import them.
* **Real-time Telemetry:** Monitor RAM, VRAM, CPU, and GPU load directly in the UI.
* **Local Gallery:** Saves generated PNGs alongside prompt metadata JSONs to `app/outputs/`.
## <a id="key-features"></a>🌟 Key Features
* **100% Offline & Private:** Run inferences locally. No internet, telemetry, cloud logging, or API keys required.
* **Zero-Install Portability:** Entire runtime (Node.js, models, GPU backends) is self-contained. Zero global system environment changes.
* **Auto-Configured Acceleration:** Auto-detects hardware specs to load CUDA (Nvidia), ROCm (AMD), Vulkan (Intel/AMD/NVIDIA), Metal (macOS), or OpenVINO (Intel NPU) backends.
* **Integrated Model Manager:** Paste Hugging Face URLs to download weights directly, or drag-and-drop local weights to import them.
* **Live Performance Monitor:** Track CPU, RAM, GPU, and VRAM utilization in real-time directly inside the web UI.
* **Local Output Gallery:** Saves generated images side-by-side with prompt parameters and metadata JSON files.
---
## 📁 Repository Structure
## <a id="workspace-architecture"></a>⚙️ Workspace & Engine Architecture
To avoid exhausting system RAM or VRAM, text and image engines are mutually exclusive by default. You can switch between workspaces inside the UI:
* **Image Generation Workspace:** Uses a dedicated `stable-diffusion.cpp` backend node. Model weights are stored in `app/models/`.
* **Text Chat Workspace:** Uses a portable `llama.cpp` server backend. Model weights (.gguf) are stored in `app/llm-models/`. A small Qwen2.5 Coder starter model can be downloaded directly from the Text Chat panel.
* **Speech Worker (Whisper):** Runs a localized `whisper-cli` process to convert your vocal input to text.
* **Audio Output (Kokoro TTS):** Utilizes `kokoro-js` locally on the server side to read responses in natural voices.
---
## <a id="folder-architecture"></a>📁 Folder Architecture
```
local-ai-image-generator/
├── start.bat # Main double-click entrypoint
├── LICENSE # MIT Open Source license
├── .gitignore
├── README.md
Uncensored-AI-Studio/
├── windows.bat # Windows Launcher (Double-click entrypoint)
├── linux.sh # Linux Launcher (Terminal entrypoint)
├── mac.sh # macOS Launcher (Terminal entrypoint)
├── LICENSE # MIT Open Source License
├── .gitignore # Excludes models and output images from version control
├── README.md # Detailed system documentation
├── scripts/
│ ├── setup.ps1 # Automated GPU-detect and environment installer
│ ├── reset.ps1 # Cleans runtime environments (keeps models & outputs)
│ └── serve.cjs # UI web server and backend lifecycle manager
│ ├── setup/ # Platform setup and backend installers
│ ├── reset/ # Clean install & environment repair
│ ├── server/ # UI web server and backend lifecycle manager
│ ├── workers/ # Local worker processes
│ ├── build/ # Optional source build helpers
│ └── config/ # Runtime configuration catalogs
└── app/
├── frontend/ # UI source code (Vite + React)
├── models/ # Place weights here (.safetensors, .gguf, .ckpt)
├── models/ # Place image weights here (.safetensors, .gguf, .ckpt)
├── llm-models/ # Place text GGUF weights here
└── outputs/ # Saved images and parameters metadata
```
---
## 🖥️ GPU Compatibility Matrix
## <a id="getting-started"></a>🚀 Getting Started
Ensure you have a modern web browser installed. Follow the quick guide below for your platform:
### Windows Setup
1. **Launch:** Double-click **`windows.bat`**.
> [!NOTE]
> On the first run, the script will automatically download a portable Node.js runtime and configure pre-compiled GPU/CPU backend binaries.
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 Setup
1. **Make executable:** Open a terminal in the project folder and make the script executable:
```bash
chmod +x linux.sh
```
2. **Launch:** Run **`./linux.sh`**.
- **NVIDIA GPU Users:** You will be prompted to set up the high-performance **CUDA** backend (downloads prebuilt or automatically compiles from source as a fallback).
- **AMD Radeon Performance:** Run with **`./linux.sh --max-perf`** to add the ROCm backend (~1.3 GB download).
- **Intel Core Ultra NPU:** Run with **`./linux.sh --setup-openvino`** to configure Intel NPU support (requires Intel Linux NPU driver).
3. **Add Models:** Drop your weights into `app/models/` or download them via the **Model Manager** tab.
4. **Generate:** Open `http://localhost:1420` in your browser.
### macOS Setup
1. **Make executable:** Open a terminal in the project folder and make the script executable:
```bash
chmod +x mac.sh
```
2. **Launch:** Run **`./mac.sh`**.
> [!IMPORTANT]
> The prebuilt macOS backend is optimized for **Apple Silicon (M1 or newer)** and uses **Metal** GPU acceleration. *(macOS Intel hardware is completely unsupported)*.
3. **Add Models:** Drop your weights into `app/models/` or download them via the **Model Manager** tab.
4. **Generate:** Open `http://localhost:1420` in your browser.
---
## <a id="hardware-compatibility-acceleration"></a>🖥️ Hardware Compatibility & Acceleration
### Windows
| GPU Vendor | Tech | Status | Notes |
| :--- | :--- | :--- | :--- |
@@ -71,24 +149,120 @@ local-ai-image-generator/
| **Intel Arc** | Vulkan | ✅ Native | Maps `sd-vulkan.exe` for Intel hardware. |
| **Integrated / None** | CPU | ⚠️ Fallback | Runs on logical CPU threads (slow). |
---
### Linux
## ⏱️ Performance Benchmarks
| GPU Vendor | Primary | Fallback | Notes |
| :--- | :--- | :--- | :--- |
| **NVIDIA** | CUDA / Vulkan | Vulkan / CPU | Auto-detects NVIDIA. Prompt-driven CUDA setup downloads prebuilt or compiles from source. Falls back to Vulkan for GTX. |
| **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). |
Typical generation times for an image with **20 steps** (e.g. 512x512 resolution; actual times can vary depending on specific hardware specifications, clock speeds, and system load):
### macOS
* **CUDA GPU (Nvidia RTX):** ~10 seconds.
* **Vulkan GPU (AMD / Intel Arc):** ~89 seconds.
* **GTX Vulkan Fallback (Nvidia GTX):** ~30 seconds (Vulkan runs significantly faster on legacy GTX series cards since they lack Tensor Cores).
* **CPU (Fallback):** ~150 - 300+ seconds (highly dependent on processor core count, speed, and AVX instruction sets).
| Hardware | Primary | Fallback | Notes |
| :--- | :--- | :--- | :--- |
| **Apple Silicon (M1 or newer)** | Metal | CPU | Uses the official Darwin arm64 stable-diffusion.cpp backend. |
> [!IMPORTANT]
> **System Requirements & Notes:**
> - **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.
> - **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.
---
## 🛠️ Troubleshooting
* **Reset Environment:** If a build fails or you want to clear dependencies, run `scripts/reset.ps1`. (This preserves your models and generated images).
* **Port Conflicts:** The frontend uses `1420` by default. The backend tries `8080` first, then automatically falls back to a free port if `8080` is already busy.
## <a id="troubleshooting-faq"></a>🛠️ Troubleshooting & FAQ
<details>
<summary><strong> Reset Environment: If a build fails or you want to clear dependencies</strong></summary>
<p>Run <code>scripts/reset/reset.ps1</code> (Windows) or <code>scripts/reset/reset.sh</code> (Linux/macOS). This will clear temporary compilation and package caches to repair your environment. <em>(Note: This preserves your model weights and generated output images).</em></p>
</details>
<details>
<summary><strong> Linux backends fail to start with <code>GLIBC_2.38 not found</code></strong></summary>
<p>The prebuilt binaries require glibc 2.38+ (e.g. Ubuntu 24.04). If your distribution uses an older glibc version, you can upgrade your operating system or compile the backend from source (see the <a href="#building-from-source">Building From Source</a> guide below).</p>
</details>
<details>
<summary><strong> Port Conflicts: Default port address already busy</strong></summary>
<p>The web user interface runs on port <code>1420</code> by default. The GPU backend manager attempts to bind to port <code>8080</code> first, then automatically detects and falls back to a free system port if <code>8080</code> is already occupied.</p>
</details>
<details>
<summary><strong> Linux ROCm not loading for AMD Radeon GPUs</strong></summary>
<p>Ensure your AMD GPU hardware and host kernel are fully compatible with ROCm 7.13. If ROCm fails to initialize correctly, the application will automatically fall back to Vulkan acceleration.</p>
</details>
<details>
<summary><strong> Windows exits with code <code>3221225781</code> (0xC0000135)</strong></summary>
<p>This code means Windows could not locate a required backend DLL:</p>
<ul>
<li><strong>For AMD/Intel Vulkan:</strong> Update your GPU driver to one with full Vulkan runtime support, then rerun the setup script to restore <code>app/backend/win/vulkan/</code>.</li>
<li><strong>For NVIDIA CUDA:</strong> Install or update your NVIDIA graphics driver, then rerun the setup script to restore the CUDA runtime DLLs.</li>
</ul>
</details>
<details>
<summary><strong> Generation shows "server is not responding or crashed"</strong></summary>
<p>This indicates that the local backend engine process terminated. Check your launch terminal (where you executed <code>windows.bat</code>, <code>./linux.sh</code>, or <code>./mac.sh</code>) for the exact console error. Common causes include glibc version mismatches, missing Vulkan drivers, or system out-of-memory (OOM) issues.</p>
</details>
---
## 📝 License
## <a id="building-from-source"></a>🔨 Building From Source
The setup script (`scripts/setup/setup.sh`) now automates building and setting up the CUDA backend from source when selected. If you want to manually build all backends (CPU, Vulkan, and CUDA) at once, you can run the included `scripts/build/build_from_source.sh` script.
For macOS, the included `scripts/build/build_from_source.sh` builds the Metal backend and copies it to `app/backend/mac/sd`.
### Requirements
- `git`, `cmake`, `make` (or `ninja`), and a C++17 compiler (`g++` / `clang++`).
- For **CUDA**: the NVIDIA CUDA toolkit (`nvcc`) must be on your `PATH`.
- For **Vulkan**: the Vulkan SDK / loader and a compatible driver.
- For **ROCm**: AMD ROCm development libraries.
- For **macOS Metal**: Apple Command Line Tools or Xcode.
### Build commands
```bash
# 1. Clone upstream
git clone https://github.com/leejet/stable-diffusion.cpp.git
cd stable-diffusion.cpp
mkdir build && cd build
# 2. Configure for your backend (pick ONE)
# CPU only
cmake .. -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
# CUDA
cmake .. -DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
# Vulkan
cmake .. -DSD_VULKAN=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
# ROCm
cmake .. -DSD_HIPBLAS=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
# macOS Metal
cmake .. -DSD_METAL=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
# 3. Build
cmake --build . --config Release -j$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu)
# 4. Copy the binaries into this project
cp bin/sd* /path/to/Uncensored-AI-Studio/app/backend/linux/<backend>/
```
After copying, rename the server binary to match what `scripts/server/serve.cjs` expects:
- Vulkan: `sd` → `sd-vulkan`
- ROCm: `sd` → `sd-rocm`
Then restart the app with `./linux.sh` (Linux) or `./mac.sh` (macOS).
---
## <a id="licensing"></a>📝 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file. Bundles [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) (MIT License). Model weights are subject to their respective creators' licenses.
-1
View File
@@ -1 +0,0 @@
import{i as D,t as i}from"./index-jXnaCwmK.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};
-1
View File
@@ -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-coLine truncated
-121
View File
@@ -1,121 +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 N of x)if(N.type==="childList")for(const G of N.addedNodes)G.tagName==="LINK"&&G.rel==="modulepreload"&&r(G)}).observe(document,{childList:!0,subtree:!0});function E(x){const N={};return x.integrity&&(N.integrity=x.integrity),x.referrerPolicy&&(N.referrerPolicy=x.referrerPolicy),x.crossOrigin==="use-credentials"?N.credentials="include":x.crossOrigin==="anonymous"?N.credentials="omit":N.credentials="same-origin",N}function r(x){if(x.ep)return;x.ep=!0;const N=E(x);fetch(x.href,N)}})();function fm(f){return f&&f.__esModule&&Object.prototype.hasOwnProperty.call(f,"default")?f.default:f}var Bs={exports:{}},ai={};var Zd;function E0(){if(Zd)return ai;Zd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.fragment");function E(r,x,N){var G=null;if(N!==void 0&&(G=""+N),x.key!==void 0&&(G=""+x.key),"key"in x){N={};for(var H in x)H!=="key"&&(N[H]=x[H])}else N=x;return x=N.ref,{$$typeof:f,type:r,key:G,ref:x!==void 0?x:null,props:N}}return ai.Fragment=b,ai.jsx=E,ai.jsxs=E,ai}var Kd;function N0(){return Kd||(Kd=1,Bs.exports=E0()),Bs.exports}var c=N0(),Rs={exports:{}},le={};var Jd;function A0(){if(Jd)return le;Jd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.portal"),E=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),x=Symbol.for("react.profiler"),N=Symbol.for("react.consumer"),G=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"),U=Symbol.for("react.activity"),V=Symbol.iterator;function fe(d){return d===null||typeof d!="object"?null:(d=V&&d[V]||d["@@iterator"],typeof d=="function"?d:null)}var X={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},L=Object.assign,te={};function W(d,j,B){this.props=d,this.context=j,this.refs=te,this.updater=B||X}W.prototype.isReactComponent={},W.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")},W.prototype.forceUpdate=function(d){this.updater.enqueueForceUpdate(this,d,"forceUpdate")};function Oe(){}Oe.prototype=W.prototype;function de(d,j,B){this.props=d,this.context=j,this.refs=te,this.updater=B||X}var Ee=de.prototype=new Oe;Ee.constructor=de,L(Ee,W.prototype),Ee.isPureReactComponent=!0;var ke=Array.isArray;function Ae(){}var P={H:null,A:null,T:null,S:null},ve=Object.prototype.hasOwnProperty;function Ke(d,j,B){var q=B.ref;return{$$typeof:f,type:d,key:j,ref:q!==void 0?q:null,props:B}}function yt(d,j){return Ke(d.type,j,d.props)}function me(d){return typeof d=="object"&&d!==null&&d.$$typeof===f}function Je(d){var j={"=":"=0",":":"=2"};return"$"+d.replace(/[=:]/g,function(B){return j[B]})}var st=/\/+/g;function $e(d,j){return typeof d=="object"&&d!==null&&d.key!=null?Je(""+d.key):j.toString(36)}function Xe(d){switch(d.status){case"fulfilled":return d.value;case"rejected":throw d.reason;default:switch(typeof d.status=="string"?d.then(Ae,Ae):(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,B,q,K){var ee=typeof d;(ee==="undefined"||ee==="boolean")&&(d=null);var se=!1;if(d===null)se=!0;else switch(ee){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,B,q,K)}}if(se)return K=K(d),se=q===""?"."+$e(d,0):q,ke(K)?(B="",se!=null&&(B=se.replace(st,"$&/")+"/"),_(K,j,B,"",function(gt){return gt})):K!=null&&(me(K)&&(K=yt(K,B+(K.key==null||d&&d.key===K.key?"":(""+K.key).replace(st,"$&/")+"/")+se)),j.push(K)),1;se=0;var Ze=q===""?".":q+":";if(ke(d))for(var Ne=0;Ne<d.length;Ne++)q=d[Ne],ee=Ze+$e(q,Ne),se+=_(q,j,B,ee,K);else if(Ne=fe(d),typeof Ne=="function")for(d=Ne.call(d),Ne=0;!(q=d.next()).done;)q=q.value,ee=Ze+$e(q,Ne++),se+=_(q,j,B,ee,K);else if(ee==="object"){if(typeof d.then=="function")return _(Xe(d),j,B,q,K);throw j=String(d),Error("Objects are not valid as a React child (found: "+(j==="[object Object]"?"object with keys {"+Object.keys(d).join(", ")+"}":j)+"). If you meant to render a collection of children, use an array instead.")}return se}function C(d,j,B){if(d==null)return d;var q=[],K=0;return _(d,q,"","",function(ee){return j.call(B,ee,K++)}),q}function J(d){if(d._status===-1){var j=d._result;j=j(),j.then(function(B){(d._status===0||d._status===-1)&&(d._status=1,d._result=B)},function(B){(d._status===0||d._status===-1)&&(d._status=2,d._result=B)}),Line truncated
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
`+We+e+Ht}var Z=!1;function ie(e,t){if(!e||Z)return"";Z=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(t){var M=function(){throw Error()};if(Object.defineProperty(M.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(M,[])}catch(S){var v=S}Reflect.construct(e,[],M)}else{try{M.call()}catch(S){v=S}e.call(M.prototype)}}else{try{throw Error()}catch(S){v=S}(M=e())&&typeof M.catch=="function"&&M.catch(function(){})}}catch(S){if(S&&v&&typeof S.stack=="string")return[S.stack,v.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var i=a.DetermineComponentFrameRoot(),u=i[0],s=i[1];if(u&&s){var o=u.split(`
`),p=s.split(`
`);for(n=a=0;a<o.length&&!o[a].includes("DetermineComponentFrameRoot");)a++;for(;n<p.length&&!p[n].includes("DetermineComponentFrameRoot");)n++;if(a===o.length||n===p.length)for(a=o.length-1,n=p.length-1;1<=a&&0<=n&&o[a]!==p[n];)n--;for(;1<=a&&0<=n;a--,n--)if(o[a]!==p[n]){if(a!==1||n!==1)do if(a--,n--,0>n||o[a]!==p[n]){var z=`
`+o[a].replace(" at new "," at ");return e.displayName&&z.includes("<anonymous>")&&(z=z.replace("<anonymous>",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{Z=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?Tt(l):""}function D(e,t){switch(e.tag){case 26:case 27:case 5:return Tt(e.type);case 16:return Tt("Lazy");case 13:return e.child!==t&&t!==null?Tt("Suspense Fallback"):Tt("Suspense");case 19:return Tt("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 Tt("Activity");default:return""}}function R(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 F=Object.prototype.hasOwnProperty,ae=f.unstable_scheduleCallback,At=f.unstable_cancelCallback,Gt=f.unstable_shouldYield,dl=f.unstable_requestPaint,Re=f.unstable_now,al=f.unstable_getCurrentPriorityLevel,Fe=f.unstable_ImmediatePriority,za=f.unstable_UserBlockingPriority,Ta=f.unstable_NormalPriority,Y=f.unstable_LowPriority,ye=f.unstable_IdlePriority,He=f.log,pt=f.unstable_setDisableYieldValue,ft=null,tt=null;function qt(e){if(typeof He=="function"&&pt(e),tt&&typeof tt.setStrictMode=="function")try{tt.setStrictMode(ft,e)}catch{}}var rt=Math.clz32?Math.clz32:xe,vt=Math.log,Se=Math.LN2;function xe(e){return e>>>=0,e===0?32:31-(vt(e)/Se|0)|0}var Ge=256,_e=262144,Ie=4194304;function je(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=je(a):(u&=s,u!==0?n=je(u):l||(l=s&~e,l!==0&&(n=je(l))))):(s=a&~i,s!==0?n=je(s):u!==0?n=je(u):l||(l=a&~e,l!==0&&(n=je(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 bt(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=Ie;return Ie<<=1,(Ie&62914560)===0&&(Ie=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<l;){var z=31-rt(l),M=1<<z;s[z]=0,o[z]=-1;var v=p[z];if(v!==null)for(p[z]=null,z=0;z<v.length;z++){var S=v[z];S!==null&&(S.lane&=-536870913)}l&=~M}a!==0&&$s(e,a,0),i!==0&&n===0&&e.tag!==0&&(e.suspendedLanes|=i&~(u&~t))}function $s(e,t,l){e.pendingLanes|=t,e.suspendedLanes&=~t;var a=31-rt(t);e.entangledLanes|=t,e.entanglements[a]=e.entanglements[a]|1073741824|l&261930}function Ws(e,t){var l=e.entangledLanes|=t;for(e=e.entanglements;l;){var a=31-rt(l),n=1<<a;n&t|e[a]&t&&(e[a]|=t),l&=~n}}function Fs(e,t){var l=t&-t;return l=(l&42)!==0?1:zu(l),(l&(e.suspendedLanes|t))!==0?0:l}function zu(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;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:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Tu(e){return e&=-e,2<e?8<e?(e&134217727)!==0?32:268435456:8:2}function Is(){var e=C.p;return e!==0?e:(e=window.event,e===void 0?32:qd(e.type))}function Ps(e,t){var l=C.p;try{return C.p=e,t()}finally{C.p=l}}var Ol=Math.random().toString(36).slice(2),St="__reactFiber$"+Ol,Dt="__reactProps$"+Ol,Ma="__reactContainer$"+Ol,Mu="__reactEvents$"+Ol,gm="__reactListeners$"+Ol,pm="__reactHandles$"+Ol,ef="__reactResources$"+Ol,hn="__reactMarker$"+Ol;function Eu(e){delete e[St],delete e[Dt],delete e[Mu],delete e[gm],delete e[pm]}function Ea(e){var t=e[St];if(t)return t;for(var l=e.parentNode;l;){if(t=l[Ma]||l[St]){if(l=t.alternate,t.child!==null||l!==null&&l.child!==null)for(e=jd(e);e!==null;){if(l=e[St])return l;e=jd(e)}return t}e=l,l=e.parentNode}return null}function Na(e){if(e=e[St]||e[Ma]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function yn(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(r(33))}function Aa(e){var t=e[ef];return t||(t=e[ef]={hoistableStyles:new Map,hoistableScripts:new Map}),t}function mt(e){e[hn]=!0}var tf=new Set,lf={};function ua(e,t){Da(e,t),Da(e+"Capture",t)}function Da(e,t){for(lf[e]=t,e=0;e<t.length;e++)tf.add(t[e])}var vm=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F Line truncated
`).replace($h,"")}function md(e,t){return t=dd(t),dd(e)===t}function we(e,t,l,a,n,i){switch(l){case"children":typeof a=="string"?t==="body"||t==="textarea"&&a===""||Oa(e,a):(typeof a=="number"||typeof a=="bigint")&&t!=="body"&&Oa(e,""+a);break;case"className":fi(e,"class",a);break;case"tabIndex":fi(e,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":fi(e,l,a);break;case"style":df(e,a,i);break;case"data":if(t!=="object"){fi(e,"data",a);break}case"src":case"href":if(a===""&&(t!=="a"||l!=="href")){e.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){e.removeAttribute(l);break}a=ri(""+a),e.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){e.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof i=="function"&&(l==="formAction"?(t!=="input"&&we(e,t,"name",n.name,n,null),we(e,t,"formEncType",n.formEncType,n,null),we(e,t,"formMethod",n.formMethod,n,null),we(e,t,"formTarget",n.formTarget,n,null)):(we(e,t,"encType",n.encType,n,null),we(e,t,"method",n.method,n,null),we(e,t,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){e.removeAttribute(l);break}a=ri(""+a),e.setAttribute(l,a);break;case"onClick":a!=null&&(e.onclick=hl);break;case"onScroll":a!=null&&re("scroll",e);break;case"onScrollEnd":a!=null&&re("scrollend",e);break;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(r(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(r(60));e.innerHTML=l}}break;case"multiple":e.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":e.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){e.removeAttribute("xlink:href");break}l=ri(""+a),e.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,""+a):e.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,""):e.removeAttribute(l);break;case"capture":case"download":a===!0?e.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,a):e.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?e.setAttribute(l,a):e.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?e.removeAttribute(l):e.setAttribute(l,a);break;case"popover":re("beforetoggle",e),re("toggle",e),si(e,"popover",a);break;case"xlinkActuate":ml(e,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":ml(e,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":ml(e,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":ml(e,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":ml(e,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":ml(e,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":ml(e,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":ml(e,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":ml(e,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":si(e,"is",a);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=jm.get(l)||l,si(e,l,a))}}function ps(e,t,l,a,n,i){switch(l){case"style":df(e,a,i);break;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(r(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(r(60));e.innerHTML=l}}break;case"children":typeof a=="string"?Oa(e,a):(typeof a=="number"||typeof a=="bigint")&&Oa(e,""+a);break;case"onScroll":a!=null&&re("scroll",e);break;case"onScrollEnd":a!=null&&re("scrollend",e);break;case"onClick":a!=null&&(e.onclick=hl);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne Line truncated
<!-- Stars -->
<circle cx="80" cy="100" r="1.5" fill="#ffffff" opacity="0.6"/>
<circle cx="160" cy="60" r="1" fill="#ffffff" opacity="0.5"/>
<circle cx="280" cy="120" r="2" fill="#ffffff" opacity="0.8"/>
<circle cx="360" cy="80" r="1.5" fill="#ffffff" opacity="0.4"/>
<circle cx="440" cy="150" r="1" fill="#ffffff" opacity="0.7"/>
<circle cx="400" cy="280" r="2" fill="#ffffff" opacity="0.9"/>
<circle cx="100" cy="300" r="1.5" fill="#ffffff" opacity="0.5"/>
<!-- Moon -->
<circle cx="256" cy="220" r="90" fill="url(#moonGrad)" />
<circle cx="226" cy="190" r="80" fill="#0F172A" />
`):L.includes("cat")||L.includes("kitten")||L.includes("feline")?(W="#4F46E5",Oe="#7C3AED",Ee="Stylized Cat Portrait",de=`
<!-- Cat head & ears -->
<polygon points="176,280 146,160 216,210" fill="#312E81" />
<polygon points="336,280 366,160 296,210" fill="#312E81" />
<circle cx="256" cy="270" r="80" fill="#4338CA" />
<!-- Eyes -->
<ellipse cx="226" cy="260" rx="14" ry="8" fill="#10B981" />
<ellipse cx="286" cy="260" rx="14" ry="8" fill="#10B981" />
<circle cx="226" cy="260" r="4" fill="#000" />
<circle cx="286" cy="260" r="4" fill="#000" />
<!-- Nose & Whiskers -->
<polygon points="256,285 248,275 264,275" fill="#F43F5E" />
<line x1="226" y1="290" x2="166" y2="280" stroke="#E2E8F0" stroke-width="2" />
<line x1="226" y1="295" x2="156" y2="295" stroke="#E2E8F0" stroke-width="2" />
<line x1="286" y1="290" x2="346" y2="280" stroke="#E2E8F0" stroke-width="2" />
<line x1="286" y1="295" x2="356" y2="295" stroke="#E2E8F0" stroke-width="2" />
`):L.includes("dog")||L.includes("puppy")||L.includes("canine")?(W="#D97706",Oe="#B45309",Ee="Friendly Dog Portrait",de=`
<!-- Dog head & floppy ears -->
<ellipse cx="166" cy="260" rx="20" ry="60" fill="#78350F" />
<ellipse cx="346" cy="260" rx="20" ry="60" fill="#78350F" />
<circle cx="256" cy="250" r="70" fill="#92400E" />
<ellipse cx="256" cy="280" rx="40" ry="30" fill="#D97706" />
<!-- Eyes -->
<circle cx="226" cy="230" r="10" fill="#1E293B" />
<circle cx="286" cy="230" r="10" fill="#1E293B" />
<circle cx="222" cy="226" r="3" fill="#ffffff" />
<circle cx="282" cy="226" r="3" fill="#ffffff" />
<!-- Nose -->
<ellipse cx="256" cy="275" rx="16" ry="10" fill="#1E293B" />
`):L.includes("city")||L.includes("cyberpunk")||L.includes("tokyo")||L.includes("neon")?(W="#111827",Oe="#311042",Ee="Neon Cyberpunk Skyline",de=`
<!-- Skyline -->
<rect x="60" y="160" width="80" height="352" fill="#1F2937" opacity="0.9"/>
<rect x="180" y="100" width="100" height="412" fill="#111827" />
<rect x="320" y="200" width="120" height="312" fill="#1F2937" opacity="0.85"/>
<rect x="100" y="240" width="110" height="272" fill="#374151" opacity="0.6"/>
<!-- Neon details -->
<line x1="230" y1="100" x2="230" y2="40" stroke="#EC4899" stroke-width="3" />
<rect x="200" y="140" width="60" height="15" fill="#10B981" opacity="0.7"/>
<rect x="200" y="170" width="60" height="15" fill="#10B981" opacity="0.7"/>
<rect x="200" y="200" width="60" height="15" fill="#3B82F6" opacity="0.7"/>
<rect x="70" y="200" width="15" height="100" fill="#F59E0B" opacity="0.5"/>
<rect x="340" y="250" width="20" height="150" fill="#EF4444" opacity="0.6"/>
`):L.includes("car")||L.includes("vehicle")||L.includes("supercar")?(W="#DC2626",Oe="#991B1B",Ee="Futuristic Concept Car",de=`
<!-- Road / Perspective grid -->
<line x1="256" y1="350" x2="0" y2="512" stroke="#4B5563" stroke-width="3" />
<line x1="256" y1="350" x2="512" y2="512" stroke="#4B5563" stroke-width="3" />
<!-- Car body -->
<polygon points="126,380 256,330 386,380 346,440 166,440" fill="#EF4444" />
<polygon points="176,370 256,340 336,370 326,390 186,390" fill="#1E293B" opacity="0.9" />
<!-- Headlights -->
<polygon points="136,410 176,415 166,425 136,420" fill="#FBBF24" />
<polygon points="376,410 336,415 346,425 376,420" fill="#FBBF24" />
<!-- Wheels -->
<ellipse cx="156" cy="440" rx="30" ry="15" fill="#111" />
<ellipse cx="356" cy="440" rx="30" ry="15" fill="#111" />
`):(W="#06B6D4",Oe="#0891B2",Ee="Abstract Creative Concept",de=`
<path d="M0,256 C150,150 350,350 512,256 L512,512 L0,512 Z" fill="#0891B2" opacity="0.4" />
<path d="M0,350 C200,200 300,450 512,350 L512,512 L0,512 Z" fill="#0E7490" opacity="0.6" />
<circle cx="380" cy="180" r="50" fill="#F59E0B" opacity="0.8" />
`);const ke=`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${W}" />
<stop offset="100%" stop-color="${Oe}" />
</linearGradient>
<linearGradient id="moonGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FEF08A" />
<stop offset="100%" stop-color="#FDE047" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="512" height="512" fill="url(#bgGrad)" />
<!-- Shapes -->
${de}
<!-- Premium UI Overlay Frame -->
<rect x="20" y="20" width="472" height="472" fill="none" stroke="#ffffff" stroke-width="1.5" opacity="0.15" rx="8"/>
<!-- Prompt & Details text -->
<rect x="40" y="410" width="432" height="62" fill="#0F172A" opacity="0.85" rx="6" />
<text x="60" y="435" fill="#F1F5F9" font-family="system-ui, -apple-system, sans-serif" font-size="13" font-weight="bold">${Ee}</text>
<text x="60" y="455" fill="#94A3B8" font-family="system-ui, -apple-system, sans-serif" font-size="11">Seed: ${te} • Steps: ${h.steps} • Scale: ${h.cfg_scale}</text>
</svg>
`,P=`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(ke)))}`,ve=parseFloat(((Date.now()-H)/1e3).toFixed(1));return{image:P,seed:h.seed,duration_sec:ve}},U=!!h.image;let V=`${w}/v1/images/generations`,fe={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(U){const X=String(h.image).replace(/^data:[^;]+;base64,/,"");V=`${w}/sdapi/v1/img2img`,fe={init_images:[X],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 X=await fetch(V,{method:"POST",headers:{"Content-Type":"application/json"},signal:G,body:JSON.stringify(fe)});if(X.ok){const L=await X.json(),te=L?.data?.[0]?.b64_json??L?.images?.[0];if(te){const W=parseFloat(((Date.now()-H)/1e3).toFixed(1));return{image:`data:image/png;base64,${te}`,seed:L.data?.[0]?.seed??h.seed,duration_sec:W}}}else{let L=`Generation failed (HTTP ${X.status})`;try{const te=await X.json();(te?.detail||te?.error?.message)&&(L=te.detail||te.error.message)}catch{}throw new Error(L)}}catch(X){if(X.name==="AbortError"||X.message.startsWith("Generation failed"))throw X;console.warn("Could not reach local server. Falling back to simulation mode.",X)}return await A()}async function Jy(f,b,E){if(f instanceof File){const H=f;if(!na())throw new Error("File import requires the local image generator server.");return await $y(H,b,E),{response:"Model imported successfully"}}if(ia()){const{listen:H}=await Uy(async()=>{const{listen:h}=await import("./event-DeDJUvqC.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(E?.aborted)throw new DOMException("The user aborted a request.","AbortError");await new Promise(U=>setTimeout(U,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 N=localStorage.getItem("imported-models"),G=N?JSON.parse(N):[];return G.includes(r)||(G.push({filename:r,sizeBytes:0,size:"Unknown"}),localStorage.setItem("imported-models",JSON.stringify(G))),{response:"Model imported successfully"}}function $y(f,b,E){return new Promise((r,x)=>{const N=new XMLHttpRequest,G=Date.now();let H=!1;N.open("POST",`/api/import-model?filename=${encodeURIComponent(f.name)}`),N.setRequestHeader("Content-Type","application/octet-stream");const w=()=>{H=!0,N.abort()};E&&(E.aborted?w():E.addEventListener("abort",w,{once:!0})),N.upload.onprogress=h=>{if(!h.lengthComputable||!b)return;const A=Math.max(.1,(Date.now()-G)/1e3),U=h.loaded/A,V=Math.max(0,h.total-h.loaded);b({filename:f.name,progress:h.loaded/h.total*100,speed_mb_s:U/(1024*1024),eta_secs:V/Math.max(1,U),status:"Copying to models folder..."})},N.onload=()=>{let h={};try{h=JSON.parse(N.responseText||"{}")}catch{}N.status>=200&&N.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 ${N.status})`))},N.onerror=()=>x(new Error("Import failed while uploading the file.")),N.onabort=()=>x(new DOMException(H?"Import cancelled by user.":"Import aborted.","AbortError")),N.send(f)})}async function Wy(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})}),N=await x.json();if(!x.ok)throw new Error(N.error||"Failed to delete model file");return N}catch(x){throw console.error("Failed to delete model via local API:",x),x}const E=localStorage.getItem("imported-models");let r=E?JSON.parse(E):[];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 Fy(f=30){const b=ia()||na()?f:3;for(let E=0;E<b;E++){let r=!1;if(na()?r=!!(await Cl()).ready:r=await hm(),r)return!0;await new Promise(x=>setTimeout(x,500))}return!1}async function Iy(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 Py(){try{return await(await fetch("/api/ca Line truncated
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"}),E&&c.jsx("div",{className:"gallery-select-badge",children:c.jsx(ks,{size:14})})]})});function lg({prompt:f,setPrompt:b,negativePrompt:E,setNegativePrompt:r,constraints:x,setConstraints:N,activeModel:G,generatedImages:H,setGeneratedImages:w,isGenerating:h,setIsGenerating:A,generationProgress:U,setGenerationProgress:V,setActiveTab:fe,showAlert:X=async({message:te})=>window.alert(te),showConfirm:L=async({message:te})=>window.confirm(te)}){const[te,W]=O.useState(null),[Oe,de]=O.useState(null),[Ee,ke]=O.useState(null),[Ae,P]=O.useState(!1),[ve,Ke]=O.useState(0),[yt,me]=O.useState(0),[Je,st]=O.useState(0),[$e,Xe]=O.useState(""),[_,C]=O.useState(!1),[J,he]=O.useState(!1),[ce,d]=O.useState(!1),[j,B]=O.useState(null),[q,K]=O.useState(null),[ee,se]=O.useState(!1),[Ze,Ne]=O.useState([]),[gt,Et]=O.useState(null),We=O.useRef(null),Ht=O.useRef(null),Tt=O.useRef(!1),Z=Y=>{const ye=Y.target.files?.[0];if(Y.target.value="",!ye)return;if(!ye.type.startsWith("image/")){K("Please choose an image file (PNG, JPG, or WEBP).");return}const He=new FileReader;He.onload=()=>Et(He.result),He.onerror=()=>K("Could not read the selected image file."),He.readAsDataURL(ye)},ie=()=>Et(null),D=async()=>{if(!f.trim())return;K(null),d(!0),B(null);let Y=!1;try{const Se=await Cl();if(Se&&Se.settings){const xe=Se.settings,Ge=xe.model?xe.model.split(/[\\/]/).pop():null,_e=G?G.split(/[\\/]/).pop():null;(Ge!==_e||parseInt(xe.steps)!==parseInt(x.steps)||Math.abs(parseFloat(xe.cfgScale)-parseFloat(x.cfgScale))>.05||xe.sampler!==x.sampler||parseInt(xe.threads)!==parseInt(x.threads)||!!xe.useGpu!=(x.useGpu!==!1)||(xe.backendType||(xe.useGpu===!1?"cpu":"auto"))!==(x.backendType||(x.useGpu===!1?"cpu":"auto")))&&(Y=!0)}else Y=!0}catch{Y=!0}if(Y)try{console.log("Settings out of sync, restarting backend..."),await Vs(G,x);let Se=!1,xe=null;for(let Ge=0;Ge<240;Ge++){const _e=await Cl();if(_e.loading&&B({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,backendMode:_e.loading.backendMode||_e.settings?.backendMode||"",device:_e.loading.device||_e.settings?.backendDevice||""}),_e.ready){Se=!0;break}if(_e.error){xe=_e.error;break}if(!_e.running&&Ge>3){xe="The backend process terminated immediately on startup.";break}await new Promise(Ie=>setTimeout(Ie,500))}if(!Se)throw new Error(xe||"Backend server failed to start after applying configuration changes.")}catch(Se){console.error("Failed to restart backend:",Se),K(Se.message||"Failed to sync settings with backend."),d(!1);return}d(!1),B(null),We.current&&clearInterval(We.current),A(!0),V(0),Ke(0),st(0),Xe(""),Tt.current=!1,C(!1),se(!1),ke(null),W(null);const ye=(x.width>=1024?140:35)*.04,He=x.width>=1024?140:35,pt=(x.backendType||(x.useGpu===!1?"cpu":"auto"))!=="cpu";let ft=pt?ye:He,tt=!1,qt=0,rt=0,vt=0;me(Math.round(x.steps*ft)),We.current=setInterval(async()=>{vt++,Ke(vt);try{const Se=await eg();if(Se&&Se.active){const xe=Se.decoding||!1;se(xe);const Ge=xe?Se.steps||x.steps||20:Se.step||0,_e=Se.steps||x.steps||20,Ie=xe?"":Se.speed||"";(xe||Se.steps>0)&&(Tt.current=!0,st(Ge)),Xe(xe?"decoding":Ie);let je=0;xe?je=99:_e>0&&(je=Math.round(Ge/_e*100)),je=Math.min(99,je),xe||Se.steps>0?V(je):V(bt=>Math.max(bt,je));let dt=0;if(xe)dt=3;else{const bt=parseFloat(Ie);if(!isNaN(bt)&&bt>0){const cl=Math.max(0,_e-Ge);Ie.includes("it/s")?dt=cl/bt:Ie.includes("s/it")&&(dt=cl*bt)}else{const cl=pt?ye:He;dt=Math.max(0,(_e-Ge)*cl)}}if(pt&&!tt){const bt=Ie.includes("it/s")&&numericSpeed<.2,cl=Ie.includes("s/it")&&numericSpeed>5;(bt||cl)&&(tt=!0,C(!0))}me(Math.round(dt))}else V(xe=>Math.min(15,xe+1)),me(xe=>Math.max(1,xe-1))}catch(Se){console.warn("Failed to get generation progress, running simulation fallback:",Se),pt&&!tt&&vt>x.steps*ye*1.3&&(tt=!0,C(!0),ft=He,qt=vt,rt=Math.min(95,Math.round(vt/(x.steps*ye)*100)));let xe=Math.min(x.steps-1,Math.floor(vt/ft)),Ge=x.steps*ft;if(vt>=Ge){const je=Math.ceil(vt/Math.max(.5,xe));ft=Math.max(ft+15,je+15),Ge=x.steps*ft,xe=Math.min(x.steps-1,Math.floor(vt/ft))}const _e=Math.max(1,Ge-vt);me(Math.round(_e));let Ie=0;if(tt){const je=99-rt,dt=Ge-qt;if(dt>0){const bt=Math.min(1,(vt-qt)/dt);if(bt<.85)Ie=Math.round(rt+bt*je);else{const cl=(vt-(qt+dt*.85))/(ft*2),ci=1-Math.exp(-cl);Ie=Math.min(99,Math.round(rt+.85*je+.14*je*ci))}}else Ie=99}else if(vt<Ge*.85)Ie=Math.round(vt/Ge*100);else{const je=(vt-Ge*.85)/(ft*2),dt=1-Math.exp(-je);Ie=Math.min(99,Math.round(85+14*dt))}Tt.current||V(je=>Math.max(je,Ie))}},1e3);try{Ht.current=new AbortController;const Se=await Ky(f,E,x,G,gt,je=>V(dt=>Math.max(dt,je)),Ht.current.signal);We.current&&clearInterval(We.current),V(100),st(x.steps),me(0),Xe("");const xe={prompt:f,negativePrompt:E,seed:Se.seed,steps:x.steps,cfgScale:x.cfgScale,width:x.width,height:x.height,sampler:x.sampler,model:G,mode:gt?"img2img":"txt2Line truncated
${dl}
Update the UI status anyway?`,confirmLabel:"Update UI"})&&(b(D),r(!0)):await G({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}te(D),Oe({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,Re=null;for(let al=0;al<240;al++){const Fe=await Cl();if(Fe.loading&&Oe({progress:Fe.loading.progress||0,phase:Fe.loading.phase||"Loading model...",speed:Fe.loading.speed||"",current:Fe.loading.current||0,total:Fe.loading.total||0,model:Fe.loading.model||D,backendMode:Fe.loading.backendMode||Fe.settings?.backendMode||"",backendBinary:Fe.loading.backendBinary||Fe.settings?.backendBinary||"",device:Fe.loading.device||Fe.settings?.backendDevice||""}),Fe.ready){dl=!0;break}if(Fe.error){Re=Fe.error;break}if(!Fe.running&&al>3){Re="The backend process terminated immediately on startup.";break}await new Promise(za=>setTimeout(za,500))}if(dl)Oe(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(Re||`Model server failed to respond on port ${At}.`)}catch(Gt){console.error("Failed to load model:",Gt),N({title:"Model Load Failed",message:Gt.message||String(Gt),danger:!0})}finally{setTimeout(()=>Oe(null),800),te(null)}},gt=async()=>{const D=Xe;_(null),await Et(),D&&await Ne(D)},Et=async()=>{if(me)return;Je(!0),$e({progress:10,phase:"Stopping backend process..."});const D=setInterval(async()=>{try{const R=await Cl();R.unloading?.active?$e({progress:R.unloading.progress||50,phase:R.unloading.phase||"Unloading model..."}):$e(F=>({progress:Math.min(95,F.progress+10),phase:F.phase||"Unloading model..."}))}catch{}},300);try{await Js(),$e({progress:100,phase:"Model unloaded"}),b(null),_(null),r(!1)}catch(R){console.error(R),N({title:"Unload Failed",message:R.message||String(R),danger:!0})}finally{clearInterval(D),setTimeout(()=>{Je(!1),$e({progress:0,phase:""})},500)}},We=async D=>{if(await G({title:"Delete Model?",message:`Delete "${D}" from your drive?`,confirmLabel:"Delete",danger:!0}))try{await Wy(D),await B(),f===D&&await Et()}catch(R){console.error("Failed to delete model:",R),N({title:"Delete Failed",message:R.message||String(R),danger:!0})}},Ht=async(D,R)=>{const F=new AbortController;yt(F),Ee(0),Ae({filename:R,speed:"0.0",eta:0,status:"Initiating file copy..."});try{await Jy(D,ae=>{Ee(Math.round(ae.progress)),Ae({filename:ae.filename,speed:ae.speed_mb_s.toFixed(1),eta:Math.round(ae.eta_secs),status:ae.status})},F.signal),await B()}catch(ae){console.error("Import failed:",ae),ae.name!=="AbortError"&&N({title:"Import Failed",message:ae.message||String(ae),danger:!0})}finally{Ee(null),yt(null)}},Tt=()=>{Ke&&Ke.abort()},Z=D=>{const R=D.target.files[0];R&&Ht(R,R.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:Et,disabled:me||L!==null,children:[me?c.jsx(aa,{className:"progress-spinner",size:16}):c.jsx(rn,{size:16}),c.jsx("span",{children:me?"Unloading":"Unload Server"})]})]})}),me&&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:st.phase||"Unloading model..."}),c.jsxs("span",{children:[Math.round(st.progress),"%"]})]}),c.jsx("div",{className:"model- Line truncated
Models and generated outputs are not included.`,confirmLabel:"Clean",danger:!0})){me(!0);try{await Gy(ve.map(D=>D.id)),await ee()}catch(D){N({title:"Cleanup Failed",message:D.message||String(D),danger:!0})}finally{me(!1)}}},[ve,ee,N,x]);O.useEffect(()=>{async function Z(){const ie=await Xy();Xe(ie)}Z()},[]),O.useEffect(()=>{X?localStorage.setItem("active-model",X):localStorage.removeItem("active-model")},[X]),O.useEffect(()=>{let Z=null;async function ie(){const D=await Cl();if(!!D.ready){Z&&clearInterval(Z);const F=localStorage.getItem("active-model");if(F)L(F),W(!0);else{try{if(D&&D.settings&&D.settings.model){const ae=D.settings.model.split(/[\\/]/).pop();L(ae)}}catch{}W(!0)}}else if(!(typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0)){const ae=localStorage.getItem("active-model");ae&&(L(ae),W(!1))}}return ie(),Z=setInterval(ie,2e3),()=>{Z&&clearInterval(Z)}},[]),O.useEffect(()=>{async function Z(){try{const D=await Ly();B(R=>R.cpu_usage===D.cpu_usage&&R.ram_used_gb===D.ram_used_gb&&R.ram_total_gb===D.ram_total_gb&&R.gpu_name===D.gpu_name&&R.vram_used_gb===D.vram_used_gb&&R.vram_total_gb===D.vram_total_gb?R:D)}catch{}}Z();const ie=setInterval(Z,1500);return()=>clearInterval(ie)},[]),O.useEffect(()=>{if(X){const Z=X.toLowerCase();Z.includes("flux")||Z.includes("schnell")?K(ie=>({...ie,steps:4,cfgScale:1,width:1024,height:1024})):Z.includes("lightning")||Z.includes("turbo")?K(ie=>({...ie,steps:4,cfgScale:1.5,width:1024,height:1024})):Z.includes("sd15")?K(ie=>({...ie,steps:25,cfgScale:7,width:512,height:512})):Z.includes("sd35")?K(ie=>({...ie,steps:20,cfgScale:4.5,width:1024,height:1024})):K(ie=>({...ie,steps:20,cfgScale:7,width:512,height:512}))}},[X]);const Ne=O.useMemo(()=>{switch(w){case"generator":return c.jsx(ag,{prompt:A,setPrompt:U,negativePrompt:V,setNegativePrompt:fe,constraints:q,setConstraints:K,activeModel:X,generatedImages:$e,setGeneratedImages:Xe,isGenerating:_,setIsGenerating:C,generationProgress:J,setGenerationProgress:he,setActiveTab:h,showAlert:N,showConfirm:x});case"models":return c.jsx(ug,{activeModel:X,setActiveModel:L,serverRunning:te,setServerRunning:W,constraints:q,showAlert:N,showConfirm:x});case"constraints":return c.jsx(fg,{constraints:q,setConstraints:K,activeModel:X,specs:ce,backendOptions:Ee});default:return null}},[w,A,V,q,X,$e,_,J,te,ce,Ee,N,x]),gt=O.useMemo(()=>c.jsx(Ny,{activeTab:w,setActiveTab:h,specs:ce}),[w,ce]),Et=O.useCallback(async()=>{if(!(!te||Oe)){de(!0);try{await Js(),W(!1),L(null)}catch(Z){console.error("Failed to stop server:",Z),N({title:"Stop Server Failed",message:Z.message||String(Z),danger:!0})}finally{de(!1)}}},[te,Oe,N]),We=[...Ae?.stale?["Restart Local AI Image Generator so the local server loads the latest API."]:[],...Ae?.issues||[]],Ht=ve.reduce((Z,ie)=>Z+Number(ie.sizeBytes||0),0),Tt=!!(Ae&&(We.length>0||ve.length>0));return c.jsxs("div",{className:"app-container",children:[gt,c.jsxs("div",{className:"main-content",children:[c.jsx(Dy,{telemetry:j,serverRunning:te,activeModel:X,onStopServer:Et,isStoppingServer:Oe,theme:G,setTheme:H}),Tt&&c.jsxs("div",{className:`m3-card readiness-card ${Ae?.stale||We.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:Ae?.stale?"Restart Required":We.length>0?"System Readiness":"Safe Cleanup Available"}),c.jsx("p",{className:"m3-card-subtitle",style:{margin:0},children:Ae?.stale?`Running server build: ${Ae.build||"unknown"}`:We.length>0?"Local AI Image Generator found setup items that may need attention.":`${ve.length} temporary item${ve.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:ee,disabled:yt,children:yt?"Checking":"Refresh"}),c.jsx("button",{className:"m3-btn m3-btn-tonal",onClick:se,children:Je?"Copied":"Copy Diagnostics"}),ve.length>0&&c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Ze,disabled:yt,children:["Clean ",ii(Ht)]})]})]}),We.length>0&&c.jsx("div",{className:"readiness-issues",children:We.slice(0,4).map(Z=>c.jsx("span",{children:Z},Z))}),ve.length>0&&c.jsx("div",{className:"readiness-cleanup-list",children:ve.slice(0,3).map(Z=>c.jsxs("span",{title:Z.path,children:[Z.name," · ",Z.size," · ",Z.reason]},Z.id))})]}),Ne]}),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: Line truncated
-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>Local AI Image Generator</title>
<script type="module" crossorigin src="/assets/index-jXnaCwmK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DW6G7wc2.css">
</head>
<body>
<div id="root"></div>␍
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
- [x] Rearrange composer JSX in `TextChat.jsx` to stack the textarea at the top and the action buttons in a separate toolbar at the bottom.
- [x] Refactor composer styles in `App.css` to align with the vertical stack layout, ensuring full-width textarea and proper spacing.
- [x] Remove obsolete `margin-bottom: 2px` properties from action buttons and center them vertically in the toolbar.
- [x] Implement sidebar visibility toggle state in `App.jsx` and persist state to `localStorage`.
- [x] Add sidebar collapse transitions and `.sidebar.collapsed` styles in `App.css`.
- [x] Integrate left sidebar toggle button in `TopStatusBar.jsx` using `PanelLeft`/`PanelLeftClose` Lucide icons.
- [x] Set AI/assistant chat bubble wrap max-width to `80%` in `App.css` to improve text readability and prevent wide stretching.
- [x] Implement global font size state in `App.jsx` and synchronize it to a CSS custom variable `--app-font-size` on the html element.
- [x] Refactor body font-size to use relative `1rem` in `App.css` to enable page-wide scaling.
- [x] Add a Font Size range slider to the "Appearance & Themes" section in `Settings.jsx` to adjust text size from 12px to 24px.
- [x] Fix dropdown overlay menu readability issues in `App.css` by using a solid color-mix base (blending transparent surface with solid background) and forcing strong `backdrop-filter: blur(20px)` to prevent underlying text/icon bleeding.
- [x] Merge "Color Themes" and "Text & Interface Size" subsections in `Settings.jsx` into a single unified card container separated by a horizontal divider line.
- [x] Test production build using `npm run build` to verify there are no compilation or syntax errors.
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>Local AI Image Generator</title>
<title>Uncensored AI Studio</title>
</head>
<body>
+152 -152
View File
@@ -21,21 +21,21 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -44,9 +44,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -55,14 +55,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.2"
"@tybys/wasm-util": "^0.10.3"
},
"funding": {
"type": "github",
@@ -74,9 +74,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
"integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -84,9 +84,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"cpu": [
"arm64"
],
@@ -101,9 +101,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"cpu": [
"arm64"
],
@@ -118,9 +118,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"cpu": [
"x64"
],
@@ -135,9 +135,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"cpu": [
"x64"
],
@@ -152,9 +152,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"cpu": [
"arm"
],
@@ -169,9 +169,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"cpu": [
"arm64"
],
@@ -186,9 +186,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"cpu": [
"arm64"
],
@@ -203,9 +203,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"cpu": [
"ppc64"
],
@@ -220,9 +220,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"cpu": [
"s390x"
],
@@ -237,9 +237,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"cpu": [
"x64"
],
@@ -254,9 +254,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"cpu": [
"x64"
],
@@ -271,9 +271,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"cpu": [
"arm64"
],
@@ -288,9 +288,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
"cpu": [
"wasm32"
],
@@ -298,18 +298,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.6"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"cpu": [
"arm64"
],
@@ -324,9 +324,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"cpu": [
"x64"
],
@@ -348,9 +348,9 @@
"license": "MIT"
},
"node_modules/@tauri-apps/api": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
"integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -358,9 +358,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.3.tgz",
"integrity": "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -374,23 +374,23 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.11.2",
"@tauri-apps/cli-darwin-x64": "2.11.2",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
"@tauri-apps/cli-darwin-arm64": "2.11.3",
"@tauri-apps/cli-darwin-x64": "2.11.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.3",
"@tauri-apps/cli-linux-arm64-musl": "2.11.3",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.3",
"@tauri-apps/cli-linux-x64-gnu": "2.11.3",
"@tauri-apps/cli-linux-x64-musl": "2.11.3",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.3",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.3",
"@tauri-apps/cli-win32-x64-msvc": "2.11.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.3.tgz",
"integrity": "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==",
"cpu": [
"arm64"
],
@@ -405,9 +405,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.3.tgz",
"integrity": "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==",
"cpu": [
"x64"
],
@@ -422,9 +422,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.3.tgz",
"integrity": "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==",
"cpu": [
"arm"
],
@@ -439,9 +439,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.3.tgz",
"integrity": "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==",
"cpu": [
"arm64"
],
@@ -456,9 +456,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.3.tgz",
"integrity": "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==",
"cpu": [
"arm64"
],
@@ -473,9 +473,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.3.tgz",
"integrity": "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==",
"cpu": [
"riscv64"
],
@@ -490,9 +490,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.3.tgz",
"integrity": "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==",
"cpu": [
"x64"
],
@@ -507,9 +507,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.3.tgz",
"integrity": "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==",
"cpu": [
"x64"
],
@@ -524,9 +524,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.3.tgz",
"integrity": "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==",
"cpu": [
"arm64"
],
@@ -541,9 +541,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.3.tgz",
"integrity": "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==",
"cpu": [
"ia32"
],
@@ -558,9 +558,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.3.tgz",
"integrity": "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==",
"cpu": [
"x64"
],
@@ -584,9 +584,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -595,13 +595,13 @@
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
"integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
"integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "^1.0.0"
"@rolldown/pluginutils": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -925,18 +925,18 @@
}
},
"node_modules/lucide-react": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
"integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz",
"integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"dev": true,
"funding": [
{
@@ -1023,13 +1023,13 @@
}
},
"node_modules/rolldown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.133.0",
"@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -1039,21 +1039,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
"@rolldown/binding-android-arm64": "1.1.3",
"@rolldown/binding-darwin-arm64": "1.1.3",
"@rolldown/binding-darwin-x64": "1.1.3",
"@rolldown/binding-freebsd-x64": "1.1.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
"@rolldown/binding-linux-arm64-musl": "1.1.3",
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
"@rolldown/binding-linux-x64-gnu": "1.1.3",
"@rolldown/binding-linux-x64-musl": "1.1.3",
"@rolldown/binding-openharmony-arm64": "1.1.3",
"@rolldown/binding-wasm32-wasi": "1.1.3",
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/scheduler": {
@@ -1098,16 +1098,16 @@
"optional": true
},
"node_modules/vite": {
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
"integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -1124,7 +1124,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.18",
"@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
+2627 -30
View File
File diff suppressed because it is too large. Load diff
+467 -132
View File
@@ -3,8 +3,11 @@ import Sidebar from "./components/Sidebar";
import TopStatusBar from "./components/TopStatusBar";
import Generator from "./components/Generator";
import ModelManager from "./components/ModelManager";
import ImageConstraints from "./components/ImageConstraints";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, stopServer } from "./services/api";
import Settings from "./components/Settings";
import TextChat from "./components/TextChat";
import SpeechTranscriber from "./components/SpeechTranscriber";
import TextToSpeech from "./components/TextToSpeech";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api";
import "./App.css";
function App() {
@@ -55,6 +58,7 @@ function App() {
const [activeModel, setActiveModel] = useState(null);
const [serverRunning, setServerRunning] = useState(false);
const [isStoppingServer, setIsStoppingServer] = useState(false);
const [isLlmLoaded, setIsLlmLoaded] = useState(false);
const [backendOptions, setBackendOptions] = useState({
options: [{ id: "cpu", label: "CPU", available: true }],
cudaAvailable: false,
@@ -97,6 +101,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,
@@ -111,6 +117,296 @@ function App() {
backendType: "auto",
});
const [textSettings, setTextSettings] = useState(() => {
const saved = localStorage.getItem("textSettings");
if (saved) {
try {
const parsed = JSON.parse(saved);
return {
...parsed,
enableThinking: parsed.enableThinking === true,
maxTokens: !parsed.maxTokens || parsed.maxTokens === 384 ? 1024 : parsed.maxTokens,
responseTokenMode: parsed.responseTokenMode || "auto",
};
} catch (_) {}
}
return {
contextSize: 0,
temperature: 0.7,
systemPrompt: "You are a helpful local AI assistant.",
threads: Math.max(4, Math.min(16, (navigator.hardwareConcurrency || 4) - 2)),
enableThinking: false,
// New performance settings
gpuLayers: -1,
maxTokens: 1024,
responseTokenMode: "auto",
topP: 0.95,
topK: 40,
minP: 0.05,
repeatPenalty: 1.1,
seed: null,
performanceProfile: "balanced",
flashAttn: true,
cacheTypeK: "q8_0",
cacheTypeV: "q8_0",
mlock: false,
mmap: true,
cachePrompt: true,
defragThold: 0.1,
batchSize: 512,
ubatchSize: 512,
preferredBackend: "auto",
};
});
useEffect(() => {
localStorage.setItem("textSettings", JSON.stringify(textSettings));
}, [textSettings]);
const [speechSettings, setSpeechSettings] = useState(() => {
const saved = localStorage.getItem("speechSettings");
if (saved) {
try {
const parsed = JSON.parse(saved);
return {
language: parsed.language || "auto",
threads: Math.max(1, Math.min(32, Number(parsed.threads) || Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)))),
backendPreference: ["auto", "vulkan", "metal", "cpu"].includes(parsed.backendPreference) ? parsed.backendPreference : "auto",
translate: parsed.translate === true,
};
} catch (_) {}
}
return {
language: "auto",
threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
backendPreference: "auto",
translate: false,
};
});
useEffect(() => {
localStorage.setItem("speechSettings", JSON.stringify(speechSettings));
}, [speechSettings]);
const [ttsSettings, setTtsSettings] = useState(() => {
const saved = localStorage.getItem("ttsSettings");
if (saved) {
try {
const parsed = JSON.parse(saved);
return {
model: parsed.model || "",
voice: parsed.voice || "af_heart",
speed: Math.max(0.5, Math.min(2, Number(parsed.speed) || 1)),
};
} catch (_) {}
}
return {
model: "",
voice: "af_heart",
speed: 1,
};
});
useEffect(() => {
localStorage.setItem("ttsSettings", JSON.stringify(ttsSettings));
}, [ttsSettings]);
// Lifted Chat History States
const [conversations, setConversations] = useState([]);
const [activeConversationId, setActiveConversationId] = useState(null);
const [showHistory, setShowHistory] = useState(false); // Default hide
const [sidebarVisible, setSidebarVisible] = useState(() => {
const saved = localStorage.getItem("sidebarVisible");
return saved !== "false";
});
useEffect(() => {
localStorage.setItem("sidebarVisible", String(sidebarVisible));
}, [sidebarVisible]);
const [fontSize, setFontSize] = useState(() => {
const saved = localStorage.getItem("fontSize");
return saved ? Number(saved) : 16;
});
useEffect(() => {
document.documentElement.style.setProperty("--app-font-size", `${fontSize}px`);
localStorage.setItem("fontSize", String(fontSize));
}, [fontSize]);
const [speechTranscriptions, setSpeechTranscriptions] = useState([]);
const [selectedSpeechTranscript, setSelectedSpeechTranscript] = useState(null);
const [showSpeechHistory, setShowSpeechHistory] = useState(false);
const [ttsOutputs, setTtsOutputs] = useState([]);
const [selectedTtsOutput, setSelectedTtsOutput] = useState(null);
const [showTtsHistory, setShowTtsHistory] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("chat_conversations");
if (saved) {
try {
setConversations(JSON.parse(saved));
} catch (_) {
localStorage.removeItem("chat_conversations");
}
}
}, []);
const sanitizeMessageForStorage = (message) => {
if (!Array.isArray(message?.content)) return message;
return {
...message,
content: message.content.map((item) => {
if (item?.type !== "image_url") return item;
return {
type: "text",
text: "[Attached image omitted from saved chat history]",
};
}),
};
};
const sanitizeConversationForStorage = (conversation) => ({
...conversation,
messages: Array.isArray(conversation.messages)
? conversation.messages.map(sanitizeMessageForStorage)
: [],
});
const persistConversations = (list) => {
const compactList = list.map(sanitizeConversationForStorage);
try {
localStorage.setItem("chat_conversations", JSON.stringify(compactList));
} catch (err) {
console.warn("Could not save chat history:", err);
}
};
const saveConversationState = useCallback((id, msgs, modelName, newTitle = null) => {
setConversations((prev) => {
const list = [...prev];
const idx = list.findIndex(c => c.id === id);
if (idx !== -1) {
list[idx] = {
...list[idx],
messages: msgs,
timestamp: Date.now(),
model: modelName,
...(newTitle ? { title: newTitle } : {})
};
} else {
list.unshift({
id,
title: newTitle || "Chat Session",
model: modelName,
messages: msgs,
timestamp: Date.now()
});
}
persistConversations(list);
return list;
});
}, []);
const handleDeleteConversation = useCallback((id, e) => {
if (e) e.stopPropagation();
setConversations((prev) => {
const filtered = prev.filter((c) => c.id !== id);
persistConversations(filtered);
return filtered;
});
if (activeConversationId === id) {
setActiveConversationId(null);
}
}, [activeConversationId]);
const refreshSpeechTranscriptions = useCallback(async () => {
try {
const list = await listSpeechTranscriptions();
setSpeechTranscriptions(list);
} catch (err) {
console.warn("Could not load speech transcriptions:", err);
setSpeechTranscriptions([]);
}
}, []);
const handleDeleteSpeechTranscription = useCallback(async (item, e) => {
if (e) e.stopPropagation();
const itemId = item.filename || item.metadata || item.textFile;
if (!itemId) return;
const ok = await showConfirm({
title: "Delete Transcription",
message: `Are you sure you want to delete "${item.displayName || item.sourceFilename || "this transcription"}"? This action cannot be undone.`,
confirmLabel: "Delete",
danger: true
});
if (!ok) return;
try {
await deleteSpeechTranscription(itemId);
setSelectedSpeechTranscript((prev) => {
if (prev) {
const prevId = prev.filename || prev.metadata || prev.textFile;
if (prevId === itemId) return null;
}
return prev;
});
refreshSpeechTranscriptions();
} catch (err) {
console.error("Failed to delete speech transcription:", err);
showAlert({ title: "Delete Failed", message: err.message || String(err), danger: true });
}
}, [showConfirm, showAlert, setSelectedSpeechTranscript, refreshSpeechTranscriptions]);
useEffect(() => {
refreshSpeechTranscriptions();
}, [refreshSpeechTranscriptions]);
const refreshTtsOutputs = useCallback(async () => {
try {
const list = await listTtsOutputs();
setTtsOutputs(list);
} catch (err) {
console.warn("Could not load TTS outputs:", err);
setTtsOutputs([]);
}
}, []);
const handleDeleteTtsOutput = useCallback(async (item, e) => {
if (e) e.stopPropagation();
const itemId = item.filename || item.metadata;
if (!itemId) return;
const ok = await showConfirm({
title: "Delete TTS Output",
message: `Are you sure you want to delete "${item.displayName || item.audioFile || "this audio"}"? This action cannot be undone.`,
confirmLabel: "Delete",
danger: true
});
if (!ok) return;
try {
await deleteTtsOutput(itemId);
setSelectedTtsOutput((prev) => {
if (prev) {
const prevId = prev.filename || prev.metadata;
if (prevId === itemId) return null;
}
return prev;
});
refreshTtsOutputs();
} catch (err) {
console.error("Failed to delete TTS output:", err);
showAlert({ title: "Delete Failed", message: err.message || String(err), danger: true });
}
}, [showConfirm, showAlert, refreshTtsOutputs]);
useEffect(() => {
refreshTtsOutputs();
}, [refreshTtsOutputs]);
// Load hardware specifications on mount
useEffect(() => {
async function loadSpecs() {
@@ -126,6 +422,10 @@ function App() {
backendType: prev.backendType === "auto" ? backendInfo.defaultBackendType || "cpu" : prev.backendType,
useGpu: (prev.backendType === "auto" ? backendInfo.defaultBackendType : prev.backendType) !== "cpu",
}));
setTextSettings((prev) => ({
...prev,
threads: prev.threads || hardware.cpu_cores_physical || 4
}));
} catch (err) {
console.error("Error fetching hardware specs:", err);
}
@@ -277,10 +577,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 +601,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 4,
standardSteps: 4,
cfgScale: 1.5,
width: 1024,
height: 1024,
@@ -297,6 +610,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 25,
standardSteps: 25,
cfgScale: 7.0,
width: 512,
height: 512,
@@ -305,6 +619,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 20,
standardSteps: 20,
cfgScale: 4.5,
width: 1024,
height: 1024,
@@ -314,6 +629,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 20,
standardSteps: 20,
cfgScale: 7.0,
width: 512,
height: 512,
@@ -322,10 +638,73 @@ function App() {
}
}, [activeModel]);
const activeTabContent = useMemo(() => {
switch (activeTab) {
case "generator":
return (
// Tab contents are kept mounted to preserve loading/generation state when switching tabs
const sidebarContent = useMemo(() => (
<Sidebar
collapsed={!sidebarVisible}
activeTab={activeTab}
setActiveTab={setActiveTab}
specs={specs}
conversations={conversations}
activeConversationId={activeConversationId}
setActiveConversationId={setActiveConversationId}
showHistory={showHistory}
setShowHistory={setShowHistory}
onDeleteConversation={handleDeleteConversation}
speechTranscriptions={speechTranscriptions}
selectedSpeechTranscript={selectedSpeechTranscript}
setSelectedSpeechTranscript={setSelectedSpeechTranscript}
showSpeechHistory={showSpeechHistory}
setShowSpeechHistory={setShowSpeechHistory}
onDeleteSpeechTranscription={handleDeleteSpeechTranscription}
ttsOutputs={ttsOutputs}
selectedTtsOutput={selectedTtsOutput}
setSelectedTtsOutput={setSelectedTtsOutput}
showTtsHistory={showTtsHistory}
setShowTtsHistory={setShowTtsHistory}
onDeleteTtsOutput={handleDeleteTtsOutput}
/>
), [sidebarVisible, activeTab, specs, conversations, activeConversationId, showHistory, handleDeleteConversation, speechTranscriptions, selectedSpeechTranscript, showSpeechHistory, handleDeleteSpeechTranscription, ttsOutputs, selectedTtsOutput, showTtsHistory, handleDeleteTtsOutput]);
const handleStopServer = useCallback(async () => {
if (!serverRunning || isStoppingServer) return;
setIsStoppingServer(true);
try {
await stopServer();
setServerRunning(false);
setActiveModel(null);
} catch (err) {
console.error("Failed to stop server:", err);
showAlert({ title: "Stop Server Failed", message: err.message || String(err), danger: true });
} finally {
setIsStoppingServer(false);
}
}, [serverRunning, isStoppingServer, showAlert]);
return (
<div className="app-container">
{/* Sidebar with Navigation Rail & Specs */}
{sidebarContent}
{/* Main Panel */}
<div className="main-content">
{/* Top telemetry specs chips bar */}
<TopStatusBar
telemetry={telemetry}
serverRunning={serverRunning}
activeModel={activeModel}
isLlmLoaded={isLlmLoaded}
onStopServer={handleStopServer}
isStoppingServer={isStoppingServer}
theme={theme}
setTheme={setTheme}
sidebarVisible={sidebarVisible}
onToggleSidebar={() => setSidebarVisible(!sidebarVisible)}
/>
{/* Dynamic Workspace Container */}
<div style={{ display: activeTab === "generator" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<Generator
prompt={prompt}
setPrompt={setPrompt}
@@ -343,147 +722,103 @@ function App() {
setActiveTab={setActiveTab}
showAlert={showAlert}
showConfirm={showConfirm}
specs={specs}
textSettings={textSettings}
/>
);
case "models":
return (
</div>
<div style={{ display: activeTab === "models" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<ModelManager
activeModel={activeModel}
setActiveModel={setActiveModel}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
constraints={constraints}
setConstraints={setConstraints}
telemetry={telemetry}
backendOptions={backendOptions}
showAlert={showAlert}
showConfirm={showConfirm}
activeTab={activeTab}
specs={specs}
textSettings={textSettings}
/>
);
case "constraints":
return (
<ImageConstraints
</div>
<div style={{ display: activeTab === "chat" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<TextChat
specs={specs}
showAlert={showAlert}
showConfirm={showConfirm}
textSettings={textSettings}
setTextSettings={setTextSettings}
setActiveModel={setActiveModel}
setServerRunning={setServerRunning}
conversations={conversations}
setConversations={setConversations}
activeConversationId={activeConversationId}
setActiveConversationId={setActiveConversationId}
showHistory={showHistory}
setShowHistory={setShowHistory}
saveConversationState={saveConversationState}
setIsLlmLoaded={setIsLlmLoaded}
/>
</div>
<div style={{ display: activeTab === "speech" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<SpeechTranscriber
showAlert={showAlert}
showConfirm={showConfirm}
selectedTranscript={selectedSpeechTranscript}
onTranscriptionsChanged={refreshSpeechTranscriptions}
speechSettings={speechSettings}
setSpeechSettings={setSpeechSettings}
/>
</div>
<div style={{ display: activeTab === "tts" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<TextToSpeech
showAlert={showAlert}
showConfirm={showConfirm}
selectedOutput={selectedTtsOutput}
onOutputsChanged={refreshTtsOutputs}
ttsSettings={ttsSettings}
setTtsSettings={setTtsSettings}
/>
</div>
<div style={{ display: activeTab === "settings" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<Settings
constraints={constraints}
setConstraints={setConstraints}
activeModel={activeModel}
specs={specs}
backendOptions={backendOptions}
setBackendOptions={setBackendOptions}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
setActiveModel={setActiveModel}
showAlert={showAlert}
showConfirm={showConfirm}
textSettings={textSettings}
setTextSettings={setTextSettings}
speechSettings={speechSettings}
setSpeechSettings={setSpeechSettings}
ttsSettings={ttsSettings}
setTtsSettings={setTtsSettings}
health={health}
cleanupItems={cleanupItems}
isReadinessBusy={isReadinessBusy}
refreshReadiness={refreshReadiness}
copyDiagnostics={copyDiagnostics}
cleanupSafeItems={cleanupSafeItems}
diagnosticsCopied={diagnosticsCopied}
theme={theme}
setTheme={setTheme}
fontSize={fontSize}
setFontSize={setFontSize}
/>
);
default:
return null;
}
}, [
activeTab,
prompt,
negativePrompt,
constraints,
activeModel,
generatedImages,
isGenerating,
generationProgress,
serverRunning,
specs,
backendOptions,
showAlert,
showConfirm,
]);
const sidebarContent = useMemo(() => (
<Sidebar
activeTab={activeTab}
setActiveTab={setActiveTab}
specs={specs}
/>
), [activeTab, specs]);
const handleStopServer = useCallback(async () => {
if (!serverRunning || isStoppingServer) return;
setIsStoppingServer(true);
try {
await stopServer();
setServerRunning(false);
setActiveModel(null);
} catch (err) {
console.error("Failed to stop server:", err);
showAlert({ title: "Stop Server Failed", message: err.message || String(err), danger: true });
} finally {
setIsStoppingServer(false);
}
}, [serverRunning, isStoppingServer, showAlert]);
const readinessIssues = [
...(health?.stale ? ["Restart Local AI Image Generator so the local server loads the latest API."] : []),
...(health?.issues || []),
];
const cleanupBytes = cleanupItems.reduce((sum, item) => sum + Number(item.sizeBytes || 0), 0);
const showReadinessPanel = Boolean(health && (readinessIssues.length > 0 || cleanupItems.length > 0));
return (
<div className="app-container">
{/* Sidebar with Navigation Rail & Specs */}
{sidebarContent}
{/* Main Panel */}
<div className="main-content">
{/* Top telemetry specs chips bar */}
<TopStatusBar
telemetry={telemetry}
serverRunning={serverRunning}
activeModel={activeModel}
onStopServer={handleStopServer}
isStoppingServer={isStoppingServer}
theme={theme}
setTheme={setTheme}
/>
{showReadinessPanel && (
<div className={`m3-card readiness-card ${health?.stale || readinessIssues.length > 0 ? "readiness-card-warning" : ""}`}>
<div className="readiness-header">
<div>
<h3 className="m3-card-title" style={{ marginBottom: "4px" }}>
{health?.stale ? "Restart Required" : readinessIssues.length > 0 ? "System Readiness" : "Safe Cleanup Available"}
</h3>
<p className="m3-card-subtitle" style={{ margin: 0 }}>
{health?.stale
? `Running server build: ${health.build || "unknown"}`
: readinessIssues.length > 0
? "Local AI Image Generator found setup items that may need attention."
: `${cleanupItems.length} temporary item${cleanupItems.length === 1 ? "" : "s"} can be cleaned (${formatBytes(cleanupBytes)}).`}
</p>
</div>
<div className="readiness-actions">
<button className="m3-btn m3-btn-outlined" onClick={refreshReadiness} disabled={isReadinessBusy}>
{isReadinessBusy ? "Checking" : "Refresh"}
</button>
<button className="m3-btn m3-btn-tonal" onClick={copyDiagnostics}>
{diagnosticsCopied ? "Copied" : "Copy Diagnostics"}
</button>
{cleanupItems.length > 0 && (
<button className="m3-btn m3-btn-error" onClick={cleanupSafeItems} disabled={isReadinessBusy}>
Clean {formatBytes(cleanupBytes)}
</button>
)}
</div>
</div>
{readinessIssues.length > 0 && (
<div className="readiness-issues">
{readinessIssues.slice(0, 4).map((issue) => (
<span key={issue}>{issue}</span>
))}
</div>
)}
{cleanupItems.length > 0 && (
<div className="readiness-cleanup-list">
{cleanupItems.slice(0, 3).map((item) => (
<span key={item.id} title={item.path}>
{item.name} · {item.size} · {item.reason}
</span>
))}
</div>
)}
</div>
)}
{/* Dynamic Workspace Container */}
{activeTabContent}
</div>
</div>
{dialog && (
+169 -81
View File
@@ -1,6 +1,19 @@
import React, { memo, useState, useRef, useCallback } from "react";
import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2, ImagePlus } from "lucide-react";
import { generateImage, startServer, stopServer, waitForServerReady, getBackendStatus, getGenerationProgress, saveGeneratedOutput, deleteGeneratedOutputs } from "../services/api";
import {
generateImage,
startServer,
stopServer,
waitForServerReady,
getBackendStatus,
getGenerationProgress,
saveGeneratedOutput,
deleteGeneratedOutputs,
getLlmStatus,
listLlmModels,
startLlm,
chatWithLlm
} from "../services/api";
const GalleryItem = memo(({ img, idx, isSelected, onClick }) => {
const handleClick = (e) => {
@@ -39,6 +52,8 @@ function Generator({
setActiveTab,
showAlert = async ({ message }) => window.alert(message),
showConfirm = async ({ message }) => window.confirm(message),
specs,
textSettings,
}) {
const [outputImage, setOutputImage] = useState(null);
const [outputSeed, setOutputSeed] = useState(null);
@@ -56,10 +71,88 @@ function Generator({
const [isDecoding, setIsDecoding] = useState(false);
const [selectedGalleryIndexes, setSelectedGalleryIndexes] = useState([]);
const [baseImage, setBaseImage] = useState(null);
const [isEnhancing, setIsEnhancing] = useState(false);
const timerRef = useRef(null);
const abortControllerRef = useRef(null);
const hasRealGenerationStepRef = useRef(false);
const handleEnhancePrompt = async () => {
if (!prompt.trim() || isEnhancing || isGenerating) return;
setIsEnhancing(true);
try {
const status = await getLlmStatus();
let targetModel = status.settings?.model;
if (!status.ready) {
const models = await listLlmModels();
if (models.length === 0) {
showAlert({
title: "No Text Models",
message: "You haven't downloaded any text models yet. Please go to the 'Model Manager' or 'Text Chat' tab to download a GGUF model first.",
danger: true
});
setIsEnhancing(false);
return;
}
const savedLlm = localStorage.getItem("selectedLlmModel") || models[0]?.filename;
targetModel = models.some(m => m.filename === savedLlm) ? savedLlm : models[0]?.filename;
const confirm = await showConfirm({
title: "Load Text Model?",
message: `Enhancing your prompt requires loading the local text model "${targetModel}". This will temporarily unload the image model from memory. Do you want to proceed?`,
confirmLabel: "Load & Enhance",
cancelLabel: "Cancel"
});
if (!confirm) {
setIsEnhancing(false);
return;
}
const cores = textSettings?.threads || specs?.cpu_cores_physical || 4;
const context = textSettings?.contextSize || 2048;
await startLlm(targetModel, {
threads: cores,
contextSize: context,
gpuLayers: -1,
preferredBackend: textSettings?.preferredBackend
});
}
const systemPrompt = "You are a helpful assistant. Rewrite the user's image prompt to be more descriptive and detailed for image generation. Keep it under 80 words. Output ONLY the rewritten prompt, no explanation or introductory text.";
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: `Please expand this prompt: "${prompt}"` }
];
const temp = textSettings?.temperature || 0.7;
const response = await chatWithLlm(messages, { temperature: temp, maxTokens: 128 });
let cleanResponse = response.content.trim();
if (cleanResponse.startsWith('"') && cleanResponse.endsWith('"')) {
cleanResponse = cleanResponse.slice(1, -1);
}
cleanResponse = cleanResponse.replace(/^Here is the expanded prompt:?\s*/i, "");
cleanResponse = cleanResponse.replace(/^Enhanced prompt:?\s*/i, "");
setPrompt(cleanResponse);
} catch (err) {
showAlert({
title: "Prompt Enhancement Failed",
message: err.message || String(err),
danger: true
});
} finally {
setIsEnhancing(false);
}
};
React.useEffect(() => {
if (constraints.backendType === "apple-npu" || constraints.backendType === "openvino-npu") {
setBaseImage(null);
}
}, [constraints.backendType]);
const handleBaseImageSelect = (e) => {
const file = e.target.files?.[0];
e.target.value = "";
@@ -91,14 +184,20 @@ function Generator({
const settings = status.settings;
const currentModelName = settings.model ? settings.model.split(/[\\/]/).pop() : null;
const targetModelName = activeModel ? activeModel.split(/[\\/]/).pop() : null;
const requestedBackend = constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto");
const currentBackend = settings.backendType || (settings.useGpu === false ? "cpu" : "auto");
const shouldCompareModel = requestedBackend !== "openvino-npu";
const isCoreMLBackend = requestedBackend === "apple-npu";
if (currentModelName !== targetModelName ||
parseInt(settings.steps) !== parseInt(constraints.steps) ||
Math.abs(parseFloat(settings.cfgScale) - parseFloat(constraints.cfgScale)) > 0.05 ||
settings.sampler !== constraints.sampler ||
parseInt(settings.threads) !== parseInt(constraints.threads) ||
if ((shouldCompareModel && currentModelName !== targetModelName) ||
(!isCoreMLBackend && parseInt(settings.steps) !== parseInt(constraints.steps)) ||
(!isCoreMLBackend && Math.abs(parseFloat(settings.cfgScale) - parseFloat(constraints.cfgScale)) > 0.05) ||
(!isCoreMLBackend && settings.sampler !== constraints.sampler) ||
(!isCoreMLBackend && parseInt(settings.threads) !== parseInt(constraints.threads)) ||
Boolean(settings.useGpu) !== (constraints.useGpu !== false) ||
(settings.backendType || (settings.useGpu === false ? "cpu" : "auto")) !== (constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"))) {
parseInt(settings.width || 512) !== parseInt(constraints.width || 512) ||
parseInt(settings.height || 512) !== parseInt(constraints.height || 512) ||
currentBackend !== requestedBackend) {
needsRestart = true;
}
} else {
@@ -112,10 +211,10 @@ function Generator({
try {
console.log("Settings out of sync, restarting backend...");
await startServer(activeModel, constraints);
let isReady = false;
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" || constraints.backendType === "apple-npu") ? 1200 : 240;
for (let i = 0; i < maxStartupPolls; i++) {
const status = await getBackendStatus();
if (status.loading) {
setRestartLoadProgress({
@@ -136,7 +235,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;
}
@@ -584,7 +683,7 @@ function Generator({
<div className="m3-field-group">
{/* Prompt Textarea */}
<div className="m3-text-field">
<label className="m3-text-field-label">Positive Prompt</label>
<label className="m3-text-field-label">Image Prompt</label>
<textarea
className="m3-textarea"
value={prompt}
@@ -594,92 +693,81 @@ function Generator({
/>
</div>
{/* Negative Prompt Text Input */}
<div className="m3-text-field">
<label className="m3-text-field-label">Negative Prompt (Optional)</label>
<input
type="text"
className="m3-input"
value={negativePrompt}
onChange={(e) => setNegativePrompt(e.target.value)}
placeholder="Items to avoid (e.g. 'blurry, low quality, deformed hands, texts')..."
disabled={isGenerating}
/>
</div>
{constraints.backendType !== "apple-npu" && constraints.backendType !== "openvino-npu" && (
<div className="m3-text-field">
<label className="m3-text-field-label">Base Image (Optional)</label>
{baseImage ? (
<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)" }}>
<img src={baseImage} alt="Base" style={{ width: "64px", height: "64px", objectFit: "cover", borderRadius: "8px", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: "0.85rem" }}>Base image ready</div>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
The AI will redraw this image guided by your prompt.
</div>
</div>
<button type="button" className="m3-btn m3-btn-error" style={{ height: "34px", flexShrink: 0 }} onClick={handleClearBaseImage} disabled={isGenerating}>
<Trash2 size={14} />
<span>Remove</span>
</button>
</div>
) : (
<label className="import-box" style={{ margin: 0, padding: "16px", cursor: isGenerating ? "not-allowed" : "pointer" }}>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
style={{ display: "none" }}
onChange={handleBaseImageSelect}
disabled={isGenerating}
/>
<ImagePlus className="import-icon" />
<span style={{ fontWeight: 600 }}>Upload a base image</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", textAlign: "center" }}>
Optional. Generate a new image based on one of your own.
</span>
</label>
)}
<div className="m3-text-field">
<label className="m3-text-field-label">Base Image (Optional)</label>
{baseImage ? (
<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)" }}>
<img src={baseImage} alt="Base" style={{ width: "64px", height: "64px", objectFit: "cover", borderRadius: "8px", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: "0.85rem" }}>Base image ready</div>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
The AI will redraw this image guided by your prompt.
{baseImage && (
<div style={{ marginTop: "12px" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.8rem", marginBottom: "4px" }}>
<span>Transformation strength</span>
<span>{constraints.denoisingStrength.toFixed(2)}</span>
</div>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={constraints.denoisingStrength}
onChange={(e) => setConstraints((prev) => ({ ...prev, denoisingStrength: parseFloat(e.target.value) }))}
disabled={isGenerating}
style={{ width: "100%" }}
/>
<div style={{ fontSize: "0.72rem", color: "var(--md-sys-color-outline)", marginTop: "2px" }}>
Lower = closer to your image; higher = more creative freedom. 0.7 is a good start.
</div>
</div>
<button type="button" className="m3-btn m3-btn-error" style={{ height: "34px", flexShrink: 0 }} onClick={handleClearBaseImage} disabled={isGenerating}>
<Trash2 size={14} />
<span>Remove</span>
</button>
</div>
) : (
<label className="import-box" style={{ margin: 0, padding: "16px", cursor: isGenerating ? "not-allowed" : "pointer" }}>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
style={{ display: "none" }}
onChange={handleBaseImageSelect}
disabled={isGenerating}
/>
<ImagePlus className="import-icon" />
<span style={{ fontWeight: 600 }}>Upload a base image</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", textAlign: "center" }}>
Optional. Generate a new image based on one of your own.
</span>
</label>
)}
{baseImage && (
<div style={{ marginTop: "12px" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.8rem", marginBottom: "4px" }}>
<span>Transformation strength</span>
<span>{constraints.denoisingStrength.toFixed(2)}</span>
</div>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={constraints.denoisingStrength}
onChange={(e) => setConstraints((prev) => ({ ...prev, denoisingStrength: parseFloat(e.target.value) }))}
disabled={isGenerating}
style={{ width: "100%" }}
/>
<div style={{ fontSize: "0.72rem", color: "var(--md-sys-color-outline)", marginTop: "2px" }}>
Lower = closer to your image; higher = more creative freedom. 0.7 is a good start.
</div>
</div>
)}
</div>
)}
</div>
)}
{/* Configuration Status Chips (Material 3 style) */}
<div className="m3-text-field">
<label className="m3-text-field-label">Active Image Constraints</label>
<div className="chips-container">
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
<div className="status-chip">
<Sliders size={14} />
<span>Resolution: {constraints.width}x{constraints.height}</span>
</div>
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
<div className="status-chip">
<RefreshCw size={14} />
<span>Steps: {constraints.steps}</span>
</div>
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
<div className="status-chip">
<Sparkles size={14} />
<span>Sampler: {constraints.sampler}</span>
</div>
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
<div className="status-chip">
<span>Seed: {constraints.seed === -1 ? "Random" : constraints.seed}</span>
</div>
</div>
@@ -1,344 +0,0 @@
import React, { memo } from "react";
import { Crop, Sliders, Cpu, Info } from "lucide-react";
const ASPECT_RATIOS = [
{ 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" }
];
const isSD15OrCustomModel = (modelName) => {
if (!modelName) return true;
const name = modelName.toLowerCase();
if (name.includes("flux") || name.includes("schnell")) return false;
if (name.includes("sdxl") || name.includes("lightning") || name.includes("turbo")) return false;
if (name.includes("sd3")) return false;
return true;
};
function ImageConstraints({ constraints, setConstraints, activeModel, specs, backendOptions }) {
const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false;
const availableBackends = backendOptions?.options?.length
? backendOptions.options
: [{ id: "cpu", label: "CPU", available: true }];
const updateConstraint = (key, value) => {
setConstraints((prev) => ({
...prev,
[key]: value
}));
};
const handleAspectRatioChange = (ratio, sizeType) => {
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 (w > h) {
h = Math.round((h * 512) / w);
w = 512;
} else {
w = Math.round((w * 512) / h);
h = 512;
}
w = Math.round(w / 64) * 64;
h = Math.round(h / 64) * 64;
}
updateConstraint("width", w);
updateConstraint("height", h);
}
};
const handleBackendChange = (backendType) => {
setConstraints((prev) => ({
...prev,
backendType,
useGpu: backendType !== "cpu",
}));
};
return (
<div className="workspace-area">
<div className="workspace-title-section">
<h2 className="workspace-title">Image Constraints</h2>
<p className="workspace-subtitle">
Configure size, quality, and performance controls for image generation.
</p>
</div>
<div className="generator-layout">
{/* Left Column: Size & Quality */}
<div>
{/* Card 1: Picture Size & Shape */}
<div className="m3-card">
<h3 className="m3-card-title">
<Crop size={18} style={{ color: "var(--md-sys-color-primary)" }} />
1. Picture Size & Shape
</h3>
{isSD15OrCustom && (
<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"
}}>
<Info size={16} style={{ color: "rgb(251, 191, 36)", flexShrink: 0, marginTop: "2px" }} />
<div>
<strong>Low-VRAM Protection Active:</strong> Capped at 512x512 for the loaded SD 1.x model (<code>{activeModel}</code>). Generating at larger sizes (like 1024x1024) is disabled to prevent out-of-memory crashes on your {specs && specs.gpu_name && !specs.gpu_name.includes("Loading") ? specs.gpu_name : "graphics processor"}.
</div>
</div>
)}
<div className="m3-field-group">
{/* Size switcher */}
<div className="m3-text-field">
<label className="m3-text-field-label">AI Generation Engine Optimization</label>
<div className="m3-segmented-button">
{isSD15OrCustom ? (
<div
className="m3-segment-item disabled"
style={{
opacity: 0.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."
>
High Quality Mode (1024px)
</div>
) : (
<div
className={`m3-segment-item ${constraints.width >= 1024 ? "active" : ""}`}
onClick={() => {
updateConstraint("width", 1024);
updateConstraint("height", 1024);
}}
>
High Quality Mode (1024px)
</div>
)}
<div
className={`m3-segment-item ${constraints.width < 1024 || isSD15OrCustom ? "active" : ""}`}
onClick={() => {
updateConstraint("width", 512);
updateConstraint("height", 512);
}}
>
Standard Speed Mode (512px)
</div>
</div>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px" }}>
{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."}
</span>
</div>
{/* Aspect Ratio Buttons */}
<div className="m3-text-field">
<label className="m3-text-field-label">Select Shape (Aspect Ratio)</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }}>
{ASPECT_RATIOS.map((ratio) => {
const isActive = Math.abs((constraints.width / constraints.height) - (ratio.width / ratio.height)) < 0.1;
return (
<button
key={ratio.id}
className={`m3-btn m3-btn-outlined aspect-ratio-btn ${isActive ? "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={() => handleAspectRatioChange(ratio.id, isSD15OrCustom ? "sd15" : (constraints.width >= 1024 ? "sdxl" : "sd15"))}
>
<div className={`aspect-ratio-preview ratio-${ratio.id.replace(":", "-")}`}></div>
<span style={{ fontWeight: 600, fontSize: "0.85rem", marginTop: "2px" }}>{ratio.label}</span>
<span style={{ fontSize: "0.68rem", opacity: 0.7 }}>
{isSD15OrCustom
? `Max: ${ratio.id === "1:1" ? "512x512" : ratio.id === "4:3" ? "512x384" : ratio.id === "16:9" ? "512x288" : "288x512"}`
: ratio.desc}
</span>
</button>
);
})}
</div>
</div>
</div>
</div>
{/* Card 2: Quality & Logic */}
<div className="m3-card">
<h3 className="m3-card-title">
<Sliders size={18} style={{ color: "var(--md-sys-color-primary)" }} />
2. Quality, Speed & Logic
</h3>
<div className="m3-field-group">
{/* Quality Steps Slider */}
<div className="m3-slider-group">
<div className="m3-slider-header">
<span className="m3-slider-label">Detail Steps (Inference speed)</span>
<span className="m3-slider-value">{constraints.steps} steps</span>
</div>
<input
type="range"
className="m3-slider"
value={constraints.steps}
onChange={(e) => updateConstraint("steps", parseInt(e.target.value))}
min="1"
max="60"
/>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", lineHeight: "1.3" }}>
The number of times the AI cleans up the image. More steps = sharper details, but takes longer.
</span>
</div>
{/* Random Seed DNA */}
<div className="m3-text-field" style={{ marginTop: "8px" }}>
<label className="m3-text-field-label">Image Blueprint (DNA Seed)</label>
<div style={{ display: "flex", gap: "8px" }}>
<input
type="number"
className="m3-input"
value={constraints.seed}
onChange={(e) => updateConstraint("seed", parseInt(e.target.value) || -1)}
placeholder="-1 for a brand new image..."
style={{ flex: 1, height: "40px" }}
/>
<button
className="m3-btn m3-btn-tonal"
onClick={() => updateConstraint("seed", -1)}
style={{ height: "40px" }}
>
Random (-1)
</button>
</div>
</div>
</div>
</div>
</div>
{/* Right Column: Speed & System */}
<div>
{/* Card 3: Performance hacks */}
<div className="m3-card">
<h3 className="m3-card-title">
<Cpu size={18} style={{ color: "var(--md-sys-color-primary)" }} />
3. System Settings
</h3>
<div className="m3-field-group">
<div className="m3-text-field">
<label className="m3-text-field-label">Generation Backend</label>
<div className="m3-segmented-button">
{availableBackends.map((backend) => (
<div
key={backend.id}
className={`m3-segment-item ${constraints.backendType === backend.id || (!constraints.backendType && backend.id === "cpu") ? "active" : ""}`}
onClick={() => handleBackendChange(backend.id)}
title={backend.id === "cuda" ? "CUDA appears only when an NVIDIA CUDA backend is available." : undefined}
>
{backend.label}
</div>
))}
</div>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px", lineHeight: 1.35 }}>
CPU is slow but safest. Vulkan works on supported GPUs. CUDA is shown only when NVIDIA CUDA support is available.
</span>
{constraints.backendType === "cuda" && specs?.gpu_name && String(specs.gpu_name).toLowerCase().includes("gtx") && (
<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"
}}>
<strong>Performance Alert:</strong> Your graphics card (<code>{specs.gpu_name}</code>) is a GTX-series GPU which lacks hardware <strong>Tensor Cores</strong>. Running in CUDA mode will be up to 3x slower. We strongly recommend switching to <strong>Vulkan GPU</strong> for optimal generation speed.
</div>
)}
{backendOptions?.unavailable?.length > 0 && (
<div style={{ marginTop: "8px", display: "flex", flexDirection: "column", gap: "4px" }}>
{backendOptions.unavailable.map((backend) => (
<span key={backend.id} style={{ fontSize: "0.75rem", color: "var(--md-sys-color-error)", lineHeight: 1.35 }}>
{backend.label} unavailable: {backend.reason}
</span>
))}
</div>
)}
</div>
<div className="m3-text-field" style={{ marginTop: "20px" }}>
<label className="m3-text-field-label">Memory Optimization (GPU VRAM)</label>
<div style={{ display: "flex", flexDirection: "column", gap: "14px", marginTop: "10px" }}>
<label style={{ display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer", fontSize: "0.85rem" }}>
<input
type="checkbox"
checked={constraints.vaeTiling !== false}
onChange={(e) => updateConstraint("vaeTiling", e.target.checked)}
style={{
width: "16px",
height: "16px",
marginTop: "3px",
accentColor: "var(--md-sys-color-primary)",
cursor: "pointer"
}}
/>
<div>
<strong style={{ color: "var(--md-sys-color-on-surface)" }}>Enable VAE Tiling</strong>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "2px", lineHeight: 1.35 }}>
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.
</div>
</div>
</label>
<label style={{ display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer", fontSize: "0.85rem" }}>
<input
type="checkbox"
checked={constraints.vaeOnCpu === true}
onChange={(e) => updateConstraint("vaeOnCpu", e.target.checked)}
style={{
width: "16px",
height: "16px",
marginTop: "3px",
accentColor: "var(--md-sys-color-primary)",
cursor: "pointer"
}}
/>
<div>
<strong style={{ color: "var(--md-sys-color-on-surface)" }}>Run VAE on CPU</strong>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "2px", lineHeight: 1.35 }}>
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.
</div>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default memo(ImageConstraints);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+397 -7
View File
@@ -1,9 +1,40 @@
import React, { memo } from "react";
import { Image, FolderDown, Sliders, Shield, Terminal } from "lucide-react";
import { Image, FolderDown, MessageSquare, Mic, Settings, Shield, Terminal, ChevronDown, ChevronUp, Trash2, Volume2 } from "lucide-react";
function Sidebar({ activeTab, setActiveTab, specs }) {
function formatSidebarDate(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const dateStr = date.toLocaleDateString();
const timeStr = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', hour12: true });
return `${dateStr} ${timeStr}`;
}
function Sidebar({
collapsed = false,
activeTab,
setActiveTab,
specs,
conversations = [],
activeConversationId,
setActiveConversationId,
showHistory,
setShowHistory,
onDeleteConversation,
speechTranscriptions = [],
selectedSpeechTranscript,
setSelectedSpeechTranscript,
showSpeechHistory,
setShowSpeechHistory,
onDeleteSpeechTranscription,
ttsOutputs = [],
selectedTtsOutput,
setSelectedTtsOutput,
showTtsHistory,
setShowTtsHistory,
onDeleteTtsOutput
}) {
return (
<div className="sidebar">
<div className={`sidebar ${collapsed ? "collapsed" : ""}`}>
<div>
{/* Sidebar Header */}
<div className="sidebar-logo">
@@ -21,6 +52,365 @@ function Sidebar({ activeTab, setActiveTab, specs }) {
<span>Image Generator</span>
</div>
<div className="nav-item-wrapper" style={{ display: "flex", flexDirection: "column" }}>
<div
className={`nav-item ${activeTab === "chat" ? "active" : ""}`}
onClick={() => setActiveTab("chat")}
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", boxSizing: "border-box" }}
>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<MessageSquare size={20} />
<span>Text Chat</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowHistory(!showHistory);
}}
style={{
background: "none",
border: "none",
color: "inherit",
cursor: "pointer",
padding: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
transition: "background-color 0.2s"
}}
className="history-toggle-arrow"
title={showHistory ? "Hide Chat History" : "Show Chat History"}
>
{showHistory ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
</div>
{/* Sidebar Chat History List */}
{showHistory && (
<div
className="sidebar-history-list"
style={{
paddingLeft: "14px",
marginTop: "6px",
marginBottom: "6px",
display: "flex",
flexDirection: "column",
gap: "4px",
maxHeight: "220px",
overflowY: "auto",
borderLeft: "2px solid var(--border-color)"
}}
>
{conversations.length === 0 ? (
<div style={{ padding: "8px 12px", fontSize: "0.78rem", color: "var(--md-sys-color-outline)", opacity: 0.8 }}>
No saved chats
</div>
) : (
conversations.map((conv) => {
const isActive = activeConversationId === conv.id;
return (
<div
key={conv.id}
onClick={() => {
setActiveConversationId(conv.id);
setActiveTab("chat");
}}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "6px 8px 6px 10px",
borderRadius: "var(--md-shape-corner-small)",
fontSize: "0.78rem",
cursor: "pointer",
background: isActive ? "var(--md-sys-color-secondary-container)" : "transparent",
color: isActive ? "var(--md-sys-color-on-secondary-container)" : "var(--md-sys-color-on-surface-variant)",
border: isActive ? "1px solid var(--md-sys-color-outline-variant)" : "1px solid transparent",
transition: "background 0.2s"
}}
className="sidebar-history-item"
title={conv.title}
>
<span style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
fontWeight: isActive ? 600 : 400
}}>
{conv.title}
</span>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteConversation(conv.id, e);
}}
style={{
background: "none",
border: "none",
color: "var(--md-sys-color-outline)",
cursor: "pointer",
padding: "2px",
marginLeft: "6px",
display: "flex",
alignItems: "center"
}}
className="sidebar-history-delete"
title="Delete Conversation"
>
<Trash2 size={12} />
</button>
</div>
);
})
)}
</div>
)}
</div>
<div className="nav-item-wrapper" style={{ display: "flex", flexDirection: "column" }}>
<div
className={`nav-item ${activeTab === "speech" ? "active" : ""}`}
onClick={() => setActiveTab("speech")}
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", boxSizing: "border-box" }}
>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<Mic size={20} />
<span>Speech Transcriber</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowSpeechHistory(!showSpeechHistory);
}}
style={{
background: "none",
border: "none",
color: "inherit",
cursor: "pointer",
padding: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
transition: "background-color 0.2s"
}}
className="history-toggle-arrow"
title={showSpeechHistory ? "Hide Transcriptions" : "Show Transcriptions"}
>
{showSpeechHistory ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
</div>
{showSpeechHistory && (
<div
className="sidebar-history-list"
style={{
paddingLeft: "14px",
marginTop: "6px",
marginBottom: "6px",
display: "flex",
flexDirection: "column",
gap: "4px",
maxHeight: "220px",
overflowY: "auto",
borderLeft: "2px solid var(--border-color)"
}}
>
{speechTranscriptions.length === 0 ? (
<div style={{ padding: "8px 12px", fontSize: "0.78rem", color: "var(--md-sys-color-outline)", opacity: 0.8 }}>
No saved transcriptions
</div>
) : (
speechTranscriptions.map((item) => {
const itemId = item.filename || item.metadata || item.textFile;
const isActive = selectedSpeechTranscript && (selectedSpeechTranscript.filename || selectedSpeechTranscript.metadata || selectedSpeechTranscript.textFile) === itemId;
const title = item.displayName || item.sourceFilename || item.textFile || item.filename || "Transcript";
const date = formatSidebarDate(item.modifiedAt || item.createdAt);
return (
<div
key={itemId}
onClick={() => {
setSelectedSpeechTranscript(item);
setActiveTab("speech");
}}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: "7px 8px 7px 10px",
borderRadius: "var(--md-shape-corner-small)",
fontSize: "0.78rem",
cursor: "pointer",
background: isActive ? "var(--md-sys-color-secondary-container)" : "transparent",
color: isActive ? "var(--md-sys-color-on-secondary-container)" : "var(--md-sys-color-on-surface-variant)",
border: isActive ? "1px solid var(--md-sys-color-outline-variant)" : "1px solid transparent",
transition: "background 0.2s"
}}
className="sidebar-history-item"
title={title}
>
<div style={{ display: "flex", flexDirection: "column", gap: "2px", flex: 1, minWidth: 0 }}>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: isActive ? 600 : 400 }}>
{title}
</span>
{date && (
<span style={{ fontSize: "0.68rem", opacity: 0.72, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{date}
</span>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteSpeechTranscription?.(item, e);
}}
style={{
background: "none",
border: "none",
color: "var(--md-sys-color-outline)",
cursor: "pointer",
padding: "2px",
marginLeft: "6px",
display: "flex",
alignItems: "center"
}}
className="sidebar-history-delete"
title="Delete Transcription"
>
<Trash2 size={12} />
</button>
</div>
);
})
)}
</div>
)}
</div>
<div className="nav-item-wrapper" style={{ display: "flex", flexDirection: "column" }}>
<div
className={`nav-item ${activeTab === "tts" ? "active" : ""}`}
onClick={() => setActiveTab("tts")}
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", boxSizing: "border-box" }}
>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<Volume2 size={20} />
<span>Text to Speech</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowTtsHistory(!showTtsHistory);
}}
style={{
background: "none",
border: "none",
color: "inherit",
cursor: "pointer",
padding: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
transition: "background-color 0.2s"
}}
className="history-toggle-arrow"
title={showTtsHistory ? "Hide TTS Outputs" : "Show TTS Outputs"}
>
{showTtsHistory ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
</div>
{showTtsHistory && (
<div
className="sidebar-history-list"
style={{
paddingLeft: "14px",
marginTop: "6px",
marginBottom: "6px",
display: "flex",
flexDirection: "column",
gap: "4px",
maxHeight: "220px",
overflowY: "auto",
borderLeft: "2px solid var(--border-color)"
}}
>
{ttsOutputs.length === 0 ? (
<div style={{ padding: "8px 12px", fontSize: "0.78rem", color: "var(--md-sys-color-outline)", opacity: 0.8 }}>
No saved audio
</div>
) : (
ttsOutputs.map((item) => {
const itemId = item.filename || item.metadata || item.audioFile;
const isActive = selectedTtsOutput && (selectedTtsOutput.filename || selectedTtsOutput.metadata || selectedTtsOutput.audioFile) === itemId;
const title = item.displayName || item.text || item.audioFile || item.filename || "TTS Output";
const date = formatSidebarDate(item.modifiedAt || item.createdAt);
return (
<div
key={itemId}
onClick={() => {
setSelectedTtsOutput(item);
setActiveTab("tts");
}}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: "7px 8px 7px 10px",
borderRadius: "var(--md-shape-corner-small)",
fontSize: "0.78rem",
cursor: "pointer",
background: isActive ? "var(--md-sys-color-secondary-container)" : "transparent",
color: isActive ? "var(--md-sys-color-on-secondary-container)" : "var(--md-sys-color-on-surface-variant)",
border: isActive ? "1px solid var(--md-sys-color-outline-variant)" : "1px solid transparent",
transition: "background 0.2s"
}}
className="sidebar-history-item"
title={title}
>
<div style={{ display: "flex", flexDirection: "column", gap: "2px", flex: 1, minWidth: 0 }}>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: isActive ? 600 : 400 }}>
{title}
</span>
{date && (
<span style={{ fontSize: "0.68rem", opacity: 0.72, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{date}
</span>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteTtsOutput?.(item, e);
}}
style={{
background: "none",
border: "none",
color: "var(--md-sys-color-outline)",
cursor: "pointer",
padding: "2px",
marginLeft: "6px",
display: "flex",
alignItems: "center"
}}
className="sidebar-history-delete"
title="Delete TTS Output"
>
<Trash2 size={12} />
</button>
</div>
);
})
)}
</div>
)}
</div>
<div
className={`nav-item ${activeTab === "models" ? "active" : ""}`}
onClick={() => setActiveTab("models")}
@@ -30,11 +420,11 @@ function Sidebar({ activeTab, setActiveTab, specs }) {
</div>
<div
className={`nav-item ${activeTab === "constraints" ? "active" : ""}`}
onClick={() => setActiveTab("constraints")}
className={`nav-item ${activeTab === "settings" ? "active" : ""}`}
onClick={() => setActiveTab("settings")}
>
<Sliders size={20} />
<span>Image Constraints</span>
<Settings size={20} />
<span>Settings</span>
</div>
</div>
</div>
@@ -0,0 +1,558 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
FileAudio,
LoaderCircle,
Mic,
Play,
Save,
Square,
Upload,
X,
} from "lucide-react";
import {
getSpeechStatus,
listSpeechModels,
startSpeech,
stopSpeech,
transcribeSpeech,
} from "../services/api";
const LANGUAGE_OPTIONS = [
{ value: "auto", label: "Auto detect" },
{ value: "en", label: "English" },
{ value: "es", label: "Spanish" },
{ value: "fr", label: "French" },
{ value: "de", label: "German" },
{ value: "it", label: "Italian" },
{ value: "pt", label: "Portuguese" },
{ value: "hi", label: "Hindi" },
{ value: "ja", label: "Japanese" },
{ value: "ko", label: "Korean" },
{ value: "zh", label: "Chinese" },
];
function flattenFloat32(chunks) {
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const merged = new Float32Array(total);
let offset = 0;
chunks.forEach((chunk) => {
merged.set(chunk, offset);
offset += chunk.length;
});
return merged;
}
function resampleLinear(input, sourceRate, targetRate) {
if (sourceRate === targetRate) return input;
const ratio = sourceRate / targetRate;
const newLength = Math.max(1, Math.round(input.length / ratio));
const output = new Float32Array(newLength);
for (let i = 0; i < newLength; i += 1) {
const src = i * ratio;
const left = Math.floor(src);
const right = Math.min(input.length - 1, left + 1);
const weight = src - left;
output[i] = input[left] * (1 - weight) + input[right] * weight;
}
return output;
}
function encodeWavPcm16(samples, sampleRate = 16000) {
const bytesPerSample = 2;
const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
const view = new DataView(buffer);
const writeString = (offset, value) => {
for (let i = 0; i < value.length; i += 1) {
view.setUint8(offset + i, value.charCodeAt(i));
}
};
writeString(0, "RIFF");
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
writeString(8, "WAVE");
writeString(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * bytesPerSample, true);
view.setUint16(32, bytesPerSample, true);
view.setUint16(34, 16, true);
writeString(36, "data");
view.setUint32(40, samples.length * bytesPerSample, true);
let offset = 44;
for (let i = 0; i < samples.length; i += 1, offset += 2) {
const sample = Math.max(-1, Math.min(1, samples[i]));
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
}
return new Blob([view], { type: "audio/wav" });
}
function downloadBlob(filename, content, type) {
const blob = content instanceof Blob ? content : new Blob([content], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function normalizeTimestamp(value) {
if (!value) return "";
if (typeof value === "string") {
const from = value.match(/from=([^;},]+)/)?.[1];
const to = value.match(/to=([^;},]+)/)?.[1];
return from && to ? `${from} - ${to}` : value;
}
if (typeof value === "object" && value.from && value.to) {
return `${value.from} - ${value.to}`;
}
return "";
}
function formatTranscriptText(transcription, showTimestamps) {
const segments = transcription?.raw?.transcription;
if (!Array.isArray(segments) || segments.length === 0) {
return transcription?.text || "";
}
return segments
.map((segment) => {
const text = String(segment.text || "").trim();
if (!showTimestamps) return text;
const time = normalizeTimestamp(segment.timestamps);
return time ? `[${time}] ${text}` : text;
})
.filter(Boolean)
.join("\n");
}
export default function SpeechTranscriber({
showAlert,
showConfirm,
selectedTranscript,
onTranscriptionsChanged,
speechSettings,
setSpeechSettings,
}) {
const [status, setStatus] = useState({ ready: false, running: false, backendInstalled: false, settings: {} });
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState("");
const language = speechSettings?.language || "auto";
const threads = speechSettings?.threads || 4;
const backendPreference = speechSettings?.backendPreference || "auto";
const translate = speechSettings?.translate === true;
const setLanguage = (val) => setSpeechSettings((prev) => ({ ...prev, language: val }));
const setThreads = (val) => setSpeechSettings((prev) => ({ ...prev, threads: val }));
const setBackendPreference = (val) => setSpeechSettings((prev) => ({ ...prev, backendPreference: val }));
const [audioBlob, setAudioBlob] = useState(null);
const [audioName, setAudioName] = useState("");
const [audioUrl, setAudioUrl] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [isTranscribing, setIsTranscribing] = useState(false);
const [result, setResult] = useState(null);
const [resultText, setResultText] = useState("");
const [showTimestamps, setShowTimestamps] = useState(false);
const [isLoadingModel, setIsLoadingModel] = useState(false);
const recorderRef = useRef(null);
const abortRef = useRef(null);
const installedModels = useMemo(() => models.filter((model) => model.installed), [models]);
const isSelectedRuntimeLoaded = status.ready &&
status.settings?.model === selectedModel &&
(status.settings?.backendPreference || "auto") === backendPreference;
const isEnglishOnly = useMemo(() => {
if (!selectedModel) return false;
const model = models.find((m) => m.filename === selectedModel);
return model?.language === "English" || selectedModel.toLowerCase().includes(".en");
}, [selectedModel, models]);
const refresh = useCallback(async () => {
const [nextStatus, nextModels] = await Promise.all([
getSpeechStatus(),
listSpeechModels(),
]);
setStatus(nextStatus);
setModels(nextModels);
setSelectedModel((current) => {
const installed = nextModels.filter((model) => model.installed);
if (current && installed.some((model) => model.filename === current || model.id === current)) return current;
const active = nextStatus.settings?.model;
if (active && installed.some((model) => model.filename === active)) return active;
return installed.find((model) => model.recommended)?.filename ||
installed[0]?.filename ||
"";
});
}, []);
useEffect(() => {
refresh().catch((err) => showAlert?.({ title: "Speech Status Failed", message: err.message || String(err), danger: true }));
const interval = setInterval(() => {
refresh().catch(() => {});
}, 2000);
return () => clearInterval(interval);
}, [refresh, showAlert]);
useEffect(() => {
if (!selectedTranscript) return;
setResult(selectedTranscript);
}, [selectedTranscript]);
useEffect(() => {
if (!result) return;
setResultText(formatTranscriptText(result, showTimestamps));
}, [result, showTimestamps]);
useEffect(() => {
return () => {
if (audioUrl) URL.revokeObjectURL(audioUrl);
abortRef.current?.abort();
};
}, [audioUrl]);
const setCurrentAudio = useCallback((blob, name) => {
setAudioBlob(blob);
setAudioName(name);
setResult(null);
setResultText("");
setAudioUrl((oldUrl) => {
if (oldUrl) URL.revokeObjectURL(oldUrl);
return URL.createObjectURL(blob);
});
}, []);
const clearCurrentAudio = useCallback(() => {
setAudioBlob(null);
setAudioName("");
setAudioUrl((oldUrl) => {
if (oldUrl) URL.revokeObjectURL(oldUrl);
return "";
});
}, []);
const handleLoadSpeechModel = async () => {
if (!selectedModel) return;
setIsLoadingModel(true);
try {
if (!isSelectedRuntimeLoaded) {
await startSpeech(selectedModel, { language, threads, backendPreference });
}
await refresh();
} catch (err) {
showAlert?.({ title: "Speech Model Load Failed", message: err.message || String(err), danger: true });
} finally {
setIsLoadingModel(false);
}
};
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
const chunks = [];
processor.onaudioprocess = (event) => {
chunks.push(new Float32Array(event.inputBuffer.getChannelData(0)));
};
source.connect(processor);
processor.connect(audioContext.destination);
recorderRef.current = { stream, audioContext, source, processor, chunks, sampleRate: audioContext.sampleRate };
setIsRecording(true);
} catch (err) {
showAlert?.({ title: "Microphone Failed", message: err.message || String(err), danger: true });
}
};
const stopRecording = async () => {
const recorder = recorderRef.current;
if (!recorder) return;
recorder.processor.disconnect();
recorder.source.disconnect();
recorder.stream.getTracks().forEach((track) => track.stop());
await recorder.audioContext.close();
recorderRef.current = null;
setIsRecording(false);
const merged = flattenFloat32(recorder.chunks);
const resampled = resampleLinear(merged, recorder.sampleRate, 16000);
const wav = encodeWavPcm16(resampled, 16000);
setCurrentAudio(wav, `recording-${new Date().toISOString().replace(/[:.]/g, "-")}.wav`);
};
const handleUploadAudio = (event) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) return;
if (!file.name.toLowerCase().endsWith(".wav")) {
showAlert?.({ title: "Unsupported Audio", message: "V1 accepts WAV files only. MP3/WebM/M4A will need the later FFmpeg path.", danger: true });
return;
}
setCurrentAudio(file, file.name);
};
const handleTranscribe = async () => {
if (!audioBlob || !selectedModel) return;
setIsTranscribing(true);
setResult(null);
setResultText("");
const controller = new AbortController();
abortRef.current = controller;
try {
if (!isSelectedRuntimeLoaded) {
setIsLoadingModel(true);
try {
await startSpeech(selectedModel, { language, threads, backendPreference });
await refresh();
} finally {
setIsLoadingModel(false);
}
}
const transcription = await transcribeSpeech(audioBlob, {
model: selectedModel,
language,
threads,
backendPreference,
translate,
filename: audioName || "audio.wav",
signal: controller.signal,
});
setResult(transcription);
await refresh();
await onTranscriptionsChanged?.();
if (transcription?.text?.toLowerCase().includes("foreign language")) {
showAlert?.({
title: "Foreign Language Detected",
message: "The loaded model is English-only and cannot transcribe non-English speech. Please download and load a Multilingual model (e.g. Whisper Base Multilingual) from the Model Manager to transcribe this audio.",
danger: false,
});
}
} catch (err) {
if (err.name !== "AbortError") {
showAlert?.({ title: "Transcription Failed", message: err.message || String(err), danger: true });
}
} finally {
abortRef.current = null;
setIsTranscribing(false);
}
};
const handleCancel = () => {
abortRef.current?.abort();
setIsTranscribing(false);
};
const saveText = () => {
if (!resultText.trim()) return;
downloadBlob("transcription.txt", resultText, "text/plain;charset=utf-8");
};
const saveJson = () => {
const payload = { ...(result || {}), text: resultText };
downloadBlob("transcription.json", JSON.stringify(payload, null, 2), "application/json;charset=utf-8");
};
return (
<div className="workspace-area speech-workspace">
<div className="workspace-title-section">
<h2 className="workspace-title">Speech Transcriber</h2>
<p className="workspace-subtitle">
Record microphone audio or upload a WAV file, then transcribe locally with whisper.cpp.
</p>
</div>
<div className="speech-grid">
<section className="m3-card speech-panel">
<div className="speech-panel-header">
<h3>Runtime</h3>
<span className={`status-chip ${status.backendInstalled ? "" : "offline"}`}>
{status.backendInstalled ? (status.ready ? "Ready" : "Installed") : "Backend missing"}
</span>
</div>
{status.error && <div className="text-progress error">{status.error}</div>}
<label className="speech-label">
Whisper model
<select className="m3-input" value={selectedModel} onChange={(event) => setSelectedModel(event.target.value)}>
{installedModels.length === 0 && <option value="">No downloaded speech models</option>}
{installedModels.map((model) => (
<option key={model.id || model.filename} value={model.filename}>
{model.name || model.filename}
</option>
))}
</select>
</label>
<div className="speech-controls-row">
<label className="speech-label">
Backend
<select className="m3-input" value={backendPreference} onChange={(event) => setBackendPreference(event.target.value)}>
<option value="auto">Auto (GPU if installed)</option>
{(status.backends || []).map((backend) => (
<option key={backend.key} value={backend.key}>
{backend.label}{backend.installed ? " - installed" : " - missing"}
</option>
))}
</select>
</label>
<label className="speech-label">
Language
<select className="m3-input" value={language} onChange={(event) => setLanguage(event.target.value)}>
{LANGUAGE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</label>
<label className="speech-label">
Threads
<input
className="m3-input"
type="number"
min="1"
max="32"
value={threads}
onChange={(event) => setThreads(Math.max(1, Math.min(32, Number(event.target.value) || 1)))}
/>
</label>
</div>
<div style={{ marginTop: "10px", paddingLeft: "2px" }}>
<label style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "0.82rem", cursor: "pointer", color: "var(--md-sys-color-on-surface)" }}>
<input
type="checkbox"
checked={translate}
onChange={(event) => setSpeechSettings((prev) => ({ ...prev, translate: event.target.checked }))}
style={{ cursor: "pointer", width: "15px", height: "15px" }}
/>
<span>Translate to English</span>
</label>
</div>
<div className="speech-button-row">
<button
className="m3-btn m3-btn-filled"
onClick={handleLoadSpeechModel}
disabled={!selectedModel || isLoadingModel || isTranscribing || isSelectedRuntimeLoaded}
>
{isLoadingModel ? <LoaderCircle className="progress-spinner" size={14} /> : <Play size={14} />}
<span>{isSelectedRuntimeLoaded ? "Loaded" : isLoadingModel ? "Loading" : "Load"}</span>
</button>
<button className="m3-btn m3-btn-outlined" onClick={refresh}>
<Play size={14} />
<span>Refresh</span>
</button>
</div>
{installedModels.length === 0 && (
<div className="text-progress">
Download a Whisper model from Model Manager, Speech Models first.
</div>
)}
</section>
<section className="m3-card speech-panel">
<div className="speech-panel-header">
<h3>Audio</h3>
<span className="status-chip">{audioName || "No audio selected"}</span>
</div>
<div className="speech-capture-zone">
<FileAudio size={42} />
<div>
<strong>{audioName || "Record or upload WAV"}</strong>
<p>Microphone recordings are converted to 16 kHz mono PCM16 WAV before transcription.</p>
</div>
</div>
{audioUrl && <audio controls src={audioUrl} style={{ width: "100%" }} />}
<div className="speech-button-row">
{isRecording ? (
<button className="m3-btn m3-btn-error" onClick={stopRecording}>
<Square size={14} />
<span>Stop Recording</span>
</button>
) : (
<button className="m3-btn m3-btn-filled" onClick={startRecording} disabled={isTranscribing}>
<Mic size={14} />
<span>Record</span>
</button>
)}
<label className="m3-btn m3-btn-outlined">
<Upload size={14} />
<span>Upload WAV</span>
<input type="file" accept=".wav,audio/wav" onChange={handleUploadAudio} hidden />
</label>
{audioBlob && (
<button className="m3-btn m3-btn-error" onClick={clearCurrentAudio} disabled={isTranscribing || isRecording}>
<X size={14} />
<span>Remove Audio</span>
</button>
)}
</div>
<div className="speech-button-row">
{isTranscribing ? (
<button className="m3-btn m3-btn-error" onClick={handleCancel}>
<Square size={14} />
<span>Cancel</span>
</button>
) : (
<button
className="m3-btn m3-btn-filled"
onClick={handleTranscribe}
disabled={!audioBlob || !selectedModel || !status.backendInstalled}
>
<Mic size={14} />
<span>Transcribe</span>
</button>
)}
{isTranscribing && <span className="text-progress"><LoaderCircle className="progress-spinner" size={14} /> Transcribing locally...</span>}
</div>
</section>
</div>
<section className="m3-card speech-result-panel">
<div className="speech-panel-header">
<h3>Transcript</h3>
<div className="speech-button-row">
<label style={{ display: "flex", alignItems: "center", gap: "6px", fontSize: "0.78rem", cursor: "pointer", userSelect: "none", color: "var(--md-sys-color-on-surface-variant)", marginRight: "12px" }}>
<input
type="checkbox"
checked={showTimestamps}
onChange={(e) => setShowTimestamps(e.target.checked)}
style={{ width: "14px", height: "14px", cursor: "pointer" }}
/>
<span>Show Timestamps</span>
</label>
<button className="m3-btn m3-btn-outlined" onClick={saveText} disabled={!resultText.trim()}>
<Save size={14} />
<span>TXT</span>
</button>
<button className="m3-btn m3-btn-outlined" onClick={saveJson} disabled={!resultText.trim()}>
<Save size={14} />
<span>JSON</span>
</button>
{status.ready && (
<button className="m3-btn m3-btn-error" onClick={() => stopSpeech().then(refresh)}>
<Square size={14} />
<span>Stop Runtime</span>
</button>
)}
</div>
</div>
<textarea
className="m3-textarea speech-result-textarea"
value={resultText}
onChange={(event) => setResultText(event.target.value)}
placeholder="Transcript output will appear here."
/>
</section>
</div>
);
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,317 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
LoaderCircle,
Play,
RefreshCw,
Save,
Square,
Volume2,
} from "lucide-react";
import {
getTtsStatus,
listTtsModels,
speakTts,
startTts,
stopTts,
} from "../services/api";
const FALLBACK_VOICES = [
{ id: "af_heart", name: "Heart", language: "en-us", gender: "Female" },
{ id: "af_bella", name: "Bella", language: "en-us", gender: "Female" },
{ id: "am_michael", name: "Michael", language: "en-us", gender: "Male" },
{ id: "bf_emma", name: "Emma", language: "en-gb", gender: "Female" },
{ id: "bm_george", name: "George", language: "en-gb", gender: "Male" },
];
function downloadUrl(url, filename) {
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}
export default function TextToSpeech({
showAlert,
selectedOutput,
onOutputsChanged,
ttsSettings,
setTtsSettings,
}) {
const [status, setStatus] = useState({ ready: false, running: false, runtimeInstalled: false, settings: {}, voices: [] });
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState("");
const [text, setText] = useState("Hello from Local AI Studio text to speech.");
const [isLoadingModel, setIsLoadingModel] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [output, setOutput] = useState(null);
const abortRef = useRef(null);
const installedModels = useMemo(() => models.filter((model) => model.installed), [models]);
const voices = status.voices?.length ? status.voices : FALLBACK_VOICES;
const selectedVoice = ttsSettings?.voice || "af_heart";
const speed = ttsSettings?.speed || 1;
const updateTtsSetting = (key, value) => {
setTtsSettings((prev) => ({ ...prev, [key]: value }));
};
const refresh = useCallback(async () => {
const [nextStatus, nextModels] = await Promise.all([
getTtsStatus(),
listTtsModels(),
]);
setStatus(nextStatus);
setModels(nextModels);
setSelectedModel((current) => {
const installed = nextModels.filter((model) => model.installed);
if (current && installed.some((model) => model.filename === current || model.id === current)) return current;
const active = nextStatus.settings?.model;
if (active && installed.some((model) => model.filename === active)) return active;
const preferred = ttsSettings?.model;
if (preferred && installed.some((model) => model.filename === preferred)) return preferred;
return installed.find((model) => model.recommended)?.filename || installed[0]?.filename || "";
});
}, [ttsSettings?.model]);
useEffect(() => {
refresh().catch((err) => showAlert?.({ title: "TTS Status Failed", message: err.message || String(err), danger: true }));
const interval = setInterval(() => {
refresh().catch(() => {});
}, 2500);
return () => clearInterval(interval);
}, [refresh, showAlert]);
useEffect(() => {
if (!selectedOutput) return;
setOutput(selectedOutput);
setText(selectedOutput.text || "");
if (selectedOutput.voice) updateTtsSetting("voice", selectedOutput.voice);
if (selectedOutput.speed) updateTtsSetting("speed", selectedOutput.speed);
if (selectedOutput.model) setSelectedModel(selectedOutput.model);
}, [selectedOutput]);
useEffect(() => {
return () => abortRef.current?.abort();
}, []);
const handleLoadModel = async () => {
if (!selectedModel) return;
setIsLoadingModel(true);
try {
await startTts(selectedModel, { voice: selectedVoice, speed });
updateTtsSetting("model", selectedModel);
await refresh();
} catch (err) {
showAlert?.({ title: "TTS Model Load Failed", message: err.message || String(err), danger: true });
} finally {
setIsLoadingModel(false);
}
};
const handleStop = async () => {
try {
await stopTts();
await refresh();
} catch (err) {
showAlert?.({ title: "Stop TTS Failed", message: err.message || String(err), danger: true });
}
};
const handleGenerate = async () => {
if (!text.trim() || !selectedModel) return;
setIsGenerating(true);
const controller = new AbortController();
abortRef.current = controller;
try {
if (!status.ready || status.settings?.model !== selectedModel) {
setIsLoadingModel(true);
try {
await startTts(selectedModel, { voice: selectedVoice, speed });
updateTtsSetting("model", selectedModel);
await refresh();
} finally {
setIsLoadingModel(false);
}
}
const generated = await speakTts(text, {
model: selectedModel,
voice: selectedVoice,
speed,
signal: controller.signal,
});
setOutput(generated);
await refresh();
await onOutputsChanged?.();
} catch (err) {
if (err.name !== "AbortError") {
showAlert?.({ title: "TTS Generation Failed", message: err.message || String(err), danger: true });
}
} finally {
abortRef.current = null;
setIsGenerating(false);
}
};
const handleCancel = () => {
abortRef.current?.abort();
setIsGenerating(false);
};
const isLoaded = status.ready && status.settings?.model === selectedModel;
return (
<div className="workspace-area speech-workspace">
<div className="workspace-title-section">
<h2 className="workspace-title">Text to Speech</h2>
<p className="workspace-subtitle">
Generate local WAV narration with Kokoro ONNX.
</p>
</div>
<div className="speech-grid">
<section className="m3-card speech-panel">
<div className="speech-panel-header">
<h3>Runtime</h3>
<span className={`status-chip ${status.runtimeInstalled ? "" : "offline"}`}>
{status.runtimeInstalled ? (status.ready ? "Ready" : "Installed") : "Runtime missing"}
</span>
</div>
{status.error && <div className="text-progress error">{status.error}</div>}
<label className="speech-label">
Kokoro model
<select className="m3-input" value={selectedModel} onChange={(event) => setSelectedModel(event.target.value)}>
{installedModels.length === 0 && <option value="">No downloaded TTS models</option>}
{installedModels.map((model) => (
<option key={model.id || model.filename} value={model.filename}>
{model.name || model.filename}
</option>
))}
</select>
</label>
<div className="speech-controls-row">
<label className="speech-label">
Voice
<select className="m3-input" value={selectedVoice} onChange={(event) => updateTtsSetting("voice", event.target.value)}>
{voices.map((voice) => (
<option key={voice.id} value={voice.id}>
{voice.name} ({voice.gender}, {voice.language})
</option>
))}
</select>
</label>
<label className="speech-label">
Speed
<input
className="m3-input"
type="number"
min="0.5"
max="2"
step="0.05"
value={speed}
onChange={(event) => updateTtsSetting("speed", Math.max(0.5, Math.min(2, Number(event.target.value) || 1)))}
/>
</label>
</div>
<div className="speech-button-row">
<button
className="m3-btn m3-btn-filled"
onClick={handleLoadModel}
disabled={!selectedModel || isLoadingModel || isGenerating || isLoaded}
>
{isLoadingModel ? <LoaderCircle className="progress-spinner" size={14} /> : <Play size={14} />}
<span>{isLoaded ? "Loaded" : isLoadingModel ? "Loading" : "Load"}</span>
</button>
<button className="m3-btn m3-btn-outlined" onClick={refresh}>
<RefreshCw size={14} />
<span>Refresh</span>
</button>
{status.ready && (
<button className="m3-btn m3-btn-error" onClick={handleStop}>
<Square size={14} />
<span>Stop Runtime</span>
</button>
)}
</div>
{installedModels.length === 0 && (
<div className="text-progress">
Download a Kokoro model from Model Manager, TTS Models first.
</div>
)}
</section>
<section className="m3-card speech-panel">
<div className="speech-panel-header">
<h3>Input</h3>
<span className="status-chip">{text.trim().length} chars</span>
</div>
<textarea
className="m3-input"
value={text}
onChange={(event) => setText(event.target.value)}
rows={10}
maxLength={5000}
style={{ width: "100%", resize: "vertical", minHeight: "220px", lineHeight: 1.45 }}
placeholder="Type text to turn into speech..."
/>
<div className="speech-button-row">
{isGenerating ? (
<button className="m3-btn m3-btn-error" onClick={handleCancel}>
<Square size={14} />
<span>Cancel</span>
</button>
) : (
<button
className="m3-btn m3-btn-filled"
onClick={handleGenerate}
disabled={!text.trim() || !selectedModel || !status.runtimeInstalled}
>
<Volume2 size={14} />
<span>Generate WAV</span>
</button>
)}
{isGenerating && <span className="text-progress"><LoaderCircle className="progress-spinner" size={14} /> Generating locally...</span>}
</div>
</section>
</div>
<section className="m3-card speech-result-panel">
<div className="speech-panel-header">
<h3>Output</h3>
<div className="speech-button-row">
<button className="m3-btn m3-btn-outlined" onClick={() => output?.url && downloadUrl(output.url, output.audioFile || "tts.wav")} disabled={!output?.url}>
<Save size={14} />
<span>WAV</span>
</button>
</div>
</div>
{output?.url ? (
<div style={{ display: "flex", flexDirection: "column", gap: "14px" }}>
<audio controls src={output.url} style={{ width: "100%" }} />
<div className="text-progress">
{output.voiceName || output.voice} &bull; {output.modelName || output.model} &bull; {new Date(output.createdAt).toLocaleString()}
</div>
<textarea
className="m3-input"
value={output.text || text}
onChange={(event) => setOutput((prev) => ({ ...(prev || {}), text: event.target.value }))}
rows={5}
style={{ width: "100%", resize: "vertical", minHeight: "120px", lineHeight: 1.45 }}
/>
</div>
) : (
<div className="empty-state" style={{ padding: "32px 0" }}>
<Volume2 size={42} />
<p>Generated speech will appear here.</p>
</div>
)}
</section>
</div>
);
}
+121 -26
View File
@@ -1,48 +1,146 @@
import React, { memo } from "react";
import { Cpu, HardDrive, Database, Square, RefreshCw, Sun, Moon } from "lucide-react";
import React, { memo, useState, useRef, useEffect } from "react";
import { Square, RefreshCw, Sun, Moon, Palette, Check, PanelLeftClose, PanelLeft } from "lucide-react";
import { THEMES } from "../themes";
function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isStoppingServer = false, theme, setTheme }) {
const formatGb = (value) => {
const truncateModelName = (name) => {
if (!name || typeof name !== "string") return "";
let cleanName = name;
if (name.includes("--")) {
const parts = name.split("--");
cleanName = parts[parts.length - 1];
}
if (cleanName.length > 25) {
return cleanName.substring(0, 22) + "...";
}
return cleanName;
};
function TopStatusBar({
telemetry,
serverRunning,
activeModel,
isLlmLoaded = false,
onStopServer,
isStoppingServer = false,
theme,
setTheme,
sidebarVisible,
onToggleSidebar
}) {
const [showThemeMenu, setShowThemeMenu] = useState(false);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowThemeMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const formatGb = (value, { allowZero = false } = {}) => {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number.toFixed(number >= 10 ? 0 : 1) : "--";
if (!Number.isFinite(number) || number < 0) return "--";
if (number === 0 && !allowZero) return "--";
return number.toFixed(number >= 10 ? 0 : 1);
};
const getStatusText = () => {
if (isLlmLoaded) return "Model Loaded (Text)";
if (activeModel) return "Model Loaded (Image)";
if (serverRunning) return "Server Active";
return "Local Mode";
};
const getStatusClass = () => {
if (activeModel || isLlmLoaded) return "status-indicator";
if (serverRunning) return "status-indicator busy";
return "status-indicator offline";
};
const isDark = (theme || "dark").startsWith("dark");
return (
<div className="top-status-bar">
<div className="current-model-info">
<div className={getStatusClass()}></div>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
{getStatusText()}
</span>
{activeModel && (
<>
<span style={{ color: "var(--md-sys-color-outline-variant)" }}>|</span>
<span style={{ color: "var(--md-sys-color-primary)", fontWeight: 700 }}>
{activeModel}
</span>
</>
)}
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<button
onClick={onToggleSidebar}
className="sidebar-toggle-btn"
title={sidebarVisible ? "Collapse Sidebar" : "Expand Sidebar"}
>
{sidebarVisible ? <PanelLeftClose size={18} /> : <PanelLeft size={18} />}
</button>
<div className="current-model-info">
<div className={getStatusClass()}></div>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
{getStatusText()}
</span>
{(activeModel || (typeof isLlmLoaded === "string" && isLlmLoaded)) && (
<>
<span style={{ color: "var(--md-sys-color-outline-variant)" }}>|</span>
<span
style={{ color: "var(--md-sys-color-primary)", fontWeight: 700 }}
title={activeModel || isLlmLoaded}
>
{truncateModelName(activeModel || isLlmLoaded)}
</span>
</>
)}
</div>
</div>
<div className="telemetry-group">
<div className="telemetry-group" style={{ position: "relative" }}>
<button
className="theme-toggle-btn"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
title={`Switch to ${theme === "dark" ? "light" : "dark"} theme`}
onClick={() => setTheme(isDark ? "light" : "dark")}
title={`Switch to ${isDark ? "light" : "dark"} theme`}
>
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
{isDark ? <Sun size={18} /> : <Moon size={18} />}
</button>
<div ref={menuRef} style={{ position: "relative", display: "inline-block" }}>
<button
className={`theme-toggle-btn ${showThemeMenu ? "active" : ""}`}
onClick={() => setShowThemeMenu(!showThemeMenu)}
title="Choose a custom color theme"
style={{ marginRight: "12px" }}
>
<Palette size={18} />
</button>
{showThemeMenu && (
<div className="theme-dropdown-menu">
<div className="theme-dropdown-header">Select Theme</div>
<div className="theme-dropdown-divider"></div>
{THEMES.map((t) => {
const isActive = theme === t.id;
return (
<button
key={t.id}
className={`theme-dropdown-item ${isActive ? "active" : ""}`}
onClick={() => {
setTheme(t.id);
setShowThemeMenu(false);
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px", width: "100%" }}>
<div style={{ display: "flex", gap: "2px" }}>
<div style={{ width: "8px", height: "8px", borderRadius: "50%", background: t.primary }} />
<div style={{ width: "8px", height: "8px", borderRadius: "50%", background: t.secondary }} />
</div>
<span className="theme-name-text">{t.name}</span>
{isActive && <Check size={12} style={{ marginLeft: "auto", color: "var(--md-sys-color-primary)" }} />}
</div>
</button>
);
})}
</div>
)}
</div>
{serverRunning && (
<button
className="m3-btn m3-btn-error"
@@ -58,21 +156,18 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isS
{/* CPU Telemetry Chip */}
<div className="telemetry-chip" title="CPU Utilization">
<Cpu className="telemetry-chip-icon" style={{ color: "var(--md-sys-color-primary)" }} />
<span>CPU: {Number.isFinite(Number(telemetry.cpu_usage)) ? telemetry.cpu_usage : "--"}%</span>
</div>
{/* RAM Telemetry Chip */}
<div className="telemetry-chip" title="System Memory Usage">
<HardDrive className="telemetry-chip-icon" style={{ color: "var(--md-sys-color-secondary)" }} />
<span>RAM: {formatGb(telemetry.ram_used_gb)} / {formatGb(telemetry.ram_total_gb)} GB</span>
</div>
{/* GPU VRAM Telemetry Chip */}
{telemetry.vram_total_gb > 0 && (
<div className="telemetry-chip" title={`${telemetry.gpu_name} VRAM`}>
<Database className="telemetry-chip-icon" style={{ color: "var(--md-sys-color-tertiary)" }} />
<span>VRAM: {formatGb(telemetry.vram_used_gb)} / {formatGb(telemetry.vram_total_gb)} GB</span>
<span>VRAM: {formatGb(telemetry.vram_used_gb, { allowZero: true })} / {formatGb(telemetry.vram_total_gb)} GB</span>
</div>
)}
</div>
+605 -178
View File
@@ -9,7 +9,7 @@ export const isTauri = () => {
// Cached hardware specs
let cachedSpecs = null;
let cachedBackendPort = null;
export const EXPECTED_SERVER_BUILD = "polish-setup-v1";
export const EXPECTED_SERVER_BUILD = "text-image-v1";
const isLocalServerMode = () => {
return typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
@@ -48,8 +48,13 @@ 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 || "",
isProjector: Boolean(model?.isProjector),
};
}
@@ -75,7 +80,8 @@ async function readJsonResponse(res, fallbackMessage = "The local server returne
export async function getHealth() {
try {
const res = await fetch("/api/health");
const data = await readJsonResponse(res, "The local server returned an invalid health response.");
const data = await res.json();
if (!res.ok) throw new Error(data.error || `Health request failed (HTTP ${res.status})`);
return {
...data,
stale: data.build !== EXPECTED_SERVER_BUILD,
@@ -199,6 +205,15 @@ export async function getBackendOptions() {
};
}
export async function downloadBackend(backendId) {
const res = await fetch("/api/download-backend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ backend_id: backendId }),
});
return await readJsonResponse(res, "The local server returned an invalid backend download response.");
}
// Get list of model files on the USB
export async function listLocalModels() {
if (isTauri()) {
@@ -241,21 +256,29 @@ export async function startServer(modelPath, constraints) {
}
// Web/portable mode — call serve.cjs management API
const backendType = constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto");
const modelName = modelPath ? modelPath.split(/[\\/]/).pop() : null;
const isOpenVinoBackend = backendType === "openvino-npu";
const requestModel = isOpenVinoBackend && /\.(safetensors|ckpt|gguf)$/i.test(modelName || "")
? null
: modelName;
try {
const res = await fetch("/api/restart-backend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: modelName,
model: requestModel,
steps: constraints.steps || 20,
cfgScale: constraints.cfgScale || 7.0,
sampler: constraints.sampler || "euler_a",
threads: constraints.threads || 8,
use_gpu: constraints.useGpu !== false,
backend_type: constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"),
backend_type: backendType,
width: constraints.width || 512,
height: constraints.height || 512,
vae_tiling: constraints.vaeTiling !== false,
vae_on_cpu: constraints.vaeOnCpu === true,
flash_attn: constraints.useFlashAttn !== false,
}),
});
const text = await res.text();
@@ -298,6 +321,492 @@ export async function getBackendStatus() {
}
}
export async function getLlmStatus() {
try {
const res = await fetch("/api/llm/status");
return await readJsonResponse(res, "The local server returned invalid text backend status.");
} catch (err) {
return { ready: false, running: false, backendInstalled: false, error: err.message, settings: {} };
}
}
export async function getLlmBackends(refresh = false) {
const res = await fetch(`/api/llm/backends${refresh ? "?refresh=1" : ""}`);
return await readJsonResponse(res, "The local server returned invalid text backend data.");
}
export async function getLlmStats() {
const res = await fetch("/api/llm/stats");
return await readJsonResponse(res, "The local server returned invalid text runtime stats.");
}
export async function benchmarkLlm(model, options = {}) {
const res = await fetch("/api/llm/benchmark", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
backends: options.backends,
includeCpu: options.includeCpu,
prompt: options.prompt,
contextSize: options.contextSize,
gpuLayers: options.gpuLayers,
}),
});
return await readJsonResponse(res, "The local server returned invalid benchmark data.");
}
export async function listLlmModels() {
const res = await fetch("/api/llm/models");
const data = await readJsonResponse(res, "The local server returned invalid text model data.");
return (data.models || []).map(normalizeModel);
}
export async function searchHuggingFaceModels(query = "", filters = [], page = 1) {
const params = new URLSearchParams();
if (query.trim()) params.set("query", query.trim());
if (filters.length > 0) params.set("filters", filters.join(","));
params.set("page", String(page));
const res = await fetch(`/api/huggingface/models?${params.toString()}`);
const data = await readJsonResponse(res, "Hugging Face model search returned invalid data.");
return {
models: Array.isArray(data.models) ? data.models : [],
source: data.source || "huggingface",
warning: data.warning || "",
page: Number(data.page || page),
hasMore: Boolean(data.hasMore),
};
}
export async function startLlm(model, options = {}) {
const res = await fetch("/api/llm/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
threads: options.threads,
contextSize: options.contextSize,
gpuLayers: options.gpuLayers,
enableThinking: options.enableThinking,
flashAttn: options.flashAttn,
cacheTypeK: options.cacheTypeK,
cacheTypeV: options.cacheTypeV,
mlock: options.mlock,
mmap: options.mmap,
cachePrompt: options.cachePrompt,
defragThold: options.defragThold,
batchSize: options.batchSize,
ubatchSize: options.ubatchSize,
performanceProfile: options.performanceProfile,
preferredBackend: options.preferredBackend,
}),
});
return await readJsonResponse(res, "The local server returned an invalid text backend response.");
}
export async function stopLlm() {
const res = await fetch("/api/llm/stop", { method: "POST" });
return await readJsonResponse(res, "The local server returned an invalid text backend response.");
}
export async function chatWithLlm(messages, options = {}) {
const res = await fetch("/api/llm/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages,
temperature: options.temperature,
max_tokens: options.maxTokens,
// New sampling parameters
top_p: options.topP,
top_k: options.topK,
min_p: options.minP,
repeat_penalty: options.repeatPenalty,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
seed: options.seed,
stop: options.stop,
useWeb: options.useWeb === true,
timeFilter: options.timeFilter || "any",
}),
});
const data = await readJsonResponse(res, "The local text model returned an invalid response.");
const content = data?.choices?.[0]?.message?.content;
if (!content) throw new Error("The text model returned an empty response.");
return {
content,
usage: data?.usage || null
};
}
export async function streamChatWithLlm(messages, options = {}, onToken = () => {}) {
const fetchOpts = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages,
temperature: options.temperature,
max_tokens: options.maxTokens,
stream: true,
// New sampling parameters
top_p: options.topP,
top_k: options.topK,
min_p: options.minP,
repeat_penalty: options.repeatPenalty,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
seed: options.seed,
stop: options.stop,
useWeb: options.useWeb === true,
timeFilter: options.timeFilter || "any",
}),
};
if (options.signal) {
fetchOpts.signal = options.signal;
}
const res = await fetch("/api/llm/chat", fetchOpts);
if (!res.ok) {
const data = await readJsonResponse(res, `Chat request failed (HTTP ${res.status}).`);
throw new Error(data.error || `Chat request failed (HTTP ${res.status}).`);
}
if (!res.body) throw new Error("Streaming is not supported by this browser.");
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let content = "";
let reasoningContent = "";
let usage = null;
let timings = null;
let finishReason = null;
let webSources = [];
const normalizeTextDelta = (value) => {
if (typeof value === "string") return value;
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "string") return item;
if (typeof item?.text === "string") return item.text;
if (typeof item?.content === "string") return item.content;
return "";
}).join("");
}
if (value && typeof value === "object") {
if (typeof value.text === "string") return value.text;
if (typeof value.content === "string") return value.content;
}
return "";
};
const consumeEvent = (eventText) => {
try {
const data = eventText
.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n")
.trim();
if (!data || data === "[DONE]") return data === "[DONE]";
const eventName = eventText
.split(/\r?\n/)
.find((line) => line.startsWith("event:"))
?.slice(6)
.trim();
const parsed = JSON.parse(data);
if (eventName === "web_sources") {
webSources = parsed.sources || [];
return false;
}
const choice = parsed.choices?.[0];
const token = normalizeTextDelta(
choice?.delta?.content ??
choice?.message?.content ??
choice?.text ??
parsed.content ??
parsed.response
);
const reasoningToken = normalizeTextDelta(
choice?.delta?.reasoning_content ??
choice?.delta?.reasoning ??
choice?.delta?.thinking ??
choice?.message?.reasoning_content ??
parsed.reasoning_content
);
if (choice?.finish_reason) finishReason = choice.finish_reason;
if (token || reasoningToken) {
content += token;
reasoningContent += reasoningToken;
onToken(token, content, reasoningToken, reasoningContent);
}
if (parsed.usage) usage = parsed.usage;
if (parsed.timings) timings = parsed.timings;
return false;
} catch (err) {
console.warn("Failed to parse EventStream JSON:", eventText, err);
return false;
}
};
let finished = false;
while (!finished) {
const { done, value } = await reader.read();
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
const events = buffer.split(/\r?\n\r?\n/);
buffer = events.pop() || "";
for (const eventText of events) {
if (consumeEvent(eventText)) {
finished = true;
break;
}
}
if (done) break;
}
if (!finished && buffer.trim()) consumeEvent(buffer);
if (!content && !reasoningContent) throw new Error("The text model returned an empty streamed response.");
return { content, reasoningContent, usage, timings, finishReason, webSources };
}
export async function searchWeb(query, options = {}) {
const res = await fetch("/api/web-search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
timeFilter: options.timeFilter || "any",
resultLimit: options.resultLimit,
fetchLimit: options.fetchLimit,
}),
});
return await readJsonResponse(res, "The local server returned an invalid web search response.");
}
export async function downloadLlmModel(url, filename = null, companion = null) {
const res = await fetch("/api/llm/download-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url,
filename,
projectorUrl: companion?.url || "",
projectorFilename: companion?.filename || "",
}),
});
return await readJsonResponse(res, "The local server returned an invalid text download response.");
}
export async function importLlmModel(file, onProgress, signal) {
return await uploadModelFileToEndpoint(file, "/api/llm/import-model", onProgress, signal);
}
export async function deleteLlmModel(filename) {
const res = await fetch("/api/llm/delete-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
return await readJsonResponse(res, "The local server returned an invalid text model delete response.");
}
export async function getSpeechStatus() {
try {
const res = await fetch("/api/speech/status");
return await readJsonResponse(res, "The local server returned invalid speech backend status.");
} catch (err) {
return { ready: false, running: false, backendInstalled: false, error: err.message, settings: {} };
}
}
export async function listSpeechModels() {
const res = await fetch("/api/speech/models");
const data = await readJsonResponse(res, "The local server returned invalid speech model data.");
return data.models || [];
}
export async function startSpeech(model, options = {}) {
const res = await fetch("/api/speech/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
language: options.language || "auto",
threads: options.threads,
backendPreference: options.backendPreference || "auto",
}),
});
return await readJsonResponse(res, "The local server returned an invalid speech start response.");
}
export async function stopSpeech() {
const res = await fetch("/api/speech/stop", { method: "POST" });
return await readJsonResponse(res, "The local server returned an invalid speech stop response.");
}
export async function transcribeSpeech(fileOrBlob, options = {}) {
const params = new URLSearchParams();
if (options.model) params.set("model", options.model);
if (options.language) params.set("language", options.language);
if (options.filename) params.set("filename", options.filename);
if (options.threads) params.set("threads", String(options.threads));
if (options.backendPreference) params.set("backendPreference", options.backendPreference);
if (options.translate) params.set("translate", "true");
const res = await fetch(`/api/speech/transcribe?${params.toString()}`, {
method: "POST",
headers: { "Content-Type": "audio/wav" },
body: fileOrBlob,
signal: options.signal,
});
const data = await readJsonResponse(res, "The local server returned an invalid transcription response.");
return data.transcription;
}
export async function downloadSpeechModel(modelIdOrUrl, filename = null) {
const isUrl = /^https?:\/\//i.test(String(modelIdOrUrl || ""));
const res = await fetch("/api/speech/download-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: isUrl ? "" : modelIdOrUrl,
url: isUrl ? modelIdOrUrl : "",
filename,
}),
});
return await readJsonResponse(res, "The local server returned an invalid speech download response.");
}
export async function importSpeechModel(file, onProgress, signal) {
return await uploadModelFileToEndpoint(file, "/api/speech/import-model", onProgress, signal);
}
export async function deleteSpeechModel(filename) {
const res = await fetch("/api/speech/delete-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
return await readJsonResponse(res, "The local server returned an invalid speech model delete response.");
}
export async function listSpeechTranscriptions() {
const res = await fetch("/api/speech/transcriptions");
const data = await readJsonResponse(res, "The local server returned invalid transcription history.");
return data.transcriptions || [];
}
export async function deleteSpeechTranscription(filename) {
const res = await fetch("/api/speech/delete-transcription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
return await readJsonResponse(res, "The local server returned an invalid speech transcription delete response.");
}
export async function getTtsStatus() {
try {
const res = await fetch("/api/tts/status");
return await readJsonResponse(res, "The local server returned invalid TTS runtime status.");
} catch (err) {
return { ready: false, running: false, runtimeInstalled: false, error: err.message, settings: {}, voices: [] };
}
}
export async function listTtsModels() {
const res = await fetch("/api/tts/models");
const data = await readJsonResponse(res, "The local server returned invalid TTS model data.");
return data.models || [];
}
export async function startTts(model, options = {}) {
const res = await fetch("/api/tts/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
voice: options.voice,
speed: options.speed,
}),
});
return await readJsonResponse(res, "The local server returned an invalid TTS start response.");
}
export async function stopTts() {
const res = await fetch("/api/tts/stop", { method: "POST" });
return await readJsonResponse(res, "The local server returned an invalid TTS stop response.");
}
export async function speakTts(text, options = {}) {
const res = await fetch("/api/tts/speak", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text,
model: options.model,
voice: options.voice,
speed: options.speed,
}),
signal: options.signal,
});
const data = await readJsonResponse(res, "The local server returned an invalid TTS response.");
return data.output;
}
export async function downloadTtsModel(modelIdOrUrl, filename = null) {
const res = await fetch("/api/tts/download-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: modelIdOrUrl,
filename,
}),
});
return await readJsonResponse(res, "The local server returned an invalid TTS download response.");
}
export async function importTtsModel(file, onProgress, signal) {
return await uploadModelFileToEndpoint(file, "/api/tts/import-model", onProgress, signal);
}
export async function deleteTtsModel(filename) {
const res = await fetch("/api/tts/delete-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
return await readJsonResponse(res, "The local server returned an invalid TTS model delete response.");
}
export async function listTtsOutputs() {
const res = await fetch("/api/tts/outputs");
const data = await readJsonResponse(res, "The local server returned invalid TTS output history.");
return data.outputs || [];
}
export async function deleteTtsOutput(filename) {
const res = await fetch("/api/tts/delete-output", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
return await readJsonResponse(res, "The local server returned an invalid TTS output delete response.");
}
export async function getLlmRecommendations(useCase = "chat", limit = 10) {
try {
const res = await fetch(`/api/llm/recommend?useCase=${encodeURIComponent(useCase)}&limit=${limit}`);
const data = await res.json();
if (!res.ok || !data.ok) return null;
return data.recommendations;
} catch (_) {
return null;
}
}
export async function listGeneratedOutputs() {
try {
const res = await fetch("/api/outputs");
@@ -330,22 +839,41 @@ 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 or mocks them if server is unreachable
// 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.
export async function generateImage(prompt, negativePrompt, constraints, activeModelName, inputImageBase64, onProgress, signal) {
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,
@@ -360,173 +888,45 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
denoising_strength: constraints.denoisingStrength || 0.7,
};
// Mock generation helper for offline/browser execution
const runMockGeneration = async () => {
// Simulate generation steps progress
const stepsCount = payload.steps;
for (let i = 1; i <= stepsCount; i++) {
if (signal && signal.aborted) {
throw new DOMException("The user aborted a request.", "AbortError");
}
await new Promise(r => setTimeout(r, (3000 / stepsCount))); // 3 seconds total
if (onProgress) {
onProgress(Math.round((i / stepsCount) * 100));
}
if (constraints.backendType === "openvino-npu") {
if (inputImageBase64) {
throw new Error("OpenVINO NPU test mode currently supports text-to-image only.");
}
const pLower = prompt.toLowerCase();
const seedVal = Math.abs(payload.seed) || Math.floor(Math.random() * 1000);
// Generate a beautiful, local vector SVG representing the requested subject, completely offline-compatible
const primaryBg = "#0F172A"; // dark premium background
let gradientStart = "#3B82F6";
let gradientEnd = "#8B5CF6";
let sceneShapes = "";
let keywordLabel = "Abstract Generation";
if (pLower.includes("moon") || pLower.includes("space") || pLower.includes("astronomy") || pLower.includes("sky")) {
gradientStart = "#1E293B";
gradientEnd = "#0F172A";
keywordLabel = "Luminous Moon in Starry Sky";
sceneShapes = `
<!-- Stars -->
<circle cx="80" cy="100" r="1.5" fill="#ffffff" opacity="0.6"/>
<circle cx="160" cy="60" r="1" fill="#ffffff" opacity="0.5"/>
<circle cx="280" cy="120" r="2" fill="#ffffff" opacity="0.8"/>
<circle cx="360" cy="80" r="1.5" fill="#ffffff" opacity="0.4"/>
<circle cx="440" cy="150" r="1" fill="#ffffff" opacity="0.7"/>
<circle cx="400" cy="280" r="2" fill="#ffffff" opacity="0.9"/>
<circle cx="100" cy="300" r="1.5" fill="#ffffff" opacity="0.5"/>
<!-- Moon -->
<circle cx="256" cy="220" r="90" fill="url(#moonGrad)" />
<circle cx="226" cy="190" r="80" fill="#0F172A" />
`;
} else if (pLower.includes("cat") || pLower.includes("kitten") || pLower.includes("feline")) {
gradientStart = "#4F46E5";
gradientEnd = "#7C3AED";
keywordLabel = "Stylized Cat Portrait";
sceneShapes = `
<!-- Cat head & ears -->
<polygon points="176,280 146,160 216,210" fill="#312E81" />
<polygon points="336,280 366,160 296,210" fill="#312E81" />
<circle cx="256" cy="270" r="80" fill="#4338CA" />
<!-- Eyes -->
<ellipse cx="226" cy="260" rx="14" ry="8" fill="#10B981" />
<ellipse cx="286" cy="260" rx="14" ry="8" fill="#10B981" />
<circle cx="226" cy="260" r="4" fill="#000" />
<circle cx="286" cy="260" r="4" fill="#000" />
<!-- Nose & Whiskers -->
<polygon points="256,285 248,275 264,275" fill="#F43F5E" />
<line x1="226" y1="290" x2="166" y2="280" stroke="#E2E8F0" stroke-width="2" />
<line x1="226" y1="295" x2="156" y2="295" stroke="#E2E8F0" stroke-width="2" />
<line x1="286" y1="290" x2="346" y2="280" stroke="#E2E8F0" stroke-width="2" />
<line x1="286" y1="295" x2="356" y2="295" stroke="#E2E8F0" stroke-width="2" />
`;
} else if (pLower.includes("dog") || pLower.includes("puppy") || pLower.includes("canine")) {
gradientStart = "#D97706";
gradientEnd = "#B45309";
keywordLabel = "Friendly Dog Portrait";
sceneShapes = `
<!-- Dog head & floppy ears -->
<ellipse cx="166" cy="260" rx="20" ry="60" fill="#78350F" />
<ellipse cx="346" cy="260" rx="20" ry="60" fill="#78350F" />
<circle cx="256" cy="250" r="70" fill="#92400E" />
<ellipse cx="256" cy="280" rx="40" ry="30" fill="#D97706" />
<!-- Eyes -->
<circle cx="226" cy="230" r="10" fill="#1E293B" />
<circle cx="286" cy="230" r="10" fill="#1E293B" />
<circle cx="222" cy="226" r="3" fill="#ffffff" />
<circle cx="282" cy="226" r="3" fill="#ffffff" />
<!-- Nose -->
<ellipse cx="256" cy="275" rx="16" ry="10" fill="#1E293B" />
`;
} else if (pLower.includes("city") || pLower.includes("cyberpunk") || pLower.includes("tokyo") || pLower.includes("neon")) {
gradientStart = "#111827";
gradientEnd = "#311042";
keywordLabel = "Neon Cyberpunk Skyline";
sceneShapes = `
<!-- Skyline -->
<rect x="60" y="160" width="80" height="352" fill="#1F2937" opacity="0.9"/>
<rect x="180" y="100" width="100" height="412" fill="#111827" />
<rect x="320" y="200" width="120" height="312" fill="#1F2937" opacity="0.85"/>
<rect x="100" y="240" width="110" height="272" fill="#374151" opacity="0.6"/>
<!-- Neon details -->
<line x1="230" y1="100" x2="230" y2="40" stroke="#EC4899" stroke-width="3" />
<rect x="200" y="140" width="60" height="15" fill="#10B981" opacity="0.7"/>
<rect x="200" y="170" width="60" height="15" fill="#10B981" opacity="0.7"/>
<rect x="200" y="200" width="60" height="15" fill="#3B82F6" opacity="0.7"/>
<rect x="70" y="200" width="15" height="100" fill="#F59E0B" opacity="0.5"/>
<rect x="340" y="250" width="20" height="150" fill="#EF4444" opacity="0.6"/>
`;
} else if (pLower.includes("car") || pLower.includes("vehicle") || pLower.includes("supercar")) {
gradientStart = "#DC2626";
gradientEnd = "#991B1B";
keywordLabel = "Futuristic Concept Car";
sceneShapes = `
<!-- Road / Perspective grid -->
<line x1="256" y1="350" x2="0" y2="512" stroke="#4B5563" stroke-width="3" />
<line x1="256" y1="350" x2="512" y2="512" stroke="#4B5563" stroke-width="3" />
<!-- Car body -->
<polygon points="126,380 256,330 386,380 346,440 166,440" fill="#EF4444" />
<polygon points="176,370 256,340 336,370 326,390 186,390" fill="#1E293B" opacity="0.9" />
<!-- Headlights -->
<polygon points="136,410 176,415 166,425 136,420" fill="#FBBF24" />
<polygon points="376,410 336,415 346,425 376,420" fill="#FBBF24" />
<!-- Wheels -->
<ellipse cx="156" cy="440" rx="30" ry="15" fill="#111" />
<ellipse cx="356" cy="440" rx="30" ry="15" fill="#111" />
`;
} else {
// General gradient waves
gradientStart = "#06B6D4";
gradientEnd = "#0891B2";
keywordLabel = "Abstract Creative Concept";
sceneShapes = `
<path d="M0,256 C150,150 350,350 512,256 L512,512 L0,512 Z" fill="#0891B2" opacity="0.4" />
<path d="M0,350 C200,200 300,450 512,350 L512,512 L0,512 Z" fill="#0E7490" opacity="0.6" />
<circle cx="380" cy="180" r="50" fill="#F59E0B" opacity="0.8" />
`;
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 svgString = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${gradientStart}" />
<stop offset="100%" stop-color="${gradientEnd}" />
</linearGradient>
<linearGradient id="moonGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FEF08A" />
<stop offset="100%" stop-color="#FDE047" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="512" height="512" fill="url(#bgGrad)" />
<!-- Shapes -->
${sceneShapes}
<!-- Premium UI Overlay Frame -->
<rect x="20" y="20" width="472" height="472" fill="none" stroke="#ffffff" stroke-width="1.5" opacity="0.15" rx="8"/>
<!-- Prompt & Details text -->
<rect x="40" y="410" width="432" height="62" fill="#0F172A" opacity="0.85" rx="6" />
<text x="60" y="435" fill="#F1F5F9" font-family="system-ui, -apple-system, sans-serif" font-size="13" font-weight="bold">${keywordLabel}</text>
<text x="60" y="455" fill="#94A3B8" font-family="system-ui, -apple-system, sans-serif" font-size="11">Seed: ${seedVal} • Steps: ${payload.steps} • Scale: ${payload.cfg_scale}</text>
</svg>
`;
const base64Svg = btoa(unescape(encodeURIComponent(svgString)));
const mockImageUrl = `data:image/svg+xml;base64,${base64Svg}`;
const durationSec = parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
const durationSec = Number(data.duration_sec) || parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
return {
image: mockImageUrl,
seed: payload.seed,
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;
@@ -582,9 +982,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,
};
@@ -601,11 +1009,12 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
}
} catch (err) {
if (err.name === "AbortError" || err.message.startsWith("Generation failed")) throw err;
console.warn("Could not reach local server. Falling back to simulation mode.", err);
console.warn("Could not reach local server.", err);
throw 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."
);
}
// Fallback to mock generation if server is offline/not reachable
return await runMockGeneration();
}
// Perform model file import (copy to USB in Tauri, simulated in Web mode)
@@ -680,12 +1089,16 @@ export async function importModelFile(sourcePath, onProgress, signal) {
}
function uploadModelFile(file, onProgress, signal) {
return uploadModelFileToEndpoint(file, "/api/import-model", onProgress, signal);
}
function uploadModelFileToEndpoint(file, endpoint, onProgress, signal) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const startedAt = Date.now();
let abortedByUser = false;
xhr.open("POST", `/api/import-model?filename=${encodeURIComponent(file.name)}`);
xhr.open("POST", `${endpoint}?filename=${encodeURIComponent(file.name)}`);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
const abortUpload = () => {
@@ -819,6 +1232,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" });
+10
View File
@@ -0,0 +1,10 @@
export const THEMES = [
{ id: "light", name: "Light Default", type: "light", primary: "#4f46e5", secondary: "#0ea5e9", bg: "#f8fafc" },
{ id: "light-nord", name: "Nord Light", type: "light", primary: "#5e81ac", secondary: "#88c0d0", bg: "#eceff4" },
{ id: "light-sakura", name: "Sakura Blossom", type: "light", primary: "#d05a74", secondary: "#e597a7", bg: "#fdf6f6" },
{ id: "dark", name: "Dark Default", type: "dark", primary: "#6366f1", secondary: "#38bdf8", bg: "#090a0f" },
{ id: "dark-dracula", name: "Dracula Classic", type: "dark", primary: "#bd93f9", secondary: "#ff79c6", bg: "#282a36" },
{ id: "dark-cyberpunk", name: "Cyberpunk Neon", type: "dark", primary: "#ff007f", secondary: "#00f0ff", bg: "#0d0714" },
{ id: "dark-forest", name: "Emerald Forest", type: "dark", primary: "#10b981", secondary: "#34d399", bg: "#060f0e" },
{ id: "dark-oled", name: "OLED Pitch Black", type: "dark", primary: "#3b82f6", secondary: "#60a5fa", bg: "#000000" }
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

Executable
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env bash
#
# Uncensored AI Studio - Linux Launcher
# Double-click or run: ./linux.sh
# Use --max-perf to enable ROCm backend downloads on Linux first setup.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$SCRIPT_DIR/app"
PLATFORM="$(uname -s)"
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/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/server/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
LLM_PORT="${LLM_PORT:-10086}"
SETUP_REASON=""
SETUP_MODE="Repair"
MAX_PERF_FLAG=""
SETUP_OPENVINO=0
# Parse args
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/setup-openvino-npu.sh"
fi
# ── Setup node_modules to avoid OS conflicts ────────────────────────────────
FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules"
LINUX_NODE_MODULES="$APP_DIR/frontend/node_modules_linux"
ACTIVE_OS_FILE="$APP_DIR/frontend/.active_modules_os"
# Attempt to create a test symlink to check if filesystem supports symlinks
USE_SYMLINKS=true
TEST_LINK="$APP_DIR/frontend/.test_symlink"
rm -f "$TEST_LINK"
if ln -s "node_modules_linux" "$TEST_LINK" 2>/dev/null; then
rm -f "$TEST_LINK"
else
USE_SYMLINKS=false
fi
if [ "$USE_SYMLINKS" = true ]; then
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Migrating existing node_modules to node_modules_linux..."
rm -rf "$LINUX_NODE_MODULES"
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"
else
# Fallback: Filesystem does not support symlinks (e.g. FAT32/exFAT)
echo " >> Filesystem does not support symlinks. Using directory swapping fallback..."
if [[ -L "$FRONTEND_NODE_MODULES" || -f "$FRONTEND_NODE_MODULES" ]]; then
rm -f "$FRONTEND_NODE_MODULES"
fi
PREV_OS=""
if [[ -f "$ACTIVE_OS_FILE" ]]; then
PREV_OS=$(cat "$ACTIVE_OS_FILE")
fi
if [[ -d "$FRONTEND_NODE_MODULES" && "$PREV_OS" != "linux" ]]; then
if [[ -n "$PREV_OS" ]]; then
echo " >> Swapping out node_modules to node_modules_$PREV_OS..."
rm -rf "$APP_DIR/frontend/node_modules_$PREV_OS"
mv "$FRONTEND_NODE_MODULES" "$APP_DIR/frontend/node_modules_$PREV_OS"
else
echo " >> Saving node_modules as node_modules_windows..."
rm -rf "$APP_DIR/frontend/node_modules_windows"
mv "$FRONTEND_NODE_MODULES" "$APP_DIR/frontend/node_modules_windows"
fi
fi
if [[ -d "$LINUX_NODE_MODULES" && ! -d "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Swapping in node_modules_linux..."
mv "$LINUX_NODE_MODULES" "$FRONTEND_NODE_MODULES"
elif [[ ! -d "$FRONTEND_NODE_MODULES" ]]; then
mkdir -p "$FRONTEND_NODE_MODULES"
fi
echo "linux" > "$ACTIVE_OS_FILE"
fi
# ── 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 Linux is missing."
fi
if [[ ! -f "$DIST_INDEX" ]]; then
SETUP_REASON="Frontend build is missing."
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"
LLM_CUDA_PATH="$APP_DIR/llm-backend/linux/cuda/llama-server"
LLM_ROCM_PATH="$APP_DIR/llm-backend/linux/rocm/llama-server"
LLM_SYCL_PATH="$APP_DIR/llm-backend/linux/sycl/llama-server"
LLM_VULKAN_PATH="$APP_DIR/llm-backend/linux/vulkan/llama-server"
LLM_CPU_PATH="$APP_DIR/llm-backend/linux/cpu/llama-server"
SPEECH_BACKEND_PATH="$APP_DIR/speech-backend/linux/cpu/whisper-cli"
TTS_RUNTIME_PATH="$APP_DIR/tts-runtime/node_modules/kokoro-js"
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 [[ ! -x "$LLM_CUDA_PATH" && ! -x "$LLM_ROCM_PATH" && ! -x "$LLM_SYCL_PATH" && ! -x "$LLM_VULKAN_PATH" && ! -x "$LLM_CPU_PATH" ]]; then
SETUP_REASON="Linux llama.cpp text backend is missing or not executable."
fi
if [[ ! -x "$SPEECH_BACKEND_PATH" ]]; then
SETUP_REASON="Linux whisper.cpp speech backend is missing or not executable."
fi
if [[ ! -d "$TTS_RUNTIME_PATH" ]]; then
SETUP_REASON="Kokoro text-to-speech runtime is missing."
fi
if [[ -n "$SETUP_REASON" ]]; then
echo ""
echo " ============================================================"
echo " UNCENSORED AI STUDIO | $PLATFORM_LABEL $SETUP_MODE"
echo " ============================================================"
echo ""
if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then
echo " This looks like your first run on Linux. Setting up automatically..."
else
echo " Uncensored AI Studio 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 and backend server processes
if command -v lsof >/dev/null 2>&1; then
lsof -t -i:"${FRONTEND_PORT}" -i:8080 -i:"${LLM_PORT}" | xargs kill -9 >/dev/null 2>&1 || true
elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fuser -k "8080/tcp" >/dev/null 2>&1 || true
fuser -k "${LLM_PORT}/tcp" >/dev/null 2>&1 || true
fi
if ! bash "$SETUP_SCRIPT" $MAX_PERF_FLAG; 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 " UNCENSORED AI STUDIO | Launching..."
echo " ============================================================"
echo ""
# Clear frontend and backend ports
if command -v lsof >/dev/null 2>&1; then
lsof -t -i:"${FRONTEND_PORT}" -i:8080 -i:"${LLM_PORT}" | xargs kill -9 >/dev/null 2>&1 || true
elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fuser -k "8080/tcp" >/dev/null 2>&1 || true
fuser -k "${LLM_PORT}/tcp" >/dev/null 2>&1 || true
fi
# Start the server
echo " Starting Uncensored AI Studio..."
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 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
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 " Text API: Starts when a GGUF model is loaded (port ${LLM_PORT})"
echo " Speech: Managed locally by the app"
echo " TTS: Managed locally by the app"
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
Executable
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env bash
#
# Uncensored AI Studio - 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"
if [[ "$(uname -m)" == "arm64" ]]; then
LLM_BACKEND_PATH="$APP_DIR/llm-backend/mac/arm64/llama-server"
else
LLM_BACKEND_PATH="$APP_DIR/llm-backend/mac/x64/llama-server"
fi
TTS_RUNTIME_PATH="$APP_DIR/tts-runtime/node_modules/kokoro-js"
SPEECH_BACKEND_PATH="$APP_DIR/speech-backend/mac/cpu/whisper-cli"
PLATFORM_LABEL="macOS"
DIST_INDEX="$APP_DIR/dist/index.html"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/server/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
LLM_PORT="${LLM_PORT:-10086}"
SETUP_REASON=""
SETUP_MODE="Repair"
# ── Setup node_modules to avoid OS conflicts ────────────────────────────────
FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules"
MAC_NODE_MODULES="$APP_DIR/frontend/node_modules_mac"
ACTIVE_OS_FILE="$APP_DIR/frontend/.active_modules_os"
# Attempt to create a test symlink to check if filesystem supports symlinks
USE_SYMLINKS=true
TEST_LINK="$APP_DIR/frontend/.test_symlink"
rm -f "$TEST_LINK"
if ln -s "node_modules_mac" "$TEST_LINK" 2>/dev/null; then
rm -f "$TEST_LINK"
else
USE_SYMLINKS=false
fi
if [ "$USE_SYMLINKS" = true ]; then
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Migrating existing node_modules to node_modules_mac..."
rm -rf "$MAC_NODE_MODULES"
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"
else
# Fallback: Filesystem does not support symlinks (e.g. FAT32/exFAT)
echo " >> Filesystem does not support symlinks. Using directory swapping fallback..."
if [[ -L "$FRONTEND_NODE_MODULES" || -f "$FRONTEND_NODE_MODULES" ]]; then
rm -f "$FRONTEND_NODE_MODULES"
fi
PREV_OS=""
if [[ -f "$ACTIVE_OS_FILE" ]]; then
PREV_OS=$(cat "$ACTIVE_OS_FILE")
fi
if [[ -d "$FRONTEND_NODE_MODULES" && "$PREV_OS" != "mac" ]]; then
if [[ -n "$PREV_OS" ]]; then
echo " >> Swapping out node_modules to node_modules_$PREV_OS..."
rm -rf "$APP_DIR/frontend/node_modules_$PREV_OS"
mv "$FRONTEND_NODE_MODULES" "$APP_DIR/frontend/node_modules_$PREV_OS"
else
echo " >> Saving node_modules as node_modules_windows..."
rm -rf "$APP_DIR/frontend/node_modules_windows"
mv "$FRONTEND_NODE_MODULES" "$APP_DIR/frontend/node_modules_windows"
fi
fi
if [[ -d "$MAC_NODE_MODULES" && ! -d "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Swapping in node_modules_mac..."
mv "$MAC_NODE_MODULES" "$FRONTEND_NODE_MODULES"
elif [[ ! -d "$FRONTEND_NODE_MODULES" ]]; then
mkdir -p "$FRONTEND_NODE_MODULES"
fi
echo "mac" > "$ACTIVE_OS_FILE"
fi
# ── 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 [[ ! -x "$LLM_BACKEND_PATH" ]]; then
SETUP_REASON="No macOS llama.cpp text backend is installed."
fi
if [[ ! -d "$TTS_RUNTIME_PATH" ]]; then
SETUP_REASON="Kokoro text-to-speech runtime is missing."
fi
if [[ ! -x "$SPEECH_BACKEND_PATH" ]]; then
SETUP_REASON="macOS whisper.cpp speech backend is missing."
fi
if [[ -n "$SETUP_REASON" ]]; then
echo ""
echo " ============================================================"
echo " UNCENSORED AI STUDIO | $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 " Uncensored AI Studio 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 and backend server processes
if command -v lsof >/dev/null 2>&1; then
lsof -t -i:"${FRONTEND_PORT}" -i:8080 -i:"${LLM_PORT}" | xargs kill -9 >/dev/null 2>&1 || true
elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fuser -k "8080/tcp" >/dev/null 2>&1 || true
fuser -k "${LLM_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 " UNCENSORED AI STUDIO | Launching..."
echo " ============================================================"
echo ""
# Clear frontend and backend ports
if command -v lsof >/dev/null 2>&1; then
lsof -t -i:"${FRONTEND_PORT}" -i:8080 -i:"${LLM_PORT}" | xargs kill -9 >/dev/null 2>&1 || true
elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fuser -k "8080/tcp" >/dev/null 2>&1 || true
fuser -k "${LLM_PORT}/tcp" >/dev/null 2>&1 || true
fi
# Start the server
echo " Starting Uncensored AI Studio..."
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 " Text API: Starts when a GGUF model is loaded (port ${LLM_PORT})"
echo " Speech: Managed locally by the app"
echo " TTS: Managed locally by the app"
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
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
#
# Uncensored AI Studio - Compile backends from source
# For Linux systems with GLIBC < 2.38 or macOS systems that need a local Metal build.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
PLATFORM="$(uname -s)"
if [ "$PLATFORM" = "Darwin" ]; then
BACKEND_DIR="$APP_DIR/backend/mac"
else
BACKEND_DIR="$APP_DIR/backend/linux"
fi
BUILD_DIR="$APP_DIR/tools/build-sd"
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
# Pinned release tag
PINNED_TAG="master-685-19bdfe2"
echo "=== Uncensored AI Studio - Build from Source ==="
echo "Target directories:"
echo " CPU Backend -> $BACKEND_DIR/cpu"
echo " Vulkan Backend -> $BACKEND_DIR/vulkan"
echo ""
# 1. Clone stable-diffusion.cpp
if [ ! -d "$BUILD_DIR" ]; then
echo "Cloning stable-diffusion.cpp..."
git clone https://github.com/leejet/stable-diffusion.cpp.git "$BUILD_DIR"
else
echo "Using existing stable-diffusion.cpp directory."
fi
cd "$BUILD_DIR"
echo "Checking out pinned tag $PINNED_TAG..."
git fetch origin
git checkout -f "$PINNED_TAG"
git submodule update --init --recursive
if [ "$PLATFORM" = "Darwin" ]; then
echo ""
echo "=== Building macOS Metal Backend ==="
rm -rf build-metal && mkdir build-metal && cd build-metal
cmake .. -DSD_METAL=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release -j"$JOBS"
echo "Copying macOS Metal backend..."
mkdir -p "$BACKEND_DIR"
if [ -f bin/sd-server ]; then
cp bin/sd-server "$BACKEND_DIR/sd"
elif [ -f bin/sd ]; then
cp bin/sd "$BACKEND_DIR/sd"
else
echo "ERROR: bin/sd-server or bin/sd was not found!"
exit 1
fi
find . -name "*.dylib" -exec cp {} "$BACKEND_DIR/" \; 2>/dev/null || true
chmod +x "$BACKEND_DIR/sd"
echo ""
echo "=== macOS Metal build completed successfully! ==="
exit 0
fi
# 2. Build CPU Backend
echo ""
echo "=== Building CPU Backend ==="
rm -rf build-cpu && mkdir build-cpu && cd build-cpu
cmake .. -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release -j"$JOBS"
echo "Copying CPU binaries..."
mkdir -p "$BACKEND_DIR/cpu"
if [ -f bin/sd-server ]; then
cp bin/sd-server "$BACKEND_DIR/cpu/sd-cpu"
cp bin/sd-server "$BACKEND_DIR/cpu/sd-server-cpu"
else
echo "ERROR: bin/sd-server was not found!"
exit 1
fi
if [ -f bin/sd ]; then
cp bin/sd "$BACKEND_DIR/cpu/sd-cli-cpu"
elif [ -f bin/sd-cli ]; then
cp bin/sd-cli "$BACKEND_DIR/cpu/sd-cli-cpu"
fi
# Find and copy libstable-diffusion.so (might be in bin/ or in build root)
SO_PATH=$(find . -name "libstable-diffusion.so" | head -n 1)
if [ -n "$SO_PATH" ]; then
cp "$SO_PATH" "$BACKEND_DIR/cpu/"
echo "Copied libstable-diffusion.so to CPU directory."
chmod +x "$BACKEND_DIR/cpu/sd-cpu" "$BACKEND_DIR/cpu/sd-server-cpu" 2>/dev/null || true
if [ -f "$BACKEND_DIR/cpu/sd-cli-cpu" ]; then
chmod +x "$BACKEND_DIR/cpu/sd-cli-cpu" 2>/dev/null || true
fi
else
echo "WARNING: libstable-diffusion.so not found for CPU build!"
fi
# 3. Build Vulkan Backend
cd "$BUILD_DIR"
echo ""
echo "=== Building Vulkan Backend ==="
if rm -rf build-vulkan && mkdir build-vulkan && cd build-vulkan && \
cmake .. -DSD_VULKAN=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release && \
cmake --build . --config Release -j"$JOBS"; then
echo "Copying Vulkan binaries..."
mkdir -p "$BACKEND_DIR/vulkan"
if [ -f bin/sd-server ]; then
cp bin/sd-server "$BACKEND_DIR/vulkan/sd-vulkan"
cp bin/sd-server "$BACKEND_DIR/vulkan/sd-server-vulkan"
else
echo "ERROR: bin/sd-server was not found!"
exit 1
fi
if [ -f bin/sd ]; then
cp bin/sd "$BACKEND_DIR/vulkan/sd-cli-vulkan"
elif [ -f bin/sd-cli ]; then
cp bin/sd-cli "$BACKEND_DIR/vulkan/sd-cli-vulkan"
fi
SO_PATH_VK=$(find . -name "libstable-diffusion.so" | head -n 1)
if [ -n "$SO_PATH_VK" ]; then
cp "$SO_PATH_VK" "$BACKEND_DIR/vulkan/"
echo "Copied libstable-diffusion.so to Vulkan directory."
else
echo "WARNING: libstable-diffusion.so not found for Vulkan build!"
fi
chmod +x "$BACKEND_DIR/vulkan/sd-vulkan" "$BACKEND_DIR/vulkan/sd-server-vulkan" 2>/dev/null || true
if [ -f "$BACKEND_DIR/vulkan/sd-cli-vulkan" ]; then
chmod +x "$BACKEND_DIR/vulkan/sd-cli-vulkan" 2>/dev/null || true
fi
else
echo "WARNING: Vulkan backend build failed. Bypassing Vulkan backend compilation..."
fi
cd "$BUILD_DIR"
echo ""
echo "=== Building CUDA Backend ==="
rm -rf build-cuda && mkdir build-cuda && cd build-cuda
cmake .. -DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA_FORCE_MMQ=ON
cmake --build . --config Release -j"$JOBS"
echo "Copying CUDA binaries..."
mkdir -p "$BACKEND_DIR/cuda"
if [ -f bin/sd-server ]; then
cp bin/sd-server "$BACKEND_DIR/cuda/sd-cuda"
cp bin/sd-server "$BACKEND_DIR/cuda/sd-server-cuda"
else
echo "ERROR: bin/sd-server was not found!"
exit 1
fi
if [ -f bin/sd ]; then
cp bin/sd "$BACKEND_DIR/cuda/sd-cli-cuda"
elif [ -f bin/sd-cli ]; then
cp bin/sd-cli "$BACKEND_DIR/cuda/sd-cli-cuda"
fi
SO_PATH_CUDA=$(find . -name "libstable-diffusion.so" | head -n 1)
if [ -n "$SO_PATH_CUDA" ]; then
cp "$SO_PATH_CUDA" "$BACKEND_DIR/cuda/"
echo "Copied libstable-diffusion.so to CUDA directory."
chmod +x "$BACKEND_DIR/cuda/sd-cuda" "$BACKEND_DIR/cuda/sd-server-cuda" 2>/dev/null || true
if [ -f "$BACKEND_DIR/cuda/sd-cli-cuda" ]; then
chmod +x "$BACKEND_DIR/cuda/sd-cli-cuda" 2>/dev/null || true
fi
else
echo "WARNING: libstable-diffusion.so not found for CUDA build!"
fi
echo ""
echo "=== Build from source completed successfully! ==="
+26
View File
@@ -0,0 +1,26 @@
{
"llamaRelease": "b9668",
"notes": "Portable llama.cpp backend manifest. Setup scripts may override the release with LLAMA_RELEASE or a script parameter.",
"windows": {
"cuda": "llama-{release}-bin-win-cuda-12.4-x64.zip",
"vulkan": "llama-{release}-bin-win-vulkan-x64.zip",
"sycl": "llama-{release}-bin-win-sycl-x64.zip",
"hip": "llama-{release}-bin-win-hip-x64.zip",
"cpu": "llama-{release}-bin-win-cpu-x64.zip",
"cudaRuntime": "cudart-llama-bin-win-cuda-12.4-x64.zip"
},
"linux": {
"cuda": "llama-{release}-bin-ubuntu-cuda-12.4-x64.tar.gz",
"rocm": "llama-{release}-bin-ubuntu-rocm-x64.tar.gz",
"vulkanX64": "llama-{release}-bin-ubuntu-vulkan-x64.tar.gz",
"vulkanArm64": "llama-{release}-bin-ubuntu-vulkan-arm64.tar.gz",
"syclFp32": "llama-{release}-bin-ubuntu-sycl-fp32-x64.tar.gz",
"syclFp16": "llama-{release}-bin-ubuntu-sycl-fp16-x64.tar.gz",
"cpuX64": "llama-{release}-bin-ubuntu-x64.tar.gz",
"cpuArm64": "llama-{release}-bin-ubuntu-arm64.tar.gz"
},
"mac": {
"arm64": "llama-{release}-bin-macos-arm64.tar.gz",
"x64": "llama-{release}-bin-macos-x64.tar.gz"
}
}
-60
View File
@@ -1,60 +0,0 @@
# scripts/reset.ps1
# Resets portable app dependencies/builds while preserving user models and outputs.
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$appDir = Join-Path $rootDir "app"
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host " Resetting Local-AI-Image-Generator..." -ForegroundColor Yellow
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host ""
# Delete tools/node
$toolsDir = Join-Path $appDir "tools"
if (Test-Path $toolsDir) {
Write-Host " >> Removing portable tools/ node folder..." -ForegroundColor Cyan
Remove-Item $toolsDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete backend
$backendDir = Join-Path $appDir "backend"
if (Test-Path $backendDir) {
Write-Host " >> Removing backend binaries..." -ForegroundColor Cyan
Remove-Item $backendDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete dist
$distDir = Join-Path $appDir "dist"
if (Test-Path $distDir) {
Write-Host " >> Removing dist/ build folder..." -ForegroundColor Cyan
Remove-Item $distDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Preserve models
$modelsDir = Join-Path $appDir "models"
if (Test-Path $modelsDir) {
Write-Host " >> Preserving downloaded models in app/models." -ForegroundColor Cyan
}
# Delete node_modules in frontend
$nodeModulesDir = Join-Path $appDir "frontend\node_modules"
if (Test-Path $nodeModulesDir) {
Write-Host " >> Removing frontend node_modules..." -ForegroundColor Cyan
Remove-Item $nodeModulesDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete package-lock.json in frontend
$lockFile = Join-Path $appDir "frontend\package-lock.json"
if (Test-Path $lockFile) {
Write-Host " >> Removing frontend package-lock.json..." -ForegroundColor Cyan
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
}
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Reset complete. Models and generated outputs were preserved." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+131
View File
@@ -0,0 +1,131 @@
# scripts/reset/reset.ps1
# Resets portable app dependencies/builds while preserving user models and outputs.
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host " Resetting Local AI Studio..." -ForegroundColor Yellow
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host ""
# Delete portable tools/runtime folder
$toolsDir = Join-Path $appDir "tools"
if (Test-Path $toolsDir) {
Write-Host " >> Removing portable tools/runtime folder..." -ForegroundColor Cyan
Remove-Item $toolsDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete backend
$backendDir = Join-Path $appDir "backend"
if (Test-Path $backendDir) {
Write-Host " >> Removing image backend binaries..." -ForegroundColor Cyan
Remove-Item $backendDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " Preserving source workers in scripts/workers." -ForegroundColor DarkGray
}
# Delete llama.cpp backend
$llmBackendDir = Join-Path $appDir "llm-backend"
if (Test-Path $llmBackendDir) {
Write-Host " >> Removing llama.cpp text backend binaries..." -ForegroundColor Cyan
Remove-Item $llmBackendDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete whisper.cpp backend
$speechBackendDir = Join-Path $appDir "speech-backend"
if (Test-Path $speechBackendDir) {
Write-Host " >> Removing whisper.cpp speech backend binaries..." -ForegroundColor Cyan
Remove-Item $speechBackendDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete Kokoro TTS runtime dependencies
$ttsRuntimeDir = Join-Path $appDir "tts-runtime"
if (Test-Path $ttsRuntimeDir) {
Write-Host " >> Removing Kokoro TTS runtime dependencies..." -ForegroundColor Cyan
Remove-Item $ttsRuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete dist
$distDir = Join-Path $appDir "dist"
if (Test-Path $distDir) {
Write-Host " >> Removing dist/ build folder..." -ForegroundColor Cyan
Remove-Item $distDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Preserve image models
$modelsDir = Join-Path $appDir "models"
if (Test-Path $modelsDir) {
Write-Host " >> Preserving image models in app/models." -ForegroundColor Cyan
}
# Preserve text models
$llmModelsDir = Join-Path $appDir "llm-models"
if (Test-Path $llmModelsDir) {
Write-Host " >> Preserving text models in app/llm-models." -ForegroundColor Cyan
}
# Preserve speech models
$speechModelsDir = Join-Path $appDir "speech-models"
if (Test-Path $speechModelsDir) {
Write-Host " >> Preserving speech models in app/speech-models." -ForegroundColor Cyan
}
# Preserve TTS models
$ttsModelsDir = Join-Path $appDir "tts-models"
if (Test-Path $ttsModelsDir) {
Write-Host " >> Preserving TTS models in app/tts-models." -ForegroundColor Cyan
}
# Preserve transcriptions
$transcriptionsDir = Join-Path $appDir "transcriptions"
if (Test-Path $transcriptionsDir) {
Write-Host " >> Preserving speech transcripts in app/transcriptions." -ForegroundColor Cyan
}
# Preserve TTS outputs/cache
$ttsOutputsDir = Join-Path $appDir "tts-outputs"
if (Test-Path $ttsOutputsDir) {
Write-Host " >> Preserving TTS outputs in app/tts-outputs." -ForegroundColor Cyan
}
$ttsCacheDir = Join-Path $appDir "tts-cache"
if (Test-Path $ttsCacheDir) {
Write-Host " >> Preserving TTS model cache in app/tts-cache." -ForegroundColor Cyan
}
# Preserve OpenVINO models
$openVinoModelsDir = Join-Path $appDir "openvino-models"
if (Test-Path $openVinoModelsDir) {
Write-Host " >> Preserving OpenVINO models in app/openvino-models." -ForegroundColor Cyan
}
# Delete all frontend dependency folders, including platform-specific copies
$frontendDir = Join-Path $appDir "frontend"
Get-ChildItem $frontendDir -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq "node_modules" -or $_.Name -like "node_modules_*" } |
ForEach-Object {
Write-Host " >> Removing frontend $($_.Name)..." -ForegroundColor Cyan
Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
$activeOsFile = Join-Path $frontendDir ".active_modules_os"
if (Test-Path $activeOsFile) {
Write-Host " >> Removing frontend platform marker..." -ForegroundColor Cyan
Remove-Item $activeOsFile -Force -ErrorAction SilentlyContinue
}
# Delete package-lock.json in frontend
$lockFile = Join-Path $appDir "frontend\package-lock.json"
if (Test-Path $lockFile) {
Write-Host " >> Removing frontend package-lock.json..." -ForegroundColor Cyan
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
}
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
#
# Local AI Studio - Linux/macOS Reset Script
# Resets portable app dependencies/builds while preserving user models and outputs.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
echo ""
echo " ============================================================"
echo " Resetting Local AI Studio..."
echo " ============================================================"
echo ""
# Delete portable tools/runtime folder
if [[ -d "$APP_DIR/tools" ]]; then
echo " >> Removing portable tools/runtime folder..."
rm -rf "$APP_DIR/tools"
fi
# Delete backend
if [[ -d "$APP_DIR/backend" ]]; then
echo " >> Removing image backend binaries..."
rm -rf "$APP_DIR/backend"
fi
# Delete llama.cpp backend
if [[ -d "$APP_DIR/llm-backend" ]]; then
echo " >> Removing llama.cpp text backend binaries..."
rm -rf "$APP_DIR/llm-backend"
fi
# Delete whisper.cpp backend
if [[ -d "$APP_DIR/speech-backend" ]]; then
echo " >> Removing whisper.cpp speech backend binaries..."
rm -rf "$APP_DIR/speech-backend"
fi
# Delete Kokoro TTS runtime dependencies
if [[ -d "$APP_DIR/tts-runtime" ]]; then
echo " >> Removing Kokoro TTS runtime dependencies..."
rm -rf "$APP_DIR/tts-runtime"
fi
# Delete dist
if [[ -d "$APP_DIR/dist" ]]; then
echo " >> Removing dist/ build folder..."
rm -rf "$APP_DIR/dist"
fi
# Preserve image models
if [[ -d "$APP_DIR/models" ]]; then
echo " >> Preserving image models in app/models."
fi
# Preserve text models
if [[ -d "$APP_DIR/llm-models" ]]; then
echo " >> Preserving text models in app/llm-models."
fi
# Preserve speech models
if [[ -d "$APP_DIR/speech-models" ]]; then
echo " >> Preserving speech models in app/speech-models."
fi
# Preserve TTS models
if [[ -d "$APP_DIR/tts-models" ]]; then
echo " >> Preserving TTS models in app/tts-models."
fi
# Preserve transcriptions
if [[ -d "$APP_DIR/transcriptions" ]]; then
echo " >> Preserving speech transcripts in app/transcriptions."
fi
# Preserve TTS outputs/cache
if [[ -d "$APP_DIR/tts-outputs" ]]; then
echo " >> Preserving TTS outputs in app/tts-outputs."
fi
if [[ -d "$APP_DIR/tts-cache" ]]; then
echo " >> Preserving TTS model cache in app/tts-cache."
fi
# Preserve OpenVINO models
if [[ -d "$APP_DIR/openvino-models" ]]; then
echo " >> Preserving OpenVINO models in app/openvino-models."
fi
# Delete all frontend dependency folders, including platform-specific copies
for modules_dir in "$APP_DIR/frontend/node_modules" "$APP_DIR/frontend"/node_modules_*; do
if [[ -L "$modules_dir" || -d "$modules_dir" ]]; then
echo " >> Removing frontend $(basename "$modules_dir")..."
rm -rf "$modules_dir"
fi
done
if [[ -f "$APP_DIR/frontend/.active_modules_os" ]]; then
echo " >> Removing frontend platform marker..."
rm -f "$APP_DIR/frontend/.active_modules_os"
fi
# Delete package-lock.json in frontend
if [[ -f "$APP_DIR/frontend/package-lock.json" ]]; then
echo " >> Removing frontend package-lock.json..."
rm -f "$APP_DIR/frontend/package-lock.json"
fi
echo ""
echo " ============================================================"
echo " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved."
echo " ============================================================"
echo ""
read -rp " Press Enter to close..."
+37
View File
@@ -0,0 +1,37 @@
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const DEFAULT_TTL_MS = 30 * 60 * 1000;
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function keyFor(value) {
return crypto.createHash("sha256").update(String(value || "")).digest("hex");
}
function readCache(cacheDir, key, ttlMs = DEFAULT_TTL_MS) {
try {
const file = path.join(cacheDir, `${keyFor(key)}.json`);
const stat = fs.statSync(file);
if (Date.now() - stat.mtimeMs > ttlMs) return null;
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch (_) {
return null;
}
}
function writeCache(cacheDir, key, value) {
try {
ensureDir(cacheDir);
const file = path.join(cacheDir, `${keyFor(key)}.json`);
fs.writeFileSync(file, JSON.stringify(value, null, 2), "utf8");
} catch (_) {}
}
module.exports = {
readCache,
writeCache,
};
+159
View File
@@ -0,0 +1,159 @@
const http = require("http");
const https = require("https");
const dns = require("dns").promises;
const net = require("net");
const zlib = require("zlib");
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) LocalAIStudio/1.0";
function decodeHtml(value) {
return String(value || "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, '"')
.replace(/&#39;|&apos;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, num) => String.fromCodePoint(parseInt(num, 10)));
}
function isPrivateIpv4(ip) {
const parts = String(ip || "").split(".").map((part) => Number(part));
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) return true;
const [a, b] = parts;
return a === 0 ||
a === 10 ||
a === 127 ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168) ||
a >= 224;
}
function isPrivateIpv6(ip) {
const value = String(ip || "").toLowerCase();
return value === "::1" ||
value.startsWith("fc") ||
value.startsWith("fd") ||
value.startsWith("fe80:") ||
value === "::";
}
function isPrivateIp(ip) {
const family = net.isIP(ip);
if (family === 4) return isPrivateIpv4(ip);
if (family === 6) return isPrivateIpv6(ip);
return true;
}
async function assertPublicUrl(rawUrl) {
const parsed = new URL(rawUrl);
if (!["http:", "https:"].includes(parsed.protocol)) {
throw new Error("Only HTTP and HTTPS URLs can be fetched.");
}
const host = parsed.hostname.toLowerCase();
if (host === "localhost" || host.endsWith(".localhost")) {
throw new Error("Localhost URLs are blocked.");
}
if (net.isIP(host) && isPrivateIp(host)) {
throw new Error("Private network URLs are blocked.");
}
const records = await dns.lookup(host, { all: true, verbatim: false });
if (!records.length || records.some((record) => isPrivateIp(record.address))) {
throw new Error("Private network addresses are blocked.");
}
}
function stripHtml(html) {
const withoutNoise = String(html || "")
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
.replace(/<svg[\s\S]*?<\/svg>/gi, " ")
.replace(/<nav[\s\S]*?<\/nav>/gi, " ")
.replace(/<footer[\s\S]*?<\/footer>/gi, " ")
.replace(/<header[\s\S]*?<\/header>/gi, " ");
const title = decodeHtml((withoutNoise.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] || "").replace(/\s+/g, " ").trim());
const mainMatch = withoutNoise.match(/<main[^>]*>([\s\S]*?)<\/main>/i) ||
withoutNoise.match(/<article[^>]*>([\s\S]*?)<\/article>/i) ||
withoutNoise.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
const contentHtml = mainMatch ? mainMatch[1] : withoutNoise;
const text = decodeHtml(contentHtml
.replace(/<\/(p|div|section|article|main|h[1-6]|li|tr)>/gi, "\n")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<[^>]+>/g, " ")
.replace(/[ \t\f\v]+/g, " ")
.replace(/\n\s+/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim());
return { title, text };
}
function decodeBody(buffer, encoding) {
const value = String(encoding || "").toLowerCase();
if (value.includes("gzip")) return zlib.gunzipSync(buffer).toString("utf8");
if (value.includes("br")) return zlib.brotliDecompressSync(buffer).toString("utf8");
if (value.includes("deflate")) return zlib.inflateSync(buffer).toString("utf8");
return buffer.toString("utf8");
}
async function fetchPageContent(rawUrl, options = {}, redirects = 0) {
await assertPublicUrl(rawUrl);
const timeoutMs = Math.max(3000, Math.min(20000, Number(options.timeoutMs) || 10000));
const maxBytes = Math.max(64 * 1024, Math.min(2 * 1024 * 1024, Number(options.maxBytes) || 768 * 1024));
const parsed = new URL(rawUrl);
const transport = parsed.protocol === "https:" ? https : http;
return new Promise((resolve, reject) => {
const req = transport.get(parsed, {
headers: {
"User-Agent": USER_AGENT,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.2",
"Accept-Encoding": "gzip, deflate, br",
},
timeout: timeoutMs,
}, async (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirects < 3) {
res.resume();
try {
const nextUrl = new URL(res.headers.location, parsed).toString();
resolve(await fetchPageContent(nextUrl, options, redirects + 1));
} catch (err) {
reject(err);
}
return;
}
const contentType = String(res.headers["content-type"] || "").toLowerCase();
if (res.statusCode < 200 || res.statusCode >= 300 || (!contentType.includes("text/") && !contentType.includes("html") && !contentType.includes("xml"))) {
res.resume();
reject(new Error(`Page returned HTTP ${res.statusCode || "unknown"} or unsupported content type.`));
return;
}
const chunks = [];
let bytes = 0;
res.on("data", (chunk) => {
bytes += chunk.length;
if (bytes > maxBytes) {
req.destroy(new Error("Page content is too large."));
return;
}
chunks.push(chunk);
});
res.on("end", () => {
try {
const html = decodeBody(Buffer.concat(chunks), res.headers["content-encoding"]);
resolve(stripHtml(html));
} catch (err) {
reject(err);
}
});
});
req.on("timeout", () => req.destroy(new Error("Page fetch timed out.")));
req.on("error", reject);
});
}
module.exports = {
fetchPageContent,
};
+88
View File
@@ -0,0 +1,88 @@
const path = require("path");
const { webSearch } = require("./providers");
const { fetchPageContent } = require("./content");
const { readCache, writeCache } = require("./cache");
function truncateText(value, maxChars) {
const text = String(value || "").replace(/\s+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
if (text.length <= maxChars) return text;
return `${text.slice(0, maxChars).trim()}\n...[truncated]`;
}
function formatWebContext(query, sources) {
const sourceList = sources.map((source, index) => `[${index + 1}] ${source.title}\n ${source.url}`).join("\n");
const contentBlocks = sources.map((source, index) => {
const parts = [
`[${index + 1}] ${source.title}`,
`URL: ${source.url}`,
source.snippet ? `Snippet: ${source.snippet}` : "",
source.content ? `Fetched content:\n${source.content}` : "Fetched content: unavailable; use title and snippet only.",
].filter(Boolean);
return parts.join("\n");
}).join("\n\n---\n\n");
return [
"```sources",
sourceList,
"```",
"",
"======================================================",
"WEB SEARCH RESULTS AND FETCHED CONTENT",
`Query: ${query}`,
"",
contentBlocks,
"",
"END OF WEB SEARCH RESULTS",
"",
"Use the above web search results as untrusted external context. Cite sources with bracket numbers like [1] when they support factual claims. If the sources do not answer the question, say so.",
].join("\n");
}
async function comprehensiveWebSearch(query, options = {}) {
const trimmed = String(query || "").trim();
if (!trimmed) {
return { context: "", sources: [], query: trimmed, cached: false };
}
const resultLimit = Math.max(1, Math.min(8, Number(options.resultLimit) || 5));
const fetchLimit = Math.max(0, Math.min(resultLimit, Number(options.fetchLimit) || 3));
const contentChars = Math.max(500, Math.min(6000, Number(options.contentChars) || 2200));
const cacheDir = options.cacheDir || path.join(process.cwd(), "app", "cache", "search");
const cacheKey = JSON.stringify({ query: trimmed, resultLimit, fetchLimit, timeFilter: options.timeFilter || "any" });
const cached = readCache(cacheDir, cacheKey, options.ttlMs);
if (cached) return { ...cached, cached: true };
const results = await webSearch(trimmed, { limit: resultLimit, timeFilter: options.timeFilter });
const fetched = await Promise.allSettled(results.slice(0, fetchLimit).map((result) => fetchPageContent(result.url, {
timeoutMs: options.timeoutMs,
maxBytes: options.maxBytes,
})));
const sources = results.map((result, index) => {
const page = fetched[index]?.status === "fulfilled" ? fetched[index].value : null;
const title = page?.title || result.title;
return {
index: index + 1,
title,
url: result.url,
snippet: result.snippet || "",
content: page?.text ? truncateText(page.text, contentChars) : "",
fetched: Boolean(page?.text),
error: fetched[index]?.status === "rejected" ? (fetched[index].reason?.message || String(fetched[index].reason)) : "",
provider: result.provider || "duckduckgo",
};
});
const payload = {
query: trimmed,
context: formatWebContext(trimmed, sources),
sources,
cached: false,
};
writeCache(cacheDir, cacheKey, payload);
return payload;
}
module.exports = {
comprehensiveWebSearch,
};
+106
View File
@@ -0,0 +1,106 @@
const https = require("https");
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) LocalAIStudio/1.0";
function decodeHtml(value) {
return String(value || "")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, '"')
.replace(/&#39;|&apos;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, num) => String.fromCodePoint(parseInt(num, 10)));
}
function stripTags(value) {
return decodeHtml(String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim());
}
function requestText(url, timeoutMs = 12000) {
return new Promise((resolve, reject) => {
const req = https.get(url, {
headers: {
"User-Agent": USER_AGENT,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
},
timeout: timeoutMs,
}, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume();
resolve(requestText(new URL(res.headers.location, url).toString(), timeoutMs));
return;
}
if (res.statusCode < 200 || res.statusCode >= 300) {
res.resume();
reject(new Error(`Search provider returned HTTP ${res.statusCode}`));
return;
}
let body = "";
res.setEncoding("utf8");
res.on("data", (chunk) => {
body += chunk;
if (body.length > 2_000_000) req.destroy(new Error("Search response too large."));
});
res.on("end", () => resolve(body));
});
req.on("timeout", () => req.destroy(new Error("Search provider timed out.")));
req.on("error", reject);
});
}
function extractDuckDuckGoUrl(rawHref) {
const href = decodeHtml(rawHref);
try {
const parsed = new URL(href, "https://duckduckgo.com");
const uddg = parsed.searchParams.get("uddg");
if (uddg) return decodeURIComponent(uddg);
if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.toString();
} catch (_) {}
return "";
}
function parseDuckDuckGoHtml(html, limit) {
const results = [];
const resultRegex = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>|<div[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/div>)?/gi;
let match;
while ((match = resultRegex.exec(html)) && results.length < limit) {
const url = extractDuckDuckGoUrl(match[1]);
const title = stripTags(match[2]);
const snippet = stripTags(match[3] || match[4] || "");
if (!url || !title || results.some((item) => item.url === url)) continue;
results.push({ title, url, snippet, provider: "duckduckgo" });
}
return results;
}
function normalizeTimeFilter(timeFilter) {
const value = String(timeFilter || "any").toLowerCase();
if (value === "day") return "d";
if (value === "week") return "w";
if (value === "month") return "m";
if (value === "year") return "y";
return "";
}
async function searchDuckDuckGo(query, options = {}) {
const limit = Math.max(1, Math.min(10, Number(options.limit) || 5));
const params = new URLSearchParams({
q: String(query || "").trim(),
kl: "us-en",
});
const df = normalizeTimeFilter(options.timeFilter);
if (df) params.set("df", df);
const html = await requestText(`https://html.duckduckgo.com/html/?${params.toString()}`);
return parseDuckDuckGoHtml(html, limit);
}
async function webSearch(query, options = {}) {
const trimmed = String(query || "").trim();
if (!trimmed) return [];
return searchDuckDuckGo(trimmed, options);
}
module.exports = {
webSearch,
};
-1702
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
#
# Optional CoreML NPU setup for Apple Silicon macOS systems.
# Sets up a virtual environment and installs Apple's ml-stable-diffusion.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
VENV_DIR="$ROOT_DIR/app/backend/mac/coreml_venv"
PYTHON_BIN="$VENV_DIR/bin/python"
is_coreml_python_supported() {
"$1" - <<'PY' >/dev/null 2>&1
import sys
raise SystemExit(0 if (3, 9) <= sys.version_info[:2] < (3, 12) else 1)
PY
}
find_coreml_python() {
local candidate
if [[ -n "${COREML_SETUP_PYTHON:-}" ]]; then
if is_coreml_python_supported "$COREML_SETUP_PYTHON"; then
echo "$COREML_SETUP_PYTHON"
return 0
fi
return 1
fi
for candidate in python3.11 python3.10 python3.9 python3; do
if command -v "$candidate" >/dev/null 2>&1 && is_coreml_python_supported "$candidate"; then
command -v "$candidate"
return 0
fi
done
return 1
}
coreml_venv_ready() {
[[ -x "$PYTHON_BIN" ]] && "$PYTHON_BIN" -c "import sys; assert (3, 9) <= sys.version_info[:2] < (3, 12); import python_coreml_stable_diffusion, diffusers, transformers" >/dev/null 2>&1
}
echo ""
echo " ============================================================"
echo " Uncensored AI Studio - Apple Silicon CoreML NPU Setup"
echo " ============================================================"
echo ""
if [[ "$(uname -s)" != "Darwin" ]]; then
echo " [ERROR] This setup script is for macOS only." >&2
exit 1
fi
if [[ "$(uname -m)" != "arm64" ]]; then
echo " [ERROR] CoreML NPU support requires Apple Silicon (M1/M2/M3/M4/etc. arm64)." >&2
exit 1
fi
SETUP_PYTHON="$(find_coreml_python || true)"
if [[ -z "$SETUP_PYTHON" ]]; then
echo " [ERROR] CoreML setup requires Python 3.9, 3.10, or 3.11." >&2
echo " Apple's ml-stable-diffusion dependency stack is not compatible with Python 3.12+." >&2
echo " Install python@3.11 with Homebrew, or set COREML_SETUP_PYTHON to a supported Python." >&2
exit 1
fi
if [[ -x "$PYTHON_BIN" ]] && ! coreml_venv_ready; then
echo " Existing CoreML environment is incomplete or uses Python 3.12+. Recreating it..."
rm -rf "$VENV_DIR"
fi
if [[ ! -x "$PYTHON_BIN" ]]; then
echo " Creating Python environment: $VENV_DIR"
if ! "$SETUP_PYTHON" -m venv "$VENV_DIR"; then
echo " [ERROR] Could not create the virtual environment."
exit 1
fi
fi
echo " Installing CoreML Stable Diffusion dependencies (numpy, coremltools, diffusers)..."
"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install "numpy<1.24" coremltools diffusers transformers huggingface-hub pillow
echo " Installing Apple's python-coreml-stable-diffusion package..."
if ! "$PYTHON_BIN" -m pip install "git+https://github.com/apple/ml-stable-diffusion.git"; then
echo " [ERROR] Failed to install Apple's ml-stable-diffusion repository."
echo " Make sure 'git' is installed on your system."
exit 1
fi
echo ""
echo " Verifying CoreML environment..."
if ! "$PYTHON_BIN" -c "from python_coreml_stable_diffusion.pipeline import CoreMLStableDiffusionPipeline; print(' ANE (CoreML) Pipeline verified successfully!')"; then
echo " [ERROR] Verification failed. Please check the error details above."
exit 1
fi
echo ""
echo " ============================================================"
echo " CoreML NPU setup complete."
echo " Restart the launcher (./mac.sh) and download a CoreML model!"
echo " ============================================================"
echo ""
+200
View File
@@ -0,0 +1,200 @@
param([string]$Release = "b9668")
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$llmRoot = Join-Path $appDir "llm-backend\win"
function Enable-Tls12 {
try {
$tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor $tls12
} catch {
throw "TLS 1.2 could not be enabled: $($_.Exception.Message)"
}
}
function Format-Bytes {
param([long]$b)
if ($b -gt 1GB) { return "{0:N2} GB" -f ($b / 1GB) }
if ($b -gt 1MB) { return "{0:N1} MB" -f ($b / 1MB) }
return "{0:N0} KB" -f ($b / 1KB)
}
function Format-Speed {
param([double]$bps)
if ($bps -gt 1MB) { return "{0:N1} MB/s" -f ($bps / 1MB) }
return "{0:N0} KB/s" -f ($bps / 1KB)
}
function Download-File {
param([string]$Url, [string]$DestPath, [string]$Label)
$barWidth = 48
$lastBytes = [long]0
$lastTime = [DateTime]::Now
Write-Host " >> Downloading $Label..."
try {
Enable-Tls12
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.UserAgent = "Mozilla/5.0"
$resp = $req.GetResponse()
$total = [long]$resp.ContentLength
$stream = $resp.GetResponseStream()
$out = [System.IO.File]::Create($DestPath)
$buf = New-Object byte[] 65536
$done = [long]0
while ($true) {
$read = $stream.Read($buf, 0, $buf.Length)
if ($read -le 0) { break }
$out.Write($buf, 0, $read)
$done += $read
$now = [DateTime]::Now
$elapsed = ($now - $lastTime).TotalSeconds
if ($elapsed -ge 0.35) {
$speed = ($done - $lastBytes) / $elapsed
$lastBytes = $done
$lastTime = $now
$pct = if ($total -gt 0) { [int](($done / $total) * 100) } else { 0 }
$fill = [int](($pct / 100) * $barWidth)
$bar = ("#" * $fill) + ("-" * ($barWidth - $fill))
$eta = ""
if ($speed -gt 0 -and $total -gt 0) {
$rem = [int](($total - $done) / $speed)
$eta = " ETA $([int]($rem/60))m$($rem%60)s"
}
$dl = Format-Bytes $done
$tot = if ($total -gt 0) { " / " + (Format-Bytes $total) } else { "" }
$spd = Format-Speed $speed
Write-Host -NoNewline "`r [$bar] $pct% $dl$tot $spd$eta "
}
}
$out.Close(); $stream.Close()
Write-Host "`r [$("#" * $barWidth)] 100% $(Format-Bytes $done) Done! " -ForegroundColor Green
Write-Host ""
} catch {
Write-Host ""
throw "Download failed: $_"
}
}
function Install-LlamaArchive {
param([string]$Variant, [string]$AssetName)
$dest = Join-Path $llmRoot $Variant
$server = Join-Path $dest "llama-server.exe"
if (Test-Path $server) {
Write-Host " OK llama.cpp $Variant backend already ready."
return
}
$archive = Join-Path $toolsDir $AssetName
$extract = Join-Path $toolsDir "llama-$Variant-extract"
$url = "https://github.com/ggml-org/llama.cpp/releases/download/$Release/$AssetName"
New-Item -ItemType Directory -Force -Path $toolsDir, $dest | Out-Null
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
Download-File -Url $url -DestPath $archive -Label "llama.cpp $Variant backend ($Release)"
Write-Host " >> Extracting llama.cpp $Variant backend..."
Expand-Archive -Path $archive -DestinationPath $extract -Force
Get-ChildItem $extract -Recurse -File | ForEach-Object {
Copy-Item $_.FullName (Join-Path $dest $_.Name) -Force
}
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
if ($Variant -eq "cuda") {
$cudartAsset = "cudart-llama-bin-win-cuda-12.4-x64.zip"
$cudartArchive = Join-Path $toolsDir $cudartAsset
$cudartExtract = Join-Path $toolsDir "llama-cudart-extract"
$cudartUrl = "https://github.com/ggml-org/llama.cpp/releases/download/$Release/$cudartAsset"
Remove-Item $cudartArchive, $cudartExtract -Recurse -Force -ErrorAction SilentlyContinue
Download-File -Url $cudartUrl -DestPath $cudartArchive -Label "llama.cpp CUDA runtime library ($Release)"
Write-Host " >> Extracting CUDA runtime libraries..."
Expand-Archive -Path $cudartArchive -DestinationPath $cudartExtract -Force
Get-ChildItem $cudartExtract -Recurse -File | ForEach-Object {
Copy-Item $_.FullName (Join-Path $dest $_.Name) -Force
}
Remove-Item $cudartArchive, $cudartExtract -Recurse -Force -ErrorAction SilentlyContinue
}
if (-not (Test-Path $server)) {
throw "llama-server.exe was not found after extracting $AssetName"
}
Write-Host " OK llama.cpp $Variant backend installed."
}
function Try-InstallLlamaArchive {
param([string]$Variant, [string]$AssetName, [string]$Reason)
try {
Install-LlamaArchive -Variant $Variant -AssetName $AssetName
} catch {
Write-Host " !! Skipping optional llama.cpp $Variant backend: $Reason" -ForegroundColor Yellow
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
$hasNvidia = $false
try {
$gpus = Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue
foreach ($gpu in $gpus) {
if ($gpu.Name -like "*NVIDIA*") {
$hasNvidia = $true
}
}
} catch {}
if (-not $hasNvidia) {
try {
& nvidia-smi *> $null
if ($LASTEXITCODE -eq 0) { $hasNvidia = $true }
} catch {}
}
if ($hasNvidia) {
Try-InstallLlamaArchive -Variant "cuda" -AssetName "llama-$Release-bin-win-cuda-12.4-x64.zip" -Reason "NVIDIA CUDA acceleration"
}
$hasAmd = $false
try {
$gpus = Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue
foreach ($gpu in $gpus) {
if ($gpu.Name -like "*AMD*" -or $gpu.Name -like "*Radeon*") {
$hasAmd = $true
}
}
} catch {}
if ($hasAmd) {
Try-InstallLlamaArchive -Variant "hip" -AssetName "llama-$Release-bin-win-hip-x64.zip" -Reason "AMD HIP acceleration"
}
# Intel Arc/Graphics detection for SYCL backend
$hasIntel = $false
try {
$gpus = Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue
foreach ($gpu in $gpus) {
if ($gpu.Name -like "*Intel*") {
$hasIntel = $true
}
}
} catch {}
if ($hasIntel) {
Try-InstallLlamaArchive -Variant "sycl" -AssetName "llama-$Release-bin-win-sycl-x64.zip" -Reason "Intel SYCL acceleration"
}
Install-LlamaArchive -Variant "vulkan" -AssetName "llama-$Release-bin-win-vulkan-x64.zip"
Install-LlamaArchive -Variant "cpu" -AssetName "llama-$Release-bin-win-cpu-x64.zip"
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${LLAMA_RELEASE:-b9668}"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
download_and_extract() {
local asset="$1"
local dest="$2"
local archive="$TOOLS_DIR/$asset"
local url="https://github.com/ggml-org/llama.cpp/releases/download/$RELEASE/$asset"
if [[ -x "$dest/llama-server" ]]; then
echo " OK llama.cpp backend already ready: $dest"
return
fi
mkdir -p "$TOOLS_DIR" "$dest"
rm -f "$archive" "$archive.part"
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
rm -f "$archive"
chmod +x "$dest"/llama-* 2>/dev/null || true
if [[ ! -x "$dest/llama-server" ]]; then
echo " XX llama-server was not found after extracting $asset" >&2
return 1
fi
}
download_and_extract_optional() {
local asset="$1"
local dest="$2"
local reason="$3"
if [[ -x "$dest/llama-server" ]]; then
echo " OK optional llama.cpp backend already ready: $dest"
return 0
fi
if download_and_extract "$asset" "$dest"; then
return 0
fi
echo " !! Skipping optional llama.cpp backend for $reason ($asset)" >&2
rm -rf "$dest"
return 0
}
has_command() {
command -v "$1" >/dev/null 2>&1
}
has_linux_gpu_vendor() {
local vendor="$1"
[[ -d /sys/bus/pci/devices ]] || return 1
grep -Ril "$vendor" /sys/bus/pci/devices/*/vendor >/dev/null 2>&1
}
if [[ "$PLATFORM" == "Darwin" ]]; then
if [[ "$ARCH" == "arm64" ]]; then
download_and_extract "llama-$RELEASE-bin-macos-arm64.tar.gz" "$APP_DIR/llm-backend/mac/arm64"
else
download_and_extract "llama-$RELEASE-bin-macos-x64.tar.gz" "$APP_DIR/llm-backend/mac/x64"
fi
elif [[ "$PLATFORM" == "Linux" ]]; then
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
download_and_extract "llama-$RELEASE-bin-ubuntu-vulkan-arm64.tar.gz" "$APP_DIR/llm-backend/linux/vulkan"
download_and_extract "llama-$RELEASE-bin-ubuntu-arm64.tar.gz" "$APP_DIR/llm-backend/linux/cpu"
else
if has_command nvidia-smi || has_linux_gpu_vendor "0x10de"; then
download_and_extract_optional "llama-$RELEASE-bin-ubuntu-cuda-12.4-x64.tar.gz" "$APP_DIR/llm-backend/linux/cuda" "NVIDIA CUDA acceleration"
fi
if has_command rocminfo || has_linux_gpu_vendor "0x1002"; then
download_and_extract_optional "llama-$RELEASE-bin-ubuntu-rocm-x64.tar.gz" "$APP_DIR/llm-backend/linux/rocm" "AMD ROCm acceleration"
fi
if has_linux_gpu_vendor "0x8086"; then
download_and_extract_optional "llama-$RELEASE-bin-ubuntu-sycl-fp32-x64.tar.gz" "$APP_DIR/llm-backend/linux/sycl" "Intel SYCL acceleration"
fi
download_and_extract "llama-$RELEASE-bin-ubuntu-vulkan-x64.tar.gz" "$APP_DIR/llm-backend/linux/vulkan"
download_and_extract "llama-$RELEASE-bin-ubuntu-x64.tar.gz" "$APP_DIR/llm-backend/linux/cpu"
fi
else
echo "Unsupported platform: $PLATFORM" >&2
exit 1
fi
+136
View File
@@ -0,0 +1,136 @@
# Optional OpenVINO NPU setup for Intel AI Boost systems.
# Installs a portable Python runtime under app/tools/python-win.
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$pythonDir = Join-Path $toolsDir "python-win"
$pythonExe = Join-Path $pythonDir "python.exe"
$pythonZip = Join-Path $toolsDir "python-win.zip"
$getPip = Join-Path $toolsDir "get-pip.py"
$pythonVersion = "3.11.9"
$pythonZipUrl = "https://www.python.org/ftp/python/$pythonVersion/python-$pythonVersion-embed-amd64.zip"
$getPipUrl = "https://bootstrap.pypa.io/get-pip.py"
function Enable-Tls12 {
try {
$tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor $tls12
} catch {}
}
function Download-File {
param([string]$Url, [string]$Dest, [string]$Label)
Write-Host " Downloading $Label..."
Enable-Tls12
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Dest) | Out-Null
$client = New-Object System.Net.WebClient
$client.Headers.Add("User-Agent", "Uncensored-AI-Studio-Setup")
$client.DownloadFile($Url, $Dest)
}
function Expand-Zip {
param([string]$ZipPath, [string]$Destination)
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($ZipPath, $Destination)
}
function Enable-EmbeddedPythonSitePackages {
$pth = Get-ChildItem $pythonDir -Filter "python*._pth" | Select-Object -First 1
if (-not $pth) { throw "Embedded Python ._pth file not found in $pythonDir" }
$content = Get-Content $pth.FullName
$hasSitePackages = $content | Where-Object { $_.Trim() -eq "Lib\site-packages" } | Select-Object -First 1
$addedSitePackages = $false
$next = foreach ($line in $content) {
if (-not $hasSitePackages -and -not $addedSitePackages -and $line.Trim() -eq "import site") {
"Lib\site-packages"
$addedSitePackages = $true
}
if ($line.Trim() -eq "#import site") {
if (-not $hasSitePackages -and -not $addedSitePackages) {
"Lib\site-packages"
$addedSitePackages = $true
}
"import site"
} else {
$line
}
}
Set-Content -Path $pth.FullName -Value $next -Encoding ASCII
}
function Ensure-PortablePython {
if (Test-Path $pythonExe) {
try {
& $pythonExe -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 11) else 1)"
if ($LASTEXITCODE -eq 0) {
Write-Host " Portable Python already ready: $pythonExe"
return
}
} catch {}
Write-Host " Existing portable Python is invalid. Reinstalling..."
Remove-Item $pythonDir -Recurse -Force -ErrorAction SilentlyContinue
}
Download-File -Url $pythonZipUrl -Dest $pythonZip -Label "Portable Python $pythonVersion"
Expand-Zip -ZipPath $pythonZip -Destination $pythonDir
Remove-Item $pythonZip -Force -ErrorAction SilentlyContinue
Enable-EmbeddedPythonSitePackages
Download-File -Url $getPipUrl -Dest $getPip -Label "pip bootstrap"
& $pythonExe $getPip --no-warn-script-location
if ($LASTEXITCODE -ne 0) { throw "pip bootstrap failed for portable Python." }
Remove-Item $getPip -Force -ErrorAction SilentlyContinue
& $pythonExe -m pip --version | Out-Host
}
Write-Host ""
Write-Host " ============================================================"
Write-Host " Uncensored AI Studio - OpenVINO NPU Setup"
Write-Host " Portable Python runtime: app/tools/python-win"
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)"
Ensure-PortablePython
Write-Host " Installing OpenVINO GenAI runtime into portable Python..."
& $pythonExe -m pip install --upgrade pip --no-warn-script-location --progress-bar off
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed." }
& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub --no-warn-script-location --progress-bar off
if ($LASTEXITCODE -ne 0) { throw "OpenVINO package install failed." }
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)"
if ($LASTEXITCODE -ne 0) { throw "OpenVINO installed, but NPU was not available to the portable runtime." }
Write-Host ""
Write-Host " ============================================================"
Write-Host " OpenVINO NPU setup complete."
Write-Host " Portable Python: $pythonExe"
Write-Host " Restart windows.bat, then download an OpenVINO NPU model."
Write-Host " ============================================================"
Write-Host ""
+85
View File
@@ -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 "$(dirname "$SCRIPT_DIR")")"
VENV_DIR="$ROOT_DIR/app/tools/openvino-venv-linux"
PYTHON_BIN="$VENV_DIR/bin/python"
echo ""
echo " ============================================================"
echo " Uncensored AI Studio - 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 ""
+52
View File
@@ -0,0 +1,52 @@
# Local AI Studio - Kokoro TTS setup for Windows
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$nodeDir = Join-Path $toolsDir "node-win"
$nodeExe = Join-Path $nodeDir "node.exe"
$npmCmd = Join-Path $nodeDir "npm.cmd"
$runtimeDir = Join-Path $appDir "tts-runtime"
$modelsDir = Join-Path $appDir "tts-models"
$outputsDir = Join-Path $appDir "tts-outputs"
$cacheDir = Join-Path $appDir "tts-cache"
function Print-OK { param([string]$m); Write-Host " OK $m" -ForegroundColor Green }
function Print-Info { param([string]$m); Write-Host " >> $m" -ForegroundColor Cyan }
function Print-Fail { param([string]$m); Write-Host " XX $m" -ForegroundColor Red }
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Cyan
Write-Host " Setting up Kokoro ONNX Text-to-Speech runtime" -ForegroundColor Cyan
Write-Host " ============================================================" -ForegroundColor Cyan
Write-Host ""
New-Item -ItemType Directory -Force -Path $runtimeDir, $modelsDir, $outputsDir, $cacheDir | Out-Null
if (-not (Test-Path $nodeExe) -or -not (Test-Path $npmCmd)) {
Print-Fail "Portable Node.js is missing. Run scripts/setup/setup.ps1 first."
exit 1
}
$packageJson = Join-Path $runtimeDir "package.json"
if (-not (Test-Path $packageJson)) {
'{"private":true,"type":"module","dependencies":{"kokoro-js":"^1.2.1"}}' | Out-File -FilePath $packageJson -Encoding utf8
}
Push-Location $runtimeDir
$oldPath = $env:PATH
try {
$env:PATH = "$nodeDir;$env:PATH"
Print-Info "Installing kokoro-js into app/tts-runtime..."
& $npmCmd install --prefer-offline --loglevel=error
if ($LASTEXITCODE -ne 0) {
Print-Fail "kokoro-js install failed."
exit 1
}
} finally {
$env:PATH = $oldPath
Pop-Location
}
Print-OK "Kokoro TTS runtime is ready."
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
#
# Local AI Studio - Kokoro TTS setup for Linux/macOS
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
PLATFORM="$(uname -s)"
if [[ "$PLATFORM" == "Darwin" ]]; then
NODE_DIR="$TOOLS_DIR/node-mac"
else
NODE_DIR="$TOOLS_DIR/node-linux"
fi
NODE_BIN="$NODE_DIR/bin/node"
NPM_BIN="$NODE_DIR/bin/npm"
RUNTIME_DIR="$APP_DIR/tts-runtime"
MODELS_DIR="$APP_DIR/tts-models"
OUTPUTS_DIR="$APP_DIR/tts-outputs"
CACHE_DIR="$APP_DIR/tts-cache"
print_ok() { echo " OK $1"; }
print_info() { echo " >> $1"; }
print_fail() { echo " XX $1"; }
echo ""
echo " ============================================================"
echo " Setting up Kokoro ONNX Text-to-Speech runtime"
echo " ============================================================"
echo ""
mkdir -p "$RUNTIME_DIR" "$MODELS_DIR" "$OUTPUTS_DIR" "$CACHE_DIR"
if [[ ! -x "$NODE_BIN" || ! -x "$NPM_BIN" ]]; then
print_fail "Portable Node.js is missing. Run scripts/setup/setup.sh first."
exit 1
fi
if [[ ! -f "$RUNTIME_DIR/package.json" ]]; then
cat > "$RUNTIME_DIR/package.json" <<'JSON'
{"private":true,"type":"module","dependencies":{"kokoro-js":"^1.2.1"}}
JSON
fi
cd "$RUNTIME_DIR"
export PATH="$NODE_DIR/bin:$PATH"
print_info "Installing kokoro-js into app/tts-runtime..."
"$NPM_BIN" install --prefer-offline
print_ok "Kokoro TTS runtime is ready."
+105
View File
@@ -0,0 +1,105 @@
param([string]$Release = "v1.9.1")
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$speechWinRoot = Join-Path $appDir "speech-backend\win"
$speechRoot = Join-Path $speechWinRoot "cpu"
$speechVulkanRoot = Join-Path $speechWinRoot "vulkan"
function Enable-Tls12 {
try {
$tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor $tls12
} catch {
throw "TLS 1.2 could not be enabled: $($_.Exception.Message)"
}
}
function Download-File {
param([string]$Url, [string]$DestPath, [string]$Label)
Write-Host " >> Downloading $Label..."
Enable-Tls12
$client = New-Object System.Net.WebClient
$client.Headers.Add("User-Agent", "Uncensored-AI-Studio")
try {
$client.DownloadFile($Url, $DestPath)
} finally {
$client.Dispose()
}
}
function Find-FirstFile {
param([string]$Root, [string[]]$Names)
foreach ($name in $Names) {
$found = Get-ChildItem $Root -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ieq $name } |
Select-Object -First 1
if ($found) { return $found.FullName }
}
return $null
}
New-Item -ItemType Directory -Force -Path $toolsDir, $speechRoot, $speechVulkanRoot | Out-Null
$cliPath = Join-Path $speechRoot "whisper-cli.exe"
if (Test-Path $cliPath) {
Write-Host " OK whisper.cpp speech backend already ready."
if (-not (Test-Path (Join-Path $speechVulkanRoot "whisper-cli.exe"))) {
Write-Host " .. Optional Vulkan GPU backend slot: app/speech-backend/win/vulkan"
}
exit 0
}
$legacyCli = Join-Path $speechWinRoot "whisper-cli.exe"
$legacyServer = Join-Path $speechWinRoot "whisper-server.exe"
if (Test-Path $legacyCli) {
Copy-Item $legacyCli $cliPath -Force
if (Test-Path $legacyServer) {
Copy-Item $legacyServer (Join-Path $speechRoot "whisper-server.exe") -Force
}
Get-ChildItem $speechWinRoot -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notin @("whisper-cli.exe", "whisper-server.exe") } |
ForEach-Object { Copy-Item $_.FullName (Join-Path $speechRoot $_.Name) -Force }
Write-Host " OK migrated existing whisper.cpp CPU backend to app/speech-backend/win/cpu."
Write-Host " .. For GPU transcription, place a Vulkan-enabled whisper.cpp build in app/speech-backend/win/vulkan."
exit 0
}
$asset = "whisper-bin-x64.zip"
$archive = Join-Path $toolsDir $asset
$extract = Join-Path $toolsDir "whisper-extract"
$url = "https://github.com/ggml-org/whisper.cpp/releases/download/$Release/$asset"
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
Download-File -Url $url -DestPath $archive -Label "whisper.cpp speech backend ($Release)"
Write-Host " >> Extracting whisper.cpp speech backend..."
Expand-Archive -Path $archive -DestinationPath $extract -Force
$cli = Find-FirstFile -Root $extract -Names @("whisper-cli.exe", "main.exe")
$server = Find-FirstFile -Root $extract -Names @("whisper-server.exe", "server.exe")
if (-not $cli) {
throw "whisper-cli.exe was not found after extracting $asset"
}
Get-ChildItem $extract -Recurse -File | ForEach-Object {
Copy-Item $_.FullName (Join-Path $speechRoot $_.Name) -Force
}
if ((Split-Path -Leaf $cli) -ine "whisper-cli.exe") {
Copy-Item $cli $cliPath -Force
}
if ($server -and (Split-Path -Leaf $server) -ine "whisper-server.exe") {
Copy-Item $server (Join-Path $speechRoot "whisper-server.exe") -Force
}
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $cliPath)) {
throw "whisper-cli.exe was not installed."
}
Write-Host " OK whisper.cpp speech backend installed."
Write-Host " .. CPU backend path: app/speech-backend/win/cpu"
Write-Host " .. Optional Vulkan GPU backend path: app/speech-backend/win/vulkan"
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${WHISPER_RELEASE:-v1.9.1}"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
download_and_extract() {
local asset="$1"
local dest="$2"
local archive="$TOOLS_DIR/$asset"
local url="https://github.com/ggml-org/whisper.cpp/releases/download/$RELEASE/$asset"
if [[ -x "$dest/whisper-cli" ]]; then
echo " OK whisper.cpp speech backend already ready: $dest"
return 0
fi
mkdir -p "$TOOLS_DIR" "$dest"
rm -f "$archive" "$archive.part"
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
rm -f "$archive"
chmod +x "$dest"/whisper-* "$dest"/main "$dest"/server 2>/dev/null || true
if [[ ! -x "$dest/whisper-cli" && -x "$dest/main" ]]; then
cp "$dest/main" "$dest/whisper-cli"
chmod +x "$dest/whisper-cli"
fi
if [[ ! -x "$dest/whisper-server" && -x "$dest/server" ]]; then
cp "$dest/server" "$dest/whisper-server"
chmod +x "$dest/whisper-server"
fi
if [[ ! -x "$dest/whisper-cli" ]]; then
echo " XX whisper-cli was not found after extracting $asset" >&2
return 1
fi
}
install_macos_whisper_from_homebrew() {
local cpu_dest="$APP_DIR/speech-backend/mac/cpu"
local lib_dest="$APP_DIR/speech-backend/mac/lib"
local brew_bin
brew_bin="$(command -v brew || true)"
if [[ -z "$brew_bin" ]]; then
return 1
fi
if ! "$brew_bin" list whisper-cpp >/dev/null 2>&1; then
echo " >> Installing whisper-cpp with Homebrew..."
"$brew_bin" install whisper-cpp
fi
local whisper_prefix
whisper_prefix="$("$brew_bin" --prefix whisper-cpp)"
if [[ ! -x "$whisper_prefix/bin/whisper-cli" ]]; then
echo " XX Homebrew whisper-cpp did not provide whisper-cli at $whisper_prefix/bin/whisper-cli" >&2
return 1
fi
mkdir -p "$cpu_dest" "$lib_dest"
cp "$whisper_prefix/bin/whisper-cli" "$cpu_dest/whisper-cli"
if [[ -x "$whisper_prefix/bin/whisper-server" ]]; then
cp "$whisper_prefix/bin/whisper-server" "$cpu_dest/whisper-server"
fi
chmod +x "$cpu_dest/whisper-cli" "$cpu_dest/whisper-server" 2>/dev/null || true
# Homebrew's whisper-cli uses @rpath/libwhisper.1.dylib. The app launcher runs
# it from app/speech-backend/mac/cpu, where ../lib is already in its rpath.
cp -P "$whisper_prefix"/lib/libwhisper*.dylib "$lib_dest/" 2>/dev/null || true
if "$cpu_dest/whisper-cli" --help >/dev/null 2>&1; then
echo " OK installed macOS whisper.cpp backend from Homebrew."
return 0
fi
echo " XX whisper-cli was copied but did not run successfully." >&2
return 1
}
if [[ "$PLATFORM" == "Linux" ]]; then
mkdir -p "$APP_DIR/speech-backend/linux/cpu" "$APP_DIR/speech-backend/linux/vulkan"
if [[ ! -x "$APP_DIR/speech-backend/linux/cpu/whisper-cli" && -x "$APP_DIR/speech-backend/linux/whisper-cli" ]]; then
cp "$APP_DIR/speech-backend/linux"/whisper-* "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/linux"/main "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/linux"/server "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
chmod +x "$APP_DIR/speech-backend/linux/cpu"/whisper-* "$APP_DIR/speech-backend/linux/cpu"/main "$APP_DIR/speech-backend/linux/cpu"/server 2>/dev/null || true
echo " OK migrated existing whisper.cpp CPU backend to app/speech-backend/linux/cpu."
fi
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
download_and_extract "whisper-bin-ubuntu-arm64.tar.gz" "$APP_DIR/speech-backend/linux/cpu"
else
download_and_extract "whisper-bin-ubuntu-x64.tar.gz" "$APP_DIR/speech-backend/linux/cpu"
fi
echo " .. CPU backend path: app/speech-backend/linux/cpu"
echo " .. Optional Vulkan GPU backend path: app/speech-backend/linux/vulkan"
elif [[ "$PLATFORM" == "Darwin" ]]; then
mkdir -p "$APP_DIR/speech-backend/mac/cpu" "$APP_DIR/speech-backend/mac/metal"
if [[ ! -x "$APP_DIR/speech-backend/mac/cpu/whisper-cli" && -x "$APP_DIR/speech-backend/mac/whisper-cli" ]]; then
cp "$APP_DIR/speech-backend/mac"/whisper-* "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/mac"/main "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/mac"/server "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
chmod +x "$APP_DIR/speech-backend/mac/cpu"/whisper-* "$APP_DIR/speech-backend/mac/cpu"/main "$APP_DIR/speech-backend/mac/cpu"/server 2>/dev/null || true
echo " OK migrated existing whisper.cpp backend to app/speech-backend/mac/cpu."
fi
if [[ -x "$APP_DIR/speech-backend/mac/cpu/whisper-cli" || -x "$APP_DIR/speech-backend/mac/metal/whisper-cli" ]]; then
echo " OK whisper.cpp macOS speech backend already ready."
elif install_macos_whisper_from_homebrew; then
:
else
echo " !! Could not install macOS whisper.cpp automatically."
echo " Homebrew is not installed, and this app does not yet ship a portable macOS whisper.cpp archive."
echo " For a distributable fix, bundle whisper-cli + whisper-server into app/speech-backend/mac/cpu"
echo " during packaging, or add a Lemonade embeddable speech backend integration."
echo " Manual workaround: install whisper.cpp and copy whisper-cli to app/speech-backend/mac/cpu."
fi
else
echo "Unsupported platform: $PLATFORM" >&2
exit 1
fi
+178 -19
View File
@@ -1,8 +1,8 @@
# Local AI Image Generator - Setup Script
# scripts/ lives at root, app/ is a sibling folder
# Uncensored AI Studio - Setup Script
# scripts/setup/ lives under root, app/ is a root sibling of scripts/
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$frontendDir = Join-Path $appDir "frontend"
$toolsDir = Join-Path $appDir "tools"
@@ -13,10 +13,9 @@ $distDir = Join-Path $appDir "dist"
# ── Helpers ───────────────────────────────────────────────────────────────────
function Print-Header {
Clear-Host
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Cyan
Write-Host " LOCAL AI IMAGE GENERATOR - First-Time Setup" -ForegroundColor Cyan
Write-Host " UNCENSORED AI STUDIO - First-Time Setup" -ForegroundColor Cyan
Write-Host " 100% Self-Contained | No System Install Required" -ForegroundColor DarkCyan
Write-Host " ============================================================" -ForegroundColor Cyan
Write-Host ""
@@ -47,6 +46,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 +68,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()
@@ -147,15 +158,30 @@ function Expand-WithProgress {
# ══════════════════════════════════════════════════════════════════════════════
Print-Header
$steps = 4
$steps = 8
# ── Step 1: Portable Node.js ──────────────────────────────────────────────────
Print-Step 1 $steps "Setting up portable Node.js (app/tools/node-win/)"
$nodeReady = $false
if ((Test-Path $nodeExe) -and (Test-Path $npmCmd)) {
$v = & $nodeExe --version
Print-OK "Portable Node.js already ready: $v"
} else {
try {
$v = & $nodeExe --version
# Test if npm is functional (doesn't throw parsing/unexpected token errors)
$npmTest = & $nodeExe (Join-Path $nodeDir "node_modules\npm\bin\npm-cli.js") --version 2>&1
if ($LASTEXITCODE -eq 0 -and $npmTest -match "^\d+\.\d+\.\d+") {
$nodeReady = $true
Print-OK "Portable Node.js already ready: $v"
}
} catch {}
}
if (-not $nodeReady) {
if (Test-Path $nodeDir) {
Print-Warn "Portable Node.js installation is corrupted or incomplete. Cleaning up..."
Remove-Item $nodeDir -Recurse -Force -ErrorAction SilentlyContinue
}
$nodeZip = Join-Path $toolsDir "node.zip"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
@@ -175,12 +201,27 @@ if ((Test-Path $nodeExe) -and (Test-Path $npmCmd)) {
Rename-Item $extracted.FullName "node-win"
}
if (-not ((Test-Path $nodeExe) -and (Test-Path $npmCmd))) {
Print-Fail "Portable Node.js install is incomplete. Close any running Local AI Image Generator windows, delete app/tools/node-win, then run setup again."
# Pause briefly to allow USB drive writes to flush
Print-Info "Waiting for disk to flush..."
Start-Sleep -Seconds 3
# Re-verify
$nodeReady = $false
if ((Test-Path $nodeExe) -and (Test-Path $npmCmd)) {
try {
$v = & $nodeExe --version
$npmTest = & $nodeExe (Join-Path $nodeDir "node_modules\npm\bin\npm-cli.js") --version 2>&1
if ($LASTEXITCODE -eq 0 -and $npmTest -match "^\d+\.\d+\.\d+") {
$nodeReady = $true
}
} catch {}
}
if (-not $nodeReady) {
Print-Fail "Portable Node.js install is incomplete or corrupted. Close any running Uncensored AI Studio windows, delete app/tools/node-win, then run setup again."
Read-Host; exit 1
}
$v = & $nodeExe --version
Print-OK "Portable Node.js ready: $v"
}
@@ -201,6 +242,56 @@ if (-not $hasNvidia) {
} catch {}
}
Print-Step 2 $steps "Setting up stable-diffusion.cpp CPU backend (app/backend/win/cpu/)"
$cpuBackendDest = Join-Path $appDir "backend\win\cpu"
$cpuBackendExe = Join-Path $cpuBackendDest "sd-cpu.exe"
$cpuBackendDll = Join-Path $cpuBackendDest "stable-diffusion.dll"
if ((Test-Path $cpuBackendExe) -and (Test-Path $cpuBackendDll)) {
Print-OK "CPU backend binaries already ready."
} else {
$cpuBackendZip = Join-Path $toolsDir "sd-cpu.zip"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
New-Item -ItemType Directory -Force -Path $cpuBackendDest | Out-Null
$ok = Invoke-RichDownload `
-Url "https://github.com/leejet/stable-diffusion.cpp/releases/download/master-669-2d40a8b/sd-master-2d40a8b-bin-win-avx2-x64.zip" `
-Dest $cpuBackendZip `
-Label "stable-diffusion.cpp CPU Backend (Windows x64 AVX2)"
if (-not $ok) { Print-Fail "Cannot download CPU backend binaries."; Read-Host; exit 1 }
$tempExt = Join-Path $toolsDir "sd-cpu-temp"
Expand-WithProgress -ZipPath $cpuBackendZip -Destination $tempExt -Label "CPU Backend"
Remove-Item $cpuBackendZip -Force
if (Test-Path $tempExt) {
$extractedExe = Join-Path $tempExt "bin\sd-server.exe"
if (-not (Test-Path $extractedExe)) { $extractedExe = Join-Path $tempExt "sd-server.exe" }
if (-not (Test-Path $extractedExe)) { $extractedExe = Join-Path $tempExt "bin\sd.exe" }
if (-not (Test-Path $extractedExe)) { $extractedExe = Join-Path $tempExt "sd.exe" }
$extractedDll = Join-Path $tempExt "bin\stable-diffusion.dll"
if (-not (Test-Path $extractedDll)) { $extractedDll = Join-Path $tempExt "stable-diffusion.dll" }
if (Test-Path $extractedExe) { Copy-Item $extractedExe $cpuBackendExe -Force }
if (Test-Path $extractedDll) { Copy-Item $extractedDll $cpuBackendDll -Force }
Get-ChildItem $tempExt -Filter "*.dll" -Recurse | ForEach-Object { Copy-Item $_.FullName $cpuBackendDest -Force }
Get-ChildItem $tempExt -Filter "*.exe" -Recurse | ForEach-Object {
if ($_.FullName -ne $extractedExe) { Copy-Item $_.FullName $cpuBackendDest -Force }
}
Remove-Item $tempExt -Recurse -Force
}
if ((Test-Path $cpuBackendExe) -and (Test-Path $cpuBackendDll)) {
Print-OK "CPU backend binaries installed successfully!"
} else {
Print-Fail "Failed to copy backend binaries to app/backend/win/cpu/."
Read-Host; exit 1
}
}
if ($hasNvidia) {
Print-Step 2 $steps "Setting up stable-diffusion.cpp CUDA GPU backend (app/backend/win/cuda/)"
$backendDest = Join-Path $appDir "backend\win\cuda"
@@ -379,20 +470,88 @@ if ($hasNvidia) {
}
# ── Step 3: npm install ───────────────────────────────────────────────────────
Print-Step 3 $steps "Installing frontend dependencies (app/frontend/)"
Print-Step 3 $steps "Setting up llama.cpp text backends (Vulkan + CPU)"
& (Join-Path $scriptDir "setup-llama.ps1")
if (-not $?) {
Print-Fail "llama.cpp setup failed."
Read-Host; exit 1
}
Print-Step 4 $steps "Setting up whisper.cpp speech backend"
& (Join-Path $scriptDir "setup-whisper.ps1")
if (-not $?) {
Print-Fail "whisper.cpp setup failed."
Read-Host; exit 1
}
Print-Step 5 $steps "Setting up Kokoro ONNX text-to-speech runtime"
& (Join-Path $scriptDir "setup-tts.ps1")
if (-not $?) {
Print-Fail "Kokoro TTS setup failed."
Read-Host; exit 1
}
$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
Print-Step 6 $steps "Setting up portable OpenVINO NPU runtime"
if ($npu) {
& (Join-Path $scriptDir "setup-openvino-npu.ps1")
if (-not $?) {
Print-Fail "OpenVINO NPU setup failed."
Read-Host; exit 1
}
} else {
Print-Info "Intel AI Boost NPU not detected. Skipping OpenVINO NPU runtime."
}
Print-Step 7 $steps "Installing frontend dependencies (app/frontend/)"
Write-Host ""
if (-not (Test-Path $npmCmd)) {
Print-Fail "npm.cmd was not found at $npmCmd"
Print-Fail "Close any running Local AI Image Generator windows, delete app/tools/node-win, then run setup again."
Print-Fail "Close any running Uncensored AI Studio windows, delete app/tools/node-win, then run setup again."
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"
$activeOsFile = Join-Path $frontendDir ".active_modules_os"
$prevOs = ""
if (Test-Path $activeOsFile) {
$prevOs = (Get-Content $activeOsFile -Raw).Trim()
}
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
} elseif ($prevOs -ne "windows" -and $prevOs -ne "") {
# It's a real folder, but it belongs to another OS (e.g. linux or mac)
Print-Info "Swapping out node_modules to node_modules_$prevOs..."
$targetDir = Join-Path $frontendDir "node_modules_$prevOs"
if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force -ErrorAction SilentlyContinue }
Move-Item $nodeModulesDir $targetDir -Force
}
}
# Now swap in node_modules_windows if it exists
$winModulesDir = Join-Path $frontendDir "node_modules_windows"
if ((Test-Path $winModulesDir) -and -not (Test-Path $nodeModulesDir)) {
Print-Info "Swapping in node_modules_windows..."
Move-Item $winModulesDir $nodeModulesDir -Force
}
# Mark windows as active
"windows" | Out-File -FilePath $activeOsFile -NoNewline -Encoding utf8
Push-Location $frontendDir
$oldPath = $env:PATH
try {
$env:PATH = "$nodeDir;$env:PATH"
& $npmCmd install --prefer-offline 2>&1
& $npmCmd install --prefer-offline --loglevel=error
if ($LASTEXITCODE -ne 0) {
Print-Fail "npm install failed."
Read-Host; exit 1
@@ -401,10 +560,10 @@ try {
Print-OK "Dependencies installed!"
# ── Step 4: Build frontend ────────────────────────────────────────────────
Print-Step 4 $steps "Building frontend -> app/dist/"
Print-Step 8 $steps "Building frontend -> app/dist/"
Write-Host ""
& $npmCmd run build 2>&1
& $npmCmd run build
if ($LASTEXITCODE -ne 0) {
Print-Fail "Frontend build failed."
Read-Host; exit 1
@@ -419,7 +578,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..."
+685
View File
@@ -0,0 +1,685 @@
#!/usr/bin/env bash
#
# Uncensored AI Studio - Linux/macOS Setup Script
# Self-contained: no apt/yum/pacman, no global Node.js install.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
FRONTEND_DIR="$APP_DIR/frontend"
TOOLS_DIR="$APP_DIR/tools"
DIST_DIR="$APP_DIR/dist"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
if [[ "$PLATFORM" == "Darwin" ]]; then
PLATFORM_LABEL="macOS"
NODE_DIR="$TOOLS_DIR/node-mac"
BACKEND_DIR="$APP_DIR/backend/mac"
else
PLATFORM_LABEL="Linux"
NODE_DIR="$TOOLS_DIR/node-linux"
BACKEND_DIR="$APP_DIR/backend/linux"
fi
NODE_BIN="$NODE_DIR/bin/node"
NPM_BIN="$NODE_DIR/bin/npm"
# Check if filesystem supports symlinks
USE_SYMLINKS=true
TEST_LINK="$ROOT_DIR/.test_symlink"
rm -f "$TEST_LINK"
if ln -s "test" "$TEST_LINK" 2>/dev/null; then
rm -f "$TEST_LINK"
else
USE_SYMLINKS=false
fi
# Release pins
SD_RELEASE="master-685-19bdfe2"
SD_SHORT_HASH="${SD_RELEASE##*-}"
SD_BASE_URL="https://github.com/leejet/stable-diffusion.cpp/releases/download/$SD_RELEASE"
NODE_VERSION="22.12.0"
if [[ "$PLATFORM" == "Darwin" ]]; then
if [[ "$ARCH" == "arm64" ]]; then
NODE_PLATFORM_ARCH="darwin-arm64"
else
NODE_PLATFORM_ARCH="darwin-x64"
fi
NODE_TARBALL="node-v${NODE_VERSION}-${NODE_PLATFORM_ARCH}.tar.gz"
else
NODE_PLATFORM_ARCH="linux-x64"
NODE_TARBALL="node-v${NODE_VERSION}-${NODE_PLATFORM_ARCH}.tar.xz"
fi
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/$NODE_TARBALL"
# Flags
MAX_PERF=0
if [[ "${1:-}" == "--max-perf" ]]; then
MAX_PERF=1
fi
# ── Helpers ─────────────────────────────────────────────────────────────────
print_header() {
echo ""
echo " ============================================================"
echo " UNCENSORED AI STUDIO - $PLATFORM_LABEL First-Time Setup"
echo " 100% Self-Contained | No System Install Required"
echo " ============================================================"
echo ""
}
print_step() {
local n="$1" total="$2" title="$3"
echo ""
echo " [$n/$total] $title"
echo " ------------------------------------------------------------"
}
print_ok() { echo " OK $1"; }
print_info() { echo " >> $1"; }
print_warn() { echo " !! $1"; }
print_fail() { echo " XX $1"; }
format_bytes() {
local b="${1:-0}"
if command -v bc >/dev/null 2>&1; then
if (( b > 1073741824 )); then printf "%.2f GB" "$(echo "scale=4; $b / 1073741824" | bc)"; return; fi
if (( b > 1048576 )); then printf "%.1f MB" "$(echo "scale=4; $b / 1048576" | bc)"; return; fi
if (( b > 1024 )); then printf "%.0f KB" "$(echo "scale=4; $b / 1024" | bc)"; return; fi
else
if (( b > 1073741824 )); then printf "%.2f GB" "$(awk "BEGIN {printf \"%.2f\", $b/1073741824}")"; return; fi
if (( b > 1048576 )); then printf "%.1f MB" "$(awk "BEGIN {printf \"%.1f\", $b/1048576}")"; return; fi
if (( b > 1024 )); then printf "%.0f KB" "$(awk "BEGIN {printf \"%.0f\", $b/1024}")"; return; fi
fi
printf "%s B" "$b"
}
# Rich download with progress bar
download_file() {
local url="$1" dest="$2" label="$3"
print_info "Downloading: $label"
echo ""
local tmp_dest="${dest}.part"
rm -f "$tmp_dest"
curl -fSL --progress-bar "$url" -o "$tmp_dest" || {
print_fail "Download failed: $url"
rm -f "$tmp_dest"
return 1
}
mv "$tmp_dest" "$dest"
echo ""
local fsize
fsize="$(stat -c%s "$dest" 2>/dev/null || stat -f%z "$dest" 2>/dev/null || echo 0)"
print_ok "Downloaded $(format_bytes "$fsize")"
}
# Extract ZIP using Python zipfile (preferred), unzip, or Node.js adm-zip
extract_zip() {
local zip_path="$1" dest="$2" label="$3"
print_info "Extracting: $label"
mkdir -p "$dest"
# Prefer Python zipfile (most reliable, no host deps beyond python3)
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import zipfile, sys, os
with zipfile.ZipFile(sys.argv[1], 'r') as z:
for member in z.namelist():
z.extract(member, sys.argv[2])
" "$zip_path" "$dest" && return 0
fi
if command -v unzip >/dev/null 2>&1; then
unzip -o -q "$zip_path" -d "$dest" && return 0
fi
if command -v python >/dev/null 2>&1; then
python -c "import zipfile, sys; zipfile.ZipFile(sys.argv[1], 'r').extractall(sys.argv[2])" "$zip_path" "$dest" && return 0
fi
print_fail "No ZIP extractor available (tried python3, unzip, python)."
return 1
}
# Extract tar.xz
extract_tarxz() {
local tar_path="$1" dest="$2" label="$3"
print_info "Extracting: $label"
mkdir -p "$dest"
if [ "$USE_SYMLINKS" = false ]; then
# On filesystems that do not support symlinks, tar will fail when attempting to create symlinks
# (e.g. for npm/npx/corepack in Node.js bin/). We allow it to continue but verify key files.
local tar_exit=0
tar -xf "$tar_path" -C "$dest" || tar_exit=$?
if [[ $tar_exit -ne 0 ]]; then
# Check if node binary was extracted successfully
local check_file
check_file="$(find "$dest" -type f -name "node" | head -n 1)"
if [[ -n "$check_file" ]]; then
print_warn "tar returned exit code $tar_exit due to symlink failures on this filesystem, but bin/node was successfully extracted."
else
print_fail "tar extraction failed with exit code $tar_exit (could not find bin/node)"
exit $tar_exit
fi
fi
else
tar -xf "$tar_path" -C "$dest"
fi
print_ok "Extracted $label"
}
detect_gpu_vendor() {
local vendor=""
if [[ -d /sys/bus/pci/devices ]]; then
for vfile in /sys/bus/pci/devices/*/vendor; do
if [[ -f "$vfile" ]]; then
local vid
vid="$(cat "$vfile" 2>/dev/null || true)"
case "$vid" in
"0x10de") vendor="nvidia" ;;
"0x1002") vendor="amd" ;;
"0x8086") [[ -z "$vendor" ]] && vendor="intel" ;;
esac
fi
done
fi
echo "$vendor"
}
# Official Linux binaries are built on Ubuntu 24.04 and link against glibc 2.38+.
# This routine prints a warning on older distributions so users know upfront.
check_glibc() {
local required="2.38"
local current=""
if command -v ldd >/dev/null 2>&1; then
current="$(ldd --version 2>/dev/null | head -n1 | grep -oP '[0-9]+\.[0-9]+' | head -n1 || true)"
fi
if [[ -z "$current" ]]; then
print_warn "Could not detect glibc version. Prebuilt Linux backends require glibc $required+ (Ubuntu 24.04)."
return
fi
if [[ "$(printf '%s\n' "$required" "$current" | sort -V | head -n1)" != "$required" ]]; then
print_warn "Detected glibc $current. Prebuilt Linux backends require glibc $required+ (Ubuntu 24.04)."
print_info "You can still use the app, but the downloaded backends will not start on this system."
print_info "To fix: upgrade to Ubuntu 24.04+ or build stable-diffusion.cpp from source (see README)."
fi
}
copy_binaries_from_extracted() {
local extracted_dir="$1" dest_dir="$2" main_name="$3" server_name="$4"
while IFS= read -r -d '' f; do
local base
base="$(basename "$f")"
case "$base" in
sd|sd-cli) cp "$f" "$dest_dir/$main_name" ;;
sd-server) cp "$f" "$dest_dir/$server_name" ;;
esac
done < <(find "$extracted_dir" -type f \( -name "sd" -o -name "sd-cli" -o -name "sd-server" \) -print0 2>/dev/null)
# Copy any .so files
find "$extracted_dir" -type f -name "*.so" -exec cp {} "$dest_dir/" \; 2>/dev/null || true
}
copy_macos_backend_from_extracted() {
local extracted_dir="$1" dest_dir="$2"
local target="$dest_dir/sd"
local server_bin=""
local cli_bin=""
server_bin="$(find "$extracted_dir" -type f -name "sd-server" | head -n 1)"
cli_bin="$(find "$extracted_dir" -type f \( -name "sd" -o -name "sd-cli" \) | head -n 1)"
if [[ -n "$server_bin" ]]; then
cp "$server_bin" "$target"
elif [[ -n "$cli_bin" ]]; then
cp "$cli_bin" "$target"
else
print_fail "No sd-server, sd, or sd-cli binary was found in the macOS backend archive."
return 1
fi
find "$extracted_dir" -type f \( -name "*.dylib" -o -name "*.metallib" \) -exec cp {} "$dest_dir/" \; 2>/dev/null || true
chmod +x "$target" 2>/dev/null || true
}
# ════════════════════════════════════════════════════════════════════════════
print_header
if [[ "$PLATFORM" == "Linux" ]]; then
check_glibc
fi
TOTAL_STEPS=7
# ── Step 1: Portable Node.js ────────────────────────────────────────────────
print_step 1 $TOTAL_STEPS "Setting up portable Node.js ($NODE_DIR/)"
if [[ -x "$NODE_BIN" && -x "$NPM_BIN" ]]; then
VERSION=$("$NODE_BIN" --version)
print_ok "Portable Node.js already ready: $VERSION"
else
mkdir -p "$TOOLS_DIR"
NODE_TAR_PATH="$TOOLS_DIR/$NODE_TARBALL"
download_file "$NODE_URL" "$NODE_TAR_PATH" "Node.js v${NODE_VERSION} LTS (Portable tarball)"
extract_tarxz "$NODE_TAR_PATH" "$TOOLS_DIR" "Node.js"
rm -f "$NODE_TAR_PATH"
EXTRACTED_DIR="$(find "$TOOLS_DIR" -maxdepth 1 -type d -name "node-v*-${NODE_PLATFORM_ARCH}" | head -n 1)"
if [[ -d "$EXTRACTED_DIR" ]]; then
rm -rf "$NODE_DIR"
mv "$EXTRACTED_DIR" "$NODE_DIR"
fi
if [ "$USE_SYMLINKS" = false ]; then
print_info "Filesystem does not support symlinks. Creating shell wrappers for npm, npx, and corepack..."
rm -f "$NODE_DIR/bin/npm" "$NODE_DIR/bin/npx" "$NODE_DIR/bin/corepack"
cat << 'EOF' > "$NODE_DIR/bin/npm"
#!/bin/sh
basedir=$(dirname "$0")
exec "$basedir/node" "$basedir/../lib/node_modules/npm/bin/npm-cli.js" "$@"
EOF
chmod +x "$NODE_DIR/bin/npm"
cat << 'EOF' > "$NODE_DIR/bin/npx"
#!/bin/sh
basedir=$(dirname "$0")
exec "$basedir/node" "$basedir/../lib/node_modules/npm/bin/npx-cli.js" "$@"
EOF
chmod +x "$NODE_DIR/bin/npx"
cat << 'EOF' > "$NODE_DIR/bin/corepack"
#!/bin/sh
basedir=$(dirname "$0")
exec "$basedir/node" "$basedir/../lib/node_modules/corepack/dist/corepack.js" "$@"
EOF
chmod +x "$NODE_DIR/bin/corepack"
print_ok "Created shell wrappers."
fi
if [[ ! -x "$NODE_BIN" || ! -x "$NPM_BIN" ]]; then
print_fail "Portable Node.js install is incomplete."
exit 1
fi
VERSION=$("$NODE_BIN" --version)
print_ok "Portable Node.js ready: $VERSION"
fi
# ── Step 2: stable-diffusion.cpp Backends ───────────────────────────────────
mkdir -p "$BACKEND_DIR"
if [[ "$PLATFORM" == "Darwin" ]]; then
print_step 2 $TOTAL_STEPS "Setting up stable-diffusion.cpp Metal backend (app/backend/mac/)"
if [[ "$ARCH" != "arm64" ]]; then
print_fail "The official macOS backend binary is Apple Silicon only (arm64)."
print_info "macOS Intel hardware is completely unsupported and has not been tested."
exit 1
fi
MAC_BACKEND="$BACKEND_DIR/sd"
if [[ -x "$MAC_BACKEND" ]]; then
print_ok "macOS Metal backend already ready."
else
MAC_ZIP="$TOOLS_DIR/sd-mac-metal.zip"
download_file "$SD_BASE_URL/sd-master-${SD_SHORT_HASH}-bin-Darwin-macOS-15.7.7-arm64.zip" "$MAC_ZIP" "stable-diffusion.cpp Metal Backend (macOS arm64)"
extract_zip "$MAC_ZIP" "$BACKEND_DIR/extracted" "macOS Metal Backend"
rm -f "$MAC_ZIP"
copy_macos_backend_from_extracted "$BACKEND_DIR/extracted" "$BACKEND_DIR"
rm -rf "$BACKEND_DIR/extracted"
print_ok "macOS Metal backend installed."
fi
# CoreML NPU Environment setup (macOS Apple Silicon only)
print_info "Setting up CoreML Python virtual environment for Apple Silicon ANE (NPU)..."
VENV_DIR="$BACKEND_DIR/coreml_venv"
PYTHON_BIN="$VENV_DIR/bin/python"
if [[ ! -x "$PYTHON_BIN" ]]; then
print_info "Creating Python virtual environment at $VENV_DIR..."
if ! python3 -m venv "$VENV_DIR"; then
print_warn "Could not create the virtual environment for CoreML. CoreML NPU mode will be unavailable."
fi
fi
if [[ -x "$PYTHON_BIN" ]]; then
print_info "Installing CoreML dependencies (this may take a couple of minutes)..."
if "$PYTHON_BIN" -m pip install --upgrade pip >/dev/null 2>&1 && \
"$PYTHON_BIN" -m pip install numpy coremltools diffusers transformers huggingface-hub pillow >/dev/null 2>&1 && \
"$PYTHON_BIN" -m pip install "git+https://github.com/apple/ml-stable-diffusion.git" >/dev/null 2>&1; then
print_ok "CoreML ANE (NPU) environment ready."
else
print_warn "CoreML dependencies installation failed. CoreML NPU mode will be unavailable."
fi
fi
else
VENDOR="$(detect_gpu_vendor)"
print_step 2 $TOTAL_STEPS "Detecting GPU vendor: ${VENDOR:-none}"
# CPU backend (always)
CPU_BACKEND_DIR="$BACKEND_DIR/cpu"
if [[ ! -f "$CPU_BACKEND_DIR/sd-cpu" || ! -f "$CPU_BACKEND_DIR/sd-server-cpu" ]]; then
mkdir -p "$CPU_BACKEND_DIR"
CPU_ZIP="$TOOLS_DIR/sd-cpu.zip"
download_file "$SD_BASE_URL/sd-master-${SD_SHORT_HASH}-bin-Linux-Ubuntu-24.04-x86_64.zip" "$CPU_ZIP" "stable-diffusion.cpp CPU Backend (Linux x86_64)"
extract_zip "$CPU_ZIP" "$CPU_BACKEND_DIR/extracted" "CPU Backend"
rm -f "$CPU_ZIP"
copy_binaries_from_extracted "$CPU_BACKEND_DIR/extracted" "$CPU_BACKEND_DIR" "sd-cpu" "sd-server-cpu"
rm -rf "$CPU_BACKEND_DIR/extracted"
print_ok "CPU backend installed."
else
print_ok "CPU backend already ready."
fi
chmod +x "$CPU_BACKEND_DIR/sd-cpu" "$CPU_BACKEND_DIR/sd-server-cpu" 2>/dev/null || true
# Vulkan backend (always - cross-vendor GPU fallback)
VULKAN_BACKEND_DIR="$BACKEND_DIR/vulkan"
if [[ ! -f "$VULKAN_BACKEND_DIR/sd-vulkan" || ! -f "$VULKAN_BACKEND_DIR/sd-server-vulkan" ]]; then
mkdir -p "$VULKAN_BACKEND_DIR"
VULKAN_ZIP="$TOOLS_DIR/sd-vulkan.zip"
download_file "$SD_BASE_URL/sd-master-${SD_SHORT_HASH}-bin-Linux-Ubuntu-24.04-x86_64-vulkan.zip" "$VULKAN_ZIP" "stable-diffusion.cpp Vulkan Backend (Linux x86_64)"
extract_zip "$VULKAN_ZIP" "$VULKAN_BACKEND_DIR/extracted" "Vulkan Backend"
rm -f "$VULKAN_ZIP"
copy_binaries_from_extracted "$VULKAN_BACKEND_DIR/extracted" "$VULKAN_BACKEND_DIR" "sd-vulkan" "sd-server-vulkan"
rm -rf "$VULKAN_BACKEND_DIR/extracted"
print_ok "Vulkan backend installed."
else
print_ok "Vulkan backend already ready."
fi
chmod +x "$VULKAN_BACKEND_DIR/sd-vulkan" "$VULKAN_BACKEND_DIR/sd-server-vulkan" 2>/dev/null || true
# ROCm backend (optional --max-perf, or auto-detected AMD)
ROCM_BACKEND_DIR="$BACKEND_DIR/rocm"
if [[ $MAX_PERF -eq 1 ]] && [[ "$VENDOR" == "amd" || "$VENDOR" == "" ]]; then
if [[ ! -f "$ROCM_BACKEND_DIR/sd-rocm" || ! -f "$ROCM_BACKEND_DIR/sd-server-rocm" ]]; then
mkdir -p "$ROCM_BACKEND_DIR"
ROCM_ZIP="$TOOLS_DIR/sd-rocm.zip"
print_warn "ROCm backend is ~1.2 GB. This may take a while..."
download_file "$SD_BASE_URL/sd-master-${SD_SHORT_HASH}-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.13.0.zip" "$ROCM_ZIP" "stable-diffusion.cpp ROCm Backend (Linux x86_64)"
extract_zip "$ROCM_ZIP" "$ROCM_BACKEND_DIR/extracted" "ROCm Backend"
rm -f "$ROCM_ZIP"
copy_binaries_from_extracted "$ROCM_BACKEND_DIR/extracted" "$ROCM_BACKEND_DIR" "sd-rocm" "sd-server-rocm"
rm -rf "$ROCM_BACKEND_DIR/extracted"
print_ok "ROCm backend installed."
else
print_ok "ROCm backend already ready."
fi
chmod +x "$ROCM_BACKEND_DIR/sd-rocm" "$ROCM_BACKEND_DIR/sd-server-rocm" 2>/dev/null || true
fi
# CUDA backend on Linux: ask user, download prebuilt first, and compile from source as a fallback.
CUDA_BACKEND_DIR="$BACKEND_DIR/cuda"
if [[ "$VENDOR" == "nvidia" ]]; then
if [[ ! -f "$CUDA_BACKEND_DIR/sd-cuda" || ! -f "$CUDA_BACKEND_DIR/sd-server-cuda" ]]; then
echo ""
echo " ============================================================"
echo " NVIDIA GPU Detected"
echo " ============================================================"
echo " To get the best performance, you can use the CUDA backend."
echo " Setting this up can download a prebuilt binary or compile from"
echo " source (which takes 10-15 minutes)."
echo ""
echo " Alternatively, you can use the Vulkan backend which is already"
echo " installed and runs immediately (recommended for GTX cards)."
echo " ============================================================"
echo ""
CHOOSE_CUDA="n"
if [[ -t 0 ]]; then
read -t 30 -rp " Do you want to proceed with CUDA setup? [y/N]: " CHOOSE_CUDA || CHOOSE_CUDA="n"
else
print_info "Non-interactive environment detected; defaulting to Vulkan."
fi
if [[ "$CHOOSE_CUDA" =~ ^[Yy]$ ]]; then
TRY_DOWNLOAD=1
PREBUILT_URL="https://github.com/leaxer-ai/leaxer-stable-diffusion/releases/download/v0.1.0/sd-server-x86_64-unknown-linux-gnu-cuda"
PREBUILT_CLI_URL="https://github.com/leaxer-ai/leaxer-stable-diffusion/releases/download/v0.1.0/sd-x86_64-unknown-linux-gnu-cuda"
mkdir -p "$CUDA_BACKEND_DIR"
print_info "Attempting to download prebuilt CUDA binary..."
if download_file "$PREBUILT_URL" "$CUDA_BACKEND_DIR/sd-server-cuda" "Prebuilt Linux CUDA Server" && \
download_file "$PREBUILT_CLI_URL" "$CUDA_BACKEND_DIR/sd-cuda" "Prebuilt Linux CUDA CLI"; then
chmod +x "$CUDA_BACKEND_DIR/sd-server-cuda" "$CUDA_BACKEND_DIR/sd-cuda" 2>/dev/null || true
print_info "Testing downloaded prebuilt CUDA binary..."
if "$CUDA_BACKEND_DIR/sd-server-cuda" --help >/dev/null 2>&1; then
print_ok "Prebuilt CUDA binary verified and works! Skipping compilation."
TRY_DOWNLOAD=0
else
print_warn "Prebuilt CUDA binary failed verification test (missing libraries or library mismatch)."
rm -f "$CUDA_BACKEND_DIR/sd-server-cuda" "$CUDA_BACKEND_DIR/sd-cuda"
fi
else
print_warn "Failed to download prebuilt CUDA binary."
rm -f "$CUDA_BACKEND_DIR/sd-server-cuda" "$CUDA_BACKEND_DIR/sd-cuda"
fi
if [[ $TRY_DOWNLOAD -eq 1 ]]; then
if command -v nvcc >/dev/null 2>&1 && command -v cmake >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then
print_info "NVIDIA GPU and CUDA compilation tools (nvcc, cmake, git) detected."
print_info "Building CUDA backend from source..."
BUILD_DIR="$TOOLS_DIR/build-sd"
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
if [[ ! -d "$BUILD_DIR" ]]; then
print_info "Cloning stable-diffusion.cpp..."
git clone https://github.com/leejet/stable-diffusion.cpp.git "$BUILD_DIR"
fi
PUSHED_DIR="$(pwd)"
cd "$BUILD_DIR"
print_info "Checking out pinned tag $SD_RELEASE..."
git fetch origin
git checkout -f "$SD_RELEASE"
git submodule update --init --recursive
rm -rf build-cuda && mkdir build-cuda && cd build-cuda
print_info "Running cmake for CUDA backend..."
if cmake .. -DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA_FORCE_MMQ=ON && \
cmake --build . --config Release -j"$JOBS"; then
mkdir -p "$CUDA_BACKEND_DIR"
if [[ -f bin/sd-server ]]; then
cp bin/sd-server "$CUDA_BACKEND_DIR/sd-cuda"
cp bin/sd-server "$CUDA_BACKEND_DIR/sd-server-cuda"
else
print_fail "CUDA build succeeded but bin/sd-server was not found."
cd "$PUSHED_DIR"
exit 1
fi
if [[ -f bin/sd ]]; then
cp bin/sd "$CUDA_BACKEND_DIR/sd-cli-cuda"
elif [[ -f bin/sd-cli ]]; then
cp bin/sd-cli "$CUDA_BACKEND_DIR/sd-cli-cuda"
fi
SO_PATH_CUDA=$(find . -name "libstable-diffusion.so" | head -n 1)
if [[ -n "$SO_PATH_CUDA" ]]; then
cp "$SO_PATH_CUDA" "$CUDA_BACKEND_DIR/"
fi
chmod +x "$CUDA_BACKEND_DIR/sd-cuda" "$CUDA_BACKEND_DIR/sd-server-cuda" 2>/dev/null || true
if [[ -f "$CUDA_BACKEND_DIR/sd-cli-cuda" ]]; then
chmod +x "$CUDA_BACKEND_DIR/sd-cli-cuda" 2>/dev/null || true
fi
print_ok "CUDA backend compiled and installed successfully from source."
else
print_warn "CUDA backend build from source failed. Falling back to Vulkan."
fi
cd "$PUSHED_DIR"
else
print_warn "CUDA compilation tools (nvcc, cmake, and/or git) are missing."
print_info "To compile the CUDA backend, install the NVIDIA CUDA Toolkit, cmake, and git."
print_info "Falling back to Vulkan."
fi
fi
else
print_info "Declined CUDA setup. Using Vulkan GPU backend instead."
fi
else
print_ok "CUDA backend already ready."
fi
fi
fi
# ── Step 3: npm install ─────────────────────────────────────────────────────
print_step 3 $TOTAL_STEPS "Setting up llama.cpp text backend"
bash "$SCRIPT_DIR/setup-llama.sh"
print_step 4 $TOTAL_STEPS "Setting up whisper.cpp speech backend"
bash "$SCRIPT_DIR/setup-whisper.sh"
print_step 5 $TOTAL_STEPS "Setting up Kokoro ONNX text-to-speech runtime"
bash "$SCRIPT_DIR/setup-tts.sh"
print_step 6 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)"
if [[ ! -x "$NPM_BIN" ]]; then
print_fail "Portable npm was not found at $NPM_BIN"
exit 1
fi
# Ensure correct OS-specific node_modules folder is symlinked or swapped to avoid conflicts
FRONTEND_NODE_MODULES="$FRONTEND_DIR/node_modules"
ACTIVE_OS_FILE="$FRONTEND_DIR/.active_modules_os"
if [[ "$PLATFORM" == "Darwin" ]]; then
OS_NODE_MODULES="$FRONTEND_DIR/node_modules_mac"
OS_LABEL="node_modules_mac"
CURRENT_OS="mac"
else
OS_NODE_MODULES="$FRONTEND_DIR/node_modules_linux"
OS_LABEL="node_modules_linux"
CURRENT_OS="linux"
fi
# Attempt to create a test symlink to check if filesystem supports symlinks
USE_SYMLINKS=true
TEST_LINK="$FRONTEND_DIR/.test_symlink"
rm -f "$TEST_LINK"
if ln -s "$OS_LABEL" "$TEST_LINK" 2>/dev/null; then
rm -f "$TEST_LINK"
else
USE_SYMLINKS=false
fi
if [ "$USE_SYMLINKS" = true ]; then
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
print_info "Migrating existing node_modules to $OS_LABEL..."
if [[ -d "$OS_NODE_MODULES" ]]; then
rm -rf "$FRONTEND_NODE_MODULES"
else
mv "$FRONTEND_NODE_MODULES" "$OS_NODE_MODULES"
fi
fi
rm -rf "$FRONTEND_NODE_MODULES"
mkdir -p "$OS_NODE_MODULES"
ln -sf "$OS_LABEL" "$FRONTEND_NODE_MODULES"
else
# Fallback: Filesystem does not support symlinks (e.g. FAT32/exFAT)
print_info "Filesystem does not support symlinks. Using directory swapping fallback..."
if [[ -L "$FRONTEND_NODE_MODULES" || -f "$FRONTEND_NODE_MODULES" ]]; then
rm -rf "$FRONTEND_NODE_MODULES"
fi
PREV_OS=""
if [[ -f "$ACTIVE_OS_FILE" ]]; then
PREV_OS=$(cat "$ACTIVE_OS_FILE")
fi
if [[ -d "$FRONTEND_NODE_MODULES" && "$PREV_OS" != "$CURRENT_OS" ]]; then
if [[ -n "$PREV_OS" ]]; then
print_info "Swapping out node_modules to node_modules_$PREV_OS..."
rm -rf "$FRONTEND_DIR/node_modules_$PREV_OS"
mv "$FRONTEND_NODE_MODULES" "$FRONTEND_DIR/node_modules_$PREV_OS"
else
print_info "Saving node_modules as node_modules_windows..."
rm -rf "$FRONTEND_DIR/node_modules_windows"
mv "$FRONTEND_NODE_MODULES" "$FRONTEND_DIR/node_modules_windows"
fi
fi
if [[ -d "$OS_NODE_MODULES" && ! -d "$FRONTEND_NODE_MODULES" ]]; then
print_info "Swapping in $OS_LABEL..."
mv "$OS_NODE_MODULES" "$FRONTEND_NODE_MODULES"
elif [[ ! -d "$FRONTEND_NODE_MODULES" ]]; then
mkdir -p "$FRONTEND_NODE_MODULES"
fi
echo "$CURRENT_OS" > "$ACTIVE_OS_FILE"
fi
cd "$FRONTEND_DIR"
export PATH="$NODE_DIR/bin:$PATH"
if [ "$USE_SYMLINKS" = false ]; then
if "$NPM_BIN" install --prefer-offline --no-bin-links; then
print_ok "Dependencies installed!"
else
print_fail "npm install failed."
exit 1
fi
else
if "$NPM_BIN" install --prefer-offline; then
print_ok "Dependencies installed!"
else
print_fail "npm install failed."
exit 1
fi
fi
# ── Step 4: Build frontend ──────────────────────────────────────────────────
print_step 7 $TOTAL_STEPS "Building frontend -> app/dist/"
if [ "$USE_SYMLINKS" = false ]; then
# If symlinks are disabled, run vite directly using the local node executable.
if "$NODE_BIN" node_modules/vite/bin/vite.js build; then
print_ok "Frontend built!"
else
print_fail "Frontend build failed."
exit 1
fi
else
if "$NPM_BIN" run build; then
print_ok "Frontend built!"
else
print_fail "Frontend build failed."
exit 1
fi
fi
# ── Done ────────────────────────────────────────────────────────────────────
echo ""
echo " ============================================================"
if [[ "$PLATFORM" == "Darwin" ]]; then
echo " Setup complete! Run ./mac.sh to launch."
else
echo " Setup complete! Run ./linux.sh to launch."
fi
echo " ============================================================"
echo ""
if [[ "$PLATFORM" == "Linux" && $MAX_PERF -eq 0 ]] && [[ "$VENDOR" == "nvidia" || "$VENDOR" == "amd" ]]; then
echo " Tip: For maximum GPU performance, re-run with:"
echo " ./scripts/setup/setup.sh --max-perf"
echo ""
fi
+306
View File
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
import argparse
import base64
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import numpy as np
import torch
from python_coreml_stable_diffusion.pipeline import (
StableDiffusionPipeline,
StableDiffusionXLPipeline,
get_coreml_pipe,
SCHEDULER_MAP
)
MODEL_VERSION_BY_NAME = {
"coreml-stable-diffusion-v1-5": "runwayml/stable-diffusion-v1-5",
"stable-diffusion-v1-5": "runwayml/stable-diffusion-v1-5",
"stable-diffusion-v1-4": "CompVis/stable-diffusion-v1-4",
"cyberrealistic": "runwayml/stable-diffusion-v1-5",
}
def get_scheduler_class(name: str):
name = str(name).lower().replace(" ", "").replace("_", "").replace("-", "")
if "eulerancestral" in name or "eulera" in name:
return SCHEDULER_MAP.get("EulerAncestralDiscrete")
if "euler" in name:
return SCHEDULER_MAP.get("EulerDiscrete")
if "dpm" in name:
return SCHEDULER_MAP.get("DPMSolverMultistep")
if "ddim" in name:
return SCHEDULER_MAP.get("DDIM")
if "lms" in name:
return SCHEDULER_MAP.get("LMSDiscrete")
if "pndm" in name:
return SCHEDULER_MAP.get("PNDM")
return None
def json_response(handler, code, payload):
body = json.dumps(payload).encode("utf-8")
handler.send_response(code)
handler.send_header("Content-Type", "application/json")
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
def find_coreml_resource_dir(model_path: Path) -> Path:
candidates = [
model_path,
model_path / "Resources",
model_path / "compiled",
model_path / "packages",
model_path / "split_einsum_v2" / "compiled",
model_path / "split_einsum" / "compiled",
model_path / "original" / "compiled",
model_path / "split_einsum_v2" / "packages",
model_path / "split_einsum" / "packages",
model_path / "original" / "packages",
]
required_names = {
"text_encoder.mlmodelc",
"text_encoder.mlpackage",
"textencoder.mlmodelc",
"textencoder.mlpackage",
}
for candidate in candidates:
if not candidate.is_dir():
continue
names = {entry.name.lower() for entry in candidate.iterdir()}
has_text_encoder = bool(names & required_names)
has_unet = "unet.mlmodelc" in names or "unet.mlpackage" in names
if has_text_encoder and has_unet:
return candidate
for candidate in model_path.rglob("*"):
if not candidate.is_dir():
continue
names = {entry.name.lower() for entry in candidate.iterdir()}
has_text_encoder = bool(names & required_names)
has_unet = "unet.mlmodelc" in names or "unet.mlpackage" in names
if has_text_encoder and has_unet:
return candidate
raise FileNotFoundError(
f"Could not find CoreML resources under {model_path}. "
"Expected a folder containing text_encoder and unet .mlmodelc/.mlpackage files."
)
def infer_model_version(model_path: Path) -> str:
override = os.environ.get("COREML_MODEL_VERSION")
if override:
return override
lower = model_path.name.lower()
for needle, version in MODEL_VERSION_BY_NAME.items():
if needle in lower:
return version
return "runwayml/stable-diffusion-v1-5"
def infer_model_sources(resource_dir: Path) -> str:
names = {entry.name.lower() for entry in resource_dir.iterdir()}
if any(name.endswith(".mlmodelc") for name in names):
return "compiled"
return "packages"
def has_coreml_safety_checker(resource_dir: Path, sources: str, model_version: str) -> bool:
names = {entry.name.lower() for entry in resource_dir.iterdir()}
if sources == "compiled":
return "safetychecker.mlmodelc" in names
package_name = f"Stable_Diffusion_version_{model_version}_safety_checker.mlpackage".replace("/", "_").lower()
return package_name in names
def latest_png(root: Path) -> Path:
images = sorted(root.rglob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
if not images:
raise FileNotFoundError("CoreML generation finished but did not write a PNG output.")
return images[0]
class CoreMLServerState:
def __init__(self, model: Path, steps: int, cfg_scale: float):
self.model = model
self.resources = find_coreml_resource_dir(model)
self.model_version = infer_model_version(model)
self.steps = steps
self.cfg_scale = cfg_scale
self.started_at = time.time()
self.lock = threading.Lock()
# Load CoreML pipeline components on startup
print(f"[coreml-npu] Loading PyTorch reference configuration for model version: {self.model_version}", flush=True)
SDP = StableDiffusionXLPipeline if 'xl' in self.model_version else StableDiffusionPipeline
pytorch_pipe = SDP.from_pretrained(
self.model_version,
use_auth_token=True,
)
compute_unit = os.environ.get("COREML_COMPUTE_UNIT", "CPU_AND_NE")
sources = infer_model_sources(self.resources)
if not has_coreml_safety_checker(self.resources, sources, self.model_version):
print("[coreml-npu] Safety checker Core ML model not found; running without safety checker.", flush=True)
pytorch_pipe.safety_checker = None
print(f"[coreml-npu] Loading Core ML models from: {self.resources} (compute unit: {compute_unit}, sources: {sources})", flush=True)
self.pipe = get_coreml_pipe(
pytorch_pipe=pytorch_pipe,
mlpackages_dir=self.resources,
model_version=self.model_version,
compute_unit=compute_unit,
sources=sources
)
print("[coreml-npu] Core ML models loaded successfully.", flush=True)
def make_handler(state: CoreMLServerState):
class Handler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
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):
if self.path == "/health":
json_response(self, 200, {
"ok": True,
"ready": True,
"model": str(state.model),
"resources": str(state.resources),
"model_version": state.model_version,
"uptime_sec": round(time.time() - state.started_at, 1),
})
return
if self.path == "/v1/models":
json_response(self, 200, {
"object": "list",
"data": [{
"id": state.model.name,
"object": "model",
"owned_by": "local-coreml",
}],
})
return
json_response(self, 404, {"ok": False, "error": "Unknown endpoint"})
def do_POST(self):
if self.path != "/v1/images/generations":
json_response(self, 404, {"ok": False, "error": "Unknown endpoint"})
return
try:
if not hasattr(state, "pipe") or not state.pipe:
raise RuntimeError("CoreML pipeline is not initialized or failed to load.")
length = int(self.headers.get("Content-Length", "0") or "0")
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
prompt = str(payload.get("prompt") or "").strip()
if not prompt:
raise ValueError("Prompt is required")
size = str(payload.get("size") or "512x512").lower().split("x")
if len(size) == 2 and size != ["512", "512"]:
raise ValueError("CoreML models in this app currently generate at their compiled resolution, normally 512x512.")
steps = max(1, int(payload.get("steps") or state.steps or 30))
guidance = float(payload.get("cfg_scale") or state.cfg_scale or 7.0)
if guidance <= 1.0:
print("[coreml-npu] CFG scale <= 1 can disable classifier-free guidance, but this compiled UNet expects CFG batch shape. Using cfg_scale=7.0.", flush=True)
guidance = 7.0
seed = int(payload.get("seed")) if payload.get("seed") not in (None, -1) else int(time.time_ns() % (2 ** 32))
negative_prompt = str(payload.get("negative_prompt") or "").strip()
started = time.time()
# Dynamic sampler / scheduler swap
sampler = payload.get("sample_method") or payload.get("sampler") or payload.get("sampler_name")
if sampler and hasattr(state, "pipe") and state.pipe:
scheduler_cls = get_scheduler_class(sampler)
if scheduler_cls is not None and state.pipe.scheduler.__class__ != scheduler_cls:
print(f"[coreml-npu] Switching scheduler to {scheduler_cls.__name__}", flush=True)
state.pipe.scheduler = scheduler_cls.from_config(state.pipe.scheduler.config)
print("[coreml-npu] generating image", flush=True)
np.random.seed(seed)
# Thread safe execution of the in-memory pipeline
with state.lock:
output = state.pipe(
prompt=prompt,
height=state.pipe.height,
width=state.pipe.width,
num_inference_steps=steps,
guidance_scale=guidance,
negative_prompt=negative_prompt if negative_prompt else None,
)
image = output.images[0]
# Convert to base64 PNG
import io
buffered = io.BytesIO()
image.save(buffered, format="PNG")
encoded = base64.b64encode(buffered.getvalue()).decode("ascii")
print("[coreml-npu] decoding complete", flush=True)
json_response(self, 200, {
"created": int(time.time()),
"data": [{
"b64_json": encoded,
"seed": seed,
}],
"duration_sec": round(time.time() - started, 2),
})
except Exception as exc:
import traceback
traceback.print_exc()
json_response(self, 500, {"ok": False, "error": str(exc)})
def log_message(self, fmt, *args):
print("[coreml-npu-http] " + fmt % args, flush=True)
return Handler
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--listen-port", required=True, type=int)
parser.add_argument("--model", required=True, type=Path)
parser.add_argument("--steps", type=int, default=30)
parser.add_argument("--cfg-scale", type=float, default=7.0)
args = parser.parse_args()
state = CoreMLServerState(args.model, args.steps, args.cfg_scale)
print(f"[coreml-npu] Model: {state.model}", flush=True)
print(f"[coreml-npu] Resources: {state.resources}", flush=True)
print(f"[coreml-npu] Model version: {state.model_version}", flush=True)
print("| 1/1 - CoreML resources ready", flush=True)
server = ThreadingHTTPServer(("127.0.0.1", args.listen_port), make_handler(state))
print(f"[coreml-npu] listening on http://127.0.0.1:{args.listen_port}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()
+187
View File
@@ -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()
+59
View File
@@ -0,0 +1,59 @@
import { createRequire } from "node:module";
import fs from "node:fs/promises";
import path from "node:path";
const require = createRequire(import.meta.url);
function readStdin() {
return new Promise((resolve, reject) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { data += chunk; });
process.stdin.on("end", () => resolve(data));
process.stdin.on("error", reject);
});
}
function resolveRuntimePackage(name) {
const runtime = process.env.TTS_RUNTIME || "";
if (runtime) {
const pkg = path.join(runtime, "node_modules", name, "dist", "kokoro.js");
return pathToFileUrl(pkg);
}
return name;
}
function pathToFileUrl(filePath) {
const resolved = path.resolve(filePath).replace(/\\/g, "/");
return `file:///${resolved.replace(/^\/+/, "")}`;
}
async function main() {
const payload = JSON.parse(await readStdin());
process.env.TRANSFORMERS_CACHE = payload.cacheDir || process.env.TRANSFORMERS_CACHE || "";
process.env.HF_HOME = payload.cacheDir || process.env.HF_HOME || "";
const kokoro = await import(resolveRuntimePackage("kokoro-js"));
const { KokoroTTS } = kokoro;
const tts = await KokoroTTS.from_pretrained(payload.modelId, {
dtype: payload.dtype || "q8",
device: "cpu",
});
const audio = await tts.generate(payload.text, {
voice: payload.voice || "af_heart",
speed: Number(payload.speed) || 1,
});
await fs.mkdir(path.dirname(payload.output), { recursive: true });
audio.save(payload.output);
process.stdout.write(JSON.stringify({
ok: true,
output: payload.output,
sampleRate: audio.sampling_rate || audio.sampleRate || 24000,
}));
}
main().catch((err) => {
process.stderr.write(err?.stack || err?.message || String(err));
process.exit(1);
});
+41 -25
View File
@@ -1,16 +1,24 @@
@echo off
title Local AI Image Generator
title Uncensored AI Studio
cd /d "%~dp0"
set APP=%~dp0app
set NODE=%APP%\tools\node-win\node.exe
set NPM=%APP%\tools\node-win\npm.cmd
set DIST=%APP%\dist\index.html
set SETUP=%~dp0scripts\setup.ps1
set SETUP=%~dp0scripts\setup\setup.ps1
set CUDA_BACKEND=%APP%\backend\win\cuda\sd-cuda.exe
set VULKAN_BACKEND=%APP%\backend\win\vulkan\sd-vulkan.exe
set SERVE=%~dp0scripts\serve.cjs
set LLM_CUDA_BACKEND=%APP%\llm-backend\win\cuda\llama-server.exe
set LLM_HIP_BACKEND=%APP%\llm-backend\win\hip\llama-server.exe
set LLM_VULKAN_BACKEND=%APP%\llm-backend\win\vulkan\llama-server.exe
set LLM_SYCL_BACKEND=%APP%\llm-backend\win\sycl\llama-server.exe
set LLM_CPU_BACKEND=%APP%\llm-backend\win\cpu\llama-server.exe
set SPEECH_BACKEND=%APP%\speech-backend\win\cpu\whisper-cli.exe
set TTS_RUNTIME=%APP%\tts-runtime\node_modules\kokoro-js
set SERVE=%~dp0scripts\server\serve.cjs
if "%FRONTEND_PORT%"=="" set FRONTEND_PORT=1420
if "%LLM_PORT%"=="" set LLM_PORT=10086
set SETUP_REASON=
set SETUP_MODE=Repair
@@ -28,6 +36,18 @@ if not exist "%DIST%" (
set SETUP_REASON=Frontend build is missing.
goto :run_setup
)
if not exist "%LLM_CUDA_BACKEND%" if not exist "%LLM_HIP_BACKEND%" if not exist "%LLM_VULKAN_BACKEND%" if not exist "%LLM_SYCL_BACKEND%" if not exist "%LLM_CPU_BACKEND%" (
set SETUP_REASON=llama.cpp text backend is missing.
goto :run_setup
)
if not exist "%SPEECH_BACKEND%" (
set SETUP_REASON=whisper.cpp speech backend is missing.
goto :run_setup
)
if not exist "%TTS_RUNTIME%" (
set SETUP_REASON=Kokoro text-to-speech runtime is missing.
goto :run_setup
)
if exist "%CUDA_BACKEND%" goto :launch
if exist "%VULKAN_BACKEND%" goto :launch
set SETUP_REASON=No backend binary is installed.
@@ -36,13 +56,13 @@ goto :run_setup
:run_setup
echo.
echo ============================================================
echo LOCAL AI IMAGE GENERATOR ^| %SETUP_MODE%
echo UNCENSORED AI STUDIO ^| %SETUP_MODE%
echo ============================================================
echo.
if "%SETUP_MODE%"=="First-Time Setup" (
echo This looks like your first run. Setting up automatically...
) else (
echo Local AI Image Generator needs a quick repair before launch.
echo Uncensored AI Studio needs a quick repair before launch.
)
if not "%SETUP_REASON%"=="" echo Reason: %SETUP_REASON%
echo Models are not downloaded during setup. Download or import them in the app.
@@ -50,8 +70,10 @@ echo.
echo Press any key to continue, or Ctrl+C to cancel.
pause >nul
:: Clear old frontend server process before setup so app/tools/node-win can be replaced
:: Clear old frontend and backend server processes before setup so app/tools/node-win can be replaced
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%FRONTEND_PORT% "') do taskkill /f /pid %%a >nul 2>nul
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":8080 "') do taskkill /f /pid %%a >nul 2>nul
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%LLM_PORT% "') do taskkill /f /pid %%a >nul 2>nul
powershell -ExecutionPolicy Bypass -File "%SETUP%"
if errorlevel 1 (
@@ -66,40 +88,34 @@ goto :launch
:: ── Launch ────────────────────────────────────────────────────────────────────
:launch
cls
echo.
echo ============================================================
echo LOCAL AI IMAGE GENERATOR ^| Launching...
echo UNCENSORED AI STUDIO ^| Launching...
echo ============================================================
echo.
:: Clear only the frontend port. The backend auto-selects a free port.
echo Clearing frontend port %FRONTEND_PORT%...
:: Clear frontend and backend ports to prevent address conflicts.
echo Clearing frontend port %FRONTEND_PORT%, backend port 8080, and text port %LLM_PORT%...
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%FRONTEND_PORT% "') do taskkill /f /pid %%a >nul 2>nul
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":8080 "') do taskkill /f /pid %%a >nul 2>nul
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%LLM_PORT% "') do taskkill /f /pid %%a >nul 2>nul
:: Start frontend server + backend manager (serve.cjs manages sd-vulkan.exe)
echo Starting Local AI Image Generator...
start "SD-Server" /min "%NODE%" "%SERVE%"
:: Wait for server to bind
timeout /t 2 >nul
:: Open browser
echo Opening browser at http://localhost:%FRONTEND_PORT%
start http://localhost:%FRONTEND_PORT%
echo Starting Uncensored AI Studio...
echo Opening browser at http://localhost:%FRONTEND_PORT%...
start /b cmd /c "timeout /t 2 >nul && start http://localhost:%FRONTEND_PORT%"
echo.
echo ============================================================
echo Running!
echo Web UI: http://localhost:%FRONTEND_PORT%
echo GPU API: Auto-selected by the app (starts at 8080)
echo Text API: Starts when a GGUF model is loaded (port %LLM_PORT%)
echo Speech: Managed locally by the app
echo TTS: Managed locally by the app
echo.
echo Close this window to stop all services.
echo Press Ctrl+C in this window to stop all services.
echo ============================================================
echo.
pause >nul
:: Cleanup on exit
echo Shutting down...
taskkill /fi "WINDOWTITLE eq SD-Server*" /f >nul 2>nul
echo Done. Goodbye!
"%NODE%" "%SERVE%"