diff --git a/README.md b/README.md index fa75ae6..cb57220 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🖼️ Local AI Image Generator -### An easy, zero-setup Stable Diffusion GUI for Windows and Linux. Run GGUF & Safetensors models offline without Python configuration. +### An easy, zero-setup Stable Diffusion GUI for Windows, Linux, and macOS. Run GGUF & Safetensors models offline without Python configuration. @@ -22,7 +22,7 @@ --- ## 📖 Overview -**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows and Linux. Double-clicking `start.bat` (Windows) or running `./start.sh` (Linux) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace. +**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows, Linux, and macOS. Double-clicking `start.bat` (Windows) or running `./start.sh` (Linux/macOS) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace. --- @@ -40,11 +40,17 @@ 3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). 4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. +### macOS +1. **Check compatibility:** The prebuilt macOS backend is for **Apple Silicon (M1 or newer)** and uses **Metal** GPU acceleration. +2. **Launch:** Open Terminal in the project folder and run **`./start.sh`** (downloads portable Node.js and the pre-compiled Metal backend on first run). +3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI). +4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt. + --- ## ✨ Features * **100% Offline & Private:** Inference runs completely locally on your hardware. -* **Auto-Detected GPU Acceleration:** Configures **CUDA** for Nvidia cards, **ROCm** for AMD cards, and **Vulkan** for AMD/Intel/NVIDIA fallback. +* **Auto-Detected GPU Acceleration:** Configures **CUDA** for Nvidia cards, **ROCm** for AMD cards, **Vulkan** for AMD/Intel/NVIDIA fallback, and **Metal** for Apple Silicon Macs. * **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. @@ -56,16 +62,16 @@ ``` local-ai-image-generator/ ├── start.bat # Main double-click entrypoint (Windows) -├── start.sh # Main terminal entrypoint (Linux) +├── start.sh # Main terminal entrypoint (Linux/macOS) ├── PLAN.md # Linux port implementation plan ├── LICENSE # MIT Open Source license ├── .gitignore ├── README.md ├── scripts/ │ ├── setup.ps1 # Automated GPU-detect and environment installer (Windows) -│ ├── setup.sh # Automated GPU-detect and environment installer (Linux) +│ ├── setup.sh # Automated GPU-detect and environment installer (Linux/macOS) │ ├── reset.ps1 # Cleans runtime environments (Windows) -│ ├── reset.sh # Cleans runtime environments (Linux) +│ ├── reset.sh # Cleans runtime environments (Linux/macOS) │ └── serve.cjs # UI web server and backend lifecycle manager └── app/ ├── frontend/ # UI source code (Vite + React) @@ -95,9 +101,17 @@ local-ai-image-generator/ | **Intel Arc / integrated** | Vulkan | CPU | Cross-vendor Vulkan support. | | **Integrated / None** | CPU | — | Runs on logical CPU threads (slow). | +### macOS + +| Hardware | Primary | Fallback | Notes | +| :--- | :--- | :--- | :--- | +| **Apple Silicon (M1 or newer)** | Metal | CPU | Uses the official Darwin arm64 stable-diffusion.cpp backend. | +| **Intel Mac** | Source build required | CPU | Official prebuilt macOS backend is Apple Silicon only. | + **System requirements:** - **glibc 2.38 or newer** is required for the prebuilt Linux backends (Ubuntu 24.04, Fedora 40+, etc.). - The setup script will warn you if your glibc is older. You can still run setup, but the prebuilt backends will not start. +- **Apple Silicon (M1 or newer)** is required for the prebuilt macOS Metal backend. **Linux setup modes:** - **Default (`./start.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB). @@ -118,7 +132,7 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution --- ## 🛠️ Troubleshooting -* **Reset Environment:** If a build fails or you want to clear dependencies, run `scripts/reset.ps1` (Windows) or `scripts/reset.sh` (Linux). (This preserves your models and generated images). +* **Reset Environment:** If a build fails or you want to clear dependencies, run `scripts/reset.ps1` (Windows) or `scripts/reset.sh` (Linux/macOS). (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. * **Linux backends fail to start with `GLIBC_2.38' not found`:** The prebuilt binaries require glibc 2.38+ (Ubuntu 24.04). Upgrade your distribution or build stable-diffusion.cpp from source (see below). * **Linux ROCm not loading:** Make sure your AMD GPU and kernel are compatible with ROCm 7.13. The app will automatically fall back to Vulkan if ROCm cannot initialize. @@ -130,11 +144,14 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution If your distribution has an older glibc than 2.38, or you want a CUDA backend on Linux, you can build `stable-diffusion.cpp` directly on your machine. The resulting binary will be linked against your system's glibc and will not have the compatibility issues of the prebuilt releases. +For macOS, the included `scripts/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 @@ -157,8 +174,11 @@ 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$(nproc) +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/Local-AI-Image-Generator/app/backend/linux// diff --git a/app/dist/index.html b/app/dist/index.html index eee907d..cdd6929 100644 --- a/app/dist/index.html +++ b/app/dist/index.html @@ -1,18 +1,18 @@ - - - - - - - - - - Local AI Image Generator + + + + + + + + + + Local AI Image Generator - - - -
- - + + + +
+ + diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index 6b97d45..9e49789 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -1554,9 +1554,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.369", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.369.tgz", - "integrity": "sha512-XM22K9FNaaCOvMMrBn1caIc8v0g6+pKt660ZbfQqUZvfil0hEzr8ZoiY7VcSLGM3L/x3rz5PqZrk+bKOOmVM9w==", + "version": "1.5.370", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.370.tgz", + "integrity": "sha512-D5tSHJReAb/Kf3Hu9F/GO4lJuSWzEWHwvQ/kKSUP7pimNgvxkSKj+gUQhHpKKACwrin7rS3byU7IxreF56rl5g==", "dev": true, "license": "ISC" }, diff --git a/scripts/build_from_source.sh b/scripts/build_from_source.sh index f2e3786..6b39a34 100755 --- a/scripts/build_from_source.sh +++ b/scripts/build_from_source.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # Local AI Image Generator - Compile backends from source -# For systems with GLIBC < 2.38 (e.g. Ubuntu 22.04) +# For Linux systems with GLIBC < 2.38 or macOS systems that need a local Metal build. # set -euo pipefail @@ -9,8 +9,14 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" APP_DIR="$ROOT_DIR/app" -BACKEND_DIR="$APP_DIR/backend/linux" +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" @@ -35,12 +41,36 @@ 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$(nproc) +cmake --build . --config Release -j"$JOBS" echo "Copying CPU binaries..." mkdir -p "$BACKEND_DIR/cpu" @@ -71,7 +101,7 @@ 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$(nproc); then + cmake --build . --config Release -j"$JOBS"; then echo "Copying Vulkan binaries..." mkdir -p "$BACKEND_DIR/vulkan" @@ -102,7 +132,7 @@ 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 -cmake --build . --config Release -j$(nproc) +cmake --build . --config Release -j"$JOBS" echo "Copying CUDA binaries..." mkdir -p "$BACKEND_DIR/cuda" diff --git a/scripts/reset.sh b/scripts/reset.sh index 81995e1..ca27b07 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Local AI Image Generator - Linux Reset Script +# Local AI Image Generator - Linux/macOS Reset Script # Resets portable app dependencies/builds while preserving user models and outputs. # diff --git a/scripts/serve.cjs b/scripts/serve.cjs index c63775e..2c75f5f 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -28,10 +28,10 @@ const BACKEND_PATHS = { cuda: path.join(ROOT, "app", "backend", "win", "cuda", "sd-cuda.exe"), vulkan: path.join(ROOT, "app", "backend", "win", "vulkan", "sd-vulkan.exe"), mac: path.join(ROOT, "app", "backend", "mac", "sd"), - linuxCpu: path.join(ROOT, "app", "backend", "linux", "cpu", "sd-cpu"), - linuxVulkan: path.join(ROOT, "app", "backend", "linux", "vulkan", "sd-vulkan"), - linuxRocm: path.join(ROOT, "app", "backend", "linux", "rocm", "sd-rocm"), - linuxCuda: path.join(ROOT, "app", "backend", "linux", "cuda", "sd-cuda"), + linuxCpu: path.join(ROOT, "app", "backend", "linux", "cpu", "sd-server-cpu"), + linuxVulkan: path.join(ROOT, "app", "backend", "linux", "vulkan", "sd-server-vulkan"), + linuxRocm: path.join(ROOT, "app", "backend", "linux", "rocm", "sd-server-rocm"), + linuxCuda: path.join(ROOT, "app", "backend", "linux", "cuda", "sd-server-cuda"), }; let BACKEND_PATH = ""; if (osPlatform === "win32") { @@ -188,6 +188,21 @@ function getGpuInfo() { } } + if (osPlatform === "darwin") { + try { + const output = execSync( + "system_profiler SPDisplaysDataType | awk -F': ' '/Chipset Model|Vendor/ {print $2; exit}'", + { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }, + ).trim(); + if (output) { + cachedGpuInfo = { name: output }; + return cachedGpuInfo; + } + } catch (_) {} + cachedGpuInfo = { name: "Apple Metal GPU" }; + return cachedGpuInfo; + } + cachedGpuInfo = { name: "Unavailable" }; return cachedGpuInfo; } @@ -588,8 +603,11 @@ function getBackendOptions() { const rocmInstalled = osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxRocm); const rocmAvailable = rocmInstalled && hasAmdGpu() && backendAccepts(BACKEND_PATHS.linuxRocm, "rocm"); const cpuInstalled = osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxCpu); + const metalInstalled = osPlatform === "darwin" && fs.existsSync(BACKEND_PATHS.mac); + const metalAvailable = metalInstalled && backendAccepts(BACKEND_PATHS.mac, "metal"); const options = [{ id: "cpu", label: "CPU", available: true }]; + if (metalAvailable) options.push({ id: "metal", label: "Metal GPU", available: true }); if (vulkanAvailable) options.push({ id: "vulkan", label: "Vulkan GPU", available: true }); if (rocmAvailable) options.push({ id: "rocm", label: "ROCm GPU (AMD)", available: true }); if (cudaAvailable) options.push({ id: "cuda", label: "CUDA GPU", available: true }); @@ -604,9 +622,14 @@ function getBackendOptions() { if (rocmInstalled && !rocmAvailable) { unavailable.push({ id: "rocm", label: "ROCm GPU (AMD)", reason: "Installed, but ROCm backend validation failed." }); } + if (metalInstalled && !metalAvailable) { + unavailable.push({ id: "metal", label: "Metal GPU", reason: "Installed, but Metal backend validation failed." }); + } let defaultBackend = "cpu"; - if (cudaAvailable) { + if (metalAvailable) { + defaultBackend = "metal"; + } else if (cudaAvailable) { const gpuName = String(getGpuInfo().name).toLowerCase(); const isGtxCard = gpuName.includes("gtx"); if (isGtxCard && vulkanAvailable) { @@ -626,6 +649,7 @@ function getBackendOptions() { cudaAvailable, vulkanAvailable, rocmAvailable, + metalAvailable, defaultBackendType: defaultBackend, }; return cachedBackendOptions; @@ -640,7 +664,17 @@ function backendAccepts(binaryPath, backendName) { "--model", path.join(MODELS, "__backend_probe_missing__.safetensors"), "--listen-port", "18082", ]; - let result = spawnSync(binaryPath, probeArgs, { encoding: "utf8", timeout: 5000 }); + const spawnEnv = { ...process.env }; + if (osPlatform === "linux") { + const dir = path.dirname(binaryPath); + const existing = spawnEnv.LD_LIBRARY_PATH || ""; + spawnEnv.LD_LIBRARY_PATH = dir + (existing ? ":" + existing : ""); + } else if (osPlatform === "darwin") { + const dir = path.dirname(binaryPath); + const existing = spawnEnv.DYLD_LIBRARY_PATH || ""; + spawnEnv.DYLD_LIBRARY_PATH = dir + (existing ? ":" + existing : ""); + } + let result = spawnSync(binaryPath, probeArgs, { encoding: "utf8", timeout: 5000, env: spawnEnv }); let output = `${result.stdout || ""}\n${result.stderr || ""}`; // Some binaries do not support --backend. If we see "unknown argument", @@ -650,7 +684,7 @@ function backendAccepts(binaryPath, backendName) { "--model", path.join(MODELS, "__backend_probe_missing__.safetensors"), "--listen-port", "18082", ]; - result = spawnSync(binaryPath, fallbackArgs, { encoding: "utf8", timeout: 5000 }); + result = spawnSync(binaryPath, fallbackArgs, { encoding: "utf8", timeout: 5000, env: spawnEnv }); output = `${result.stdout || ""}\n${result.stderr || ""}`; } @@ -691,7 +725,11 @@ function selectBackendPath(useGpu, backendType = "auto") { if (fs.existsSync(BACKEND_PATHS.linuxCpu)) return BACKEND_PATHS.linuxCpu; return BACKEND_PATH; } - // macOS fallback + if (osPlatform === "darwin") { + if (fs.existsSync(BACKEND_PATHS.mac)) return BACKEND_PATHS.mac; + return BACKEND_PATH; + } + // generic fallback if (fs.existsSync(BACKEND_PATHS.mac)) return BACKEND_PATHS.mac; return BACKEND_PATH; } @@ -709,6 +747,7 @@ function getBackendMode(backendPath, useGpu, backendType = "auto") { if (name.includes("cuda")) return "CUDA GPU"; if (name.includes("rocm")) return "ROCm GPU"; if (name.includes("vulkan")) return "Vulkan GPU"; + if (osPlatform === "darwin" || backendType === "metal") return "Metal GPU"; return "GPU"; } @@ -882,6 +921,13 @@ async function startBackend(settings = {}) { "--rng", "cpu", "--sampler-rng", "cpu", ); + } else if (requestedBackend === "metal") { + args.push( + "--backend", "metal", + "--params-backend", "metal", + "--rng", "cpu", + "--sampler-rng", "cpu", + ); } if (currentSettings.vaeTiling) { @@ -909,6 +955,10 @@ async function startBackend(settings = {}) { const existing = spawnEnv.LD_LIBRARY_PATH || ""; spawnEnv.LD_LIBRARY_PATH = extraLibs.join(":") + (existing ? ":" + existing : ""); } + } else if (osPlatform === "darwin") { + const existing = spawnEnv.DYLD_LIBRARY_PATH || ""; + const backendDir = path.dirname(BACKEND_PATHS.mac); + spawnEnv.DYLD_LIBRARY_PATH = backendDir + (existing ? ":" + existing : ""); } console.log(" [backend] Starting:", path.basename(backendPath), args.join(" ")); @@ -1825,7 +1875,7 @@ const server = http.createServer(async (req, res) => { }); }); -server.listen(PORT_FRONTEND, "127.0.0.1", () => { +server.listen(PORT_FRONTEND, "0.0.0.0", () => { console.log(""); console.log(" ============================================================"); console.log(" LOCAL AI IMAGE GENERATOR | Running"); diff --git a/scripts/setup.sh b/scripts/setup.sh index 8c47af1..5ad0d56 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Local AI Image Generator - Linux Setup Script +# Local AI Image Generator - Linux/macOS Setup Script # Self-contained: no apt/yum/pacman, no global Node.js install. # @@ -11,18 +11,40 @@ ROOT_DIR="$(dirname "$SCRIPT_DIR")" APP_DIR="$ROOT_DIR/app" FRONTEND_DIR="$APP_DIR/frontend" TOOLS_DIR="$APP_DIR/tools" -NODE_DIR="$TOOLS_DIR/node-linux" +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" -DIST_DIR="$APP_DIR/dist" -BACKEND_DIR="$APP_DIR/backend/linux" # 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" -NODE_TARBALL="node-v${NODE_VERSION}-linux-x64.tar.xz" + +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 @@ -36,7 +58,7 @@ print_header() { clear 2>/dev/null || true echo "" echo " ============================================================" - echo " LOCAL AI IMAGE GENERATOR - Linux First-Time Setup" + echo " LOCAL AI IMAGE GENERATOR - $PLATFORM_LABEL First-Time Setup" echo " 100% Self-Contained | No System Install Required" echo " ============================================================" echo "" @@ -180,15 +202,39 @@ copy_binaries_from_extracted() { 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 -check_glibc +if [[ "$PLATFORM" == "Linux" ]]; then + check_glibc +fi TOTAL_STEPS=4 # ── Step 1: Portable Node.js ──────────────────────────────────────────────── -print_step 1 $TOTAL_STEPS "Setting up portable Node.js (app/tools/node-linux/)" +print_step 1 $TOTAL_STEPS "Setting up portable Node.js ($NODE_DIR/)" if [[ -x "$NODE_BIN" && -x "$NPM_BIN" ]]; then VERSION=$("$NODE_BIN" --version) @@ -201,7 +247,7 @@ else 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*-linux-x64' | head -n 1)" + 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" @@ -217,11 +263,32 @@ else fi # ── Step 2: stable-diffusion.cpp Backends ─────────────────────────────────── -VENDOR="$(detect_gpu_vendor)" -print_step 2 $TOTAL_STEPS "Detecting GPU vendor: ${VENDOR:-none}" - 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 "Intel Macs need a local source build with Metal/OpenBLAS and a matching app/backend/mac/sd binary." + 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 +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" ]]; then @@ -284,6 +351,7 @@ if [[ $MAX_PERF -eq 1 ]] && [[ "$VENDOR" == "nvidia" ]]; then print_info "Linux CUDA backend is disabled pending a reliable upstream binary." print_info "NVIDIA GPUs on Linux will use the Vulkan backend instead." fi +fi # ── Step 3: npm install ───────────────────────────────────────────────────── print_step 3 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)" @@ -320,7 +388,7 @@ echo " Setup complete! Run ./start.sh to launch." echo " ============================================================" echo "" -if [[ $MAX_PERF -eq 0 ]] && [[ "$VENDOR" == "nvidia" || "$VENDOR" == "amd" ]]; then +if [[ "$PLATFORM" == "Linux" && $MAX_PERF -eq 0 ]] && [[ "$VENDOR" == "nvidia" || "$VENDOR" == "amd" ]]; then echo " Tip: For maximum GPU performance, re-run with:" echo " ./scripts/setup.sh --max-perf" echo "" diff --git a/start.sh b/start.sh index 399e82f..4736315 100755 --- a/start.sh +++ b/start.sh @@ -1,15 +1,25 @@ #!/usr/bin/env bash # -# Local AI Image Generator - Linux Launcher +# Local AI Image Generator - Linux/macOS Launcher # Double-click or run: ./start.sh -# Use --max-perf to enable ROCm/CUDA backend downloads on first setup. +# 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" -NODE_DIR="$APP_DIR/tools/node-linux" +PLATFORM="$(uname -s)" +if [[ "$PLATFORM" == "Darwin" ]]; then + NODE_DIR="$APP_DIR/tools/node-mac" + BACKEND_PATH="$APP_DIR/backend/mac/sd" + PLATFORM_LABEL="macOS" +else + NODE_DIR="$APP_DIR/tools/node-linux" + BACKEND_PATH="$APP_DIR/backend/linux/vulkan/sd-vulkan" + CPU_BACKEND_PATH="$APP_DIR/backend/linux/cpu/sd-cpu" + PLATFORM_LABEL="Linux" +fi NODE_BIN="$NODE_DIR/bin/node" DIST_INDEX="$APP_DIR/dist/index.html" SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh" @@ -38,15 +48,21 @@ if [[ ! -f "$DIST_INDEX" ]]; then SETUP_REASON="Frontend build is missing." fi -# At minimum we need CPU or Vulkan backend on Linux -if [[ ! -f "$APP_DIR/backend/linux/cpu/sd-cpu" && ! -f "$APP_DIR/backend/linux/vulkan/sd-vulkan" ]]; then - SETUP_REASON="No Linux backend binary is installed." +if [[ "$PLATFORM" == "Darwin" ]]; then + if [[ ! -x "$BACKEND_PATH" ]]; then + SETUP_REASON="No macOS Metal backend binary is installed." + fi +else + # At minimum we need CPU or Vulkan backend on Linux + if [[ ! -f "$CPU_BACKEND_PATH" && ! -f "$BACKEND_PATH" ]]; then + SETUP_REASON="No Linux backend binary is installed." + fi fi if [[ -n "$SETUP_REASON" ]]; then echo "" echo " ============================================================" - echo " LOCAL AI IMAGE GENERATOR | $SETUP_MODE" + echo " LOCAL AI IMAGE GENERATOR | $PLATFORM_LABEL $SETUP_MODE" echo " ============================================================" echo "" if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then @@ -98,7 +114,10 @@ SERVER_PID=$! sleep 2 # Open browser -if command -v xdg-open >/dev/null 2>&1; then +if [[ "$PLATFORM" == "Darwin" ]] && command -v open >/dev/null 2>&1; then + echo " Opening browser at http://localhost:${FRONTEND_PORT}" + open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 & +elif command -v xdg-open >/dev/null 2>&1; then echo " Opening browser at http://localhost:${FRONTEND_PORT}" xdg-open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 & else