diff --git a/README.md b/README.md index 3638f91..6c4c94c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ * [What is Uncensored AI Studio?](#what-is-uncensored-ai-studio) * [Key Features](#key-features) * [Workspace & Engine Architecture](#workspace-architecture) +* [Supported Models](#supported-models) * [Folder Architecture](#folder-architecture) * [Getting Started](#getting-started) * [Windows Setup](#windows-setup) @@ -73,6 +74,46 @@ To avoid exhausting system RAM or VRAM, text and image engines are mutually excl --- +## Supported Models + +The app is designed around single-file local models that can be loaded directly by the bundled backend engines. + +### Image generation + +| Model type | Supported | Put files in | Notes | +| :--- | :--- | :--- | :--- | +| Stable Diffusion 1.5 checkpoints | Yes | `app/models/` | Best compatibility. Use `.safetensors` or `.ckpt` files. | +| SDXL checkpoints | Yes | `app/models/` | Supported as single-file checkpoints. Requires more RAM/VRAM than SD 1.5. | +| Single-file SD/SDXL GGUF checkpoints | Limited | `app/models/` | Only complete single-file checkpoints are supported. | +| OpenVINO image model folders | Intel NPU only | `app/openvino-models/` | Download from the Model Manager after running the OpenVINO setup. | +| CoreML image models | Apple Silicon only | `app/models/` | Requires macOS on Apple Silicon and the CoreML setup path. | +| Flux, HiDream, Hunyuan, Wan, Qwen Image, Z-Image workflows | No | N/A | These usually require separate diffusion, VAE, and text encoder files and are not one-click checkpoint loads in this app. | +| LoRA, ControlNet, VAE-only, text-encoder-only, or diffusion-only files | No | N/A | Companion files are not loaded as standalone image models. | + +Known-good image models available from the Model Manager: + +| Name | Filename | Type | Approx. size | Recommended use | +| :--- | :--- | :--- | :--- | :--- | +| Juggernaut XL v9 Lightning | `Juggernaut_RunDiffusionPhoto2_Lightning_4Steps.safetensors` | SDXL | 6.6 GB | High-quality photorealism on mid/high tier machines. | +| DreamShaper XL Lightning | `DreamShaperXL_Lightning.safetensors` | SDXL | 6.6 GB | General SDXL images, fantasy, renders, and illustration. | +| DreamShaper 8 | `DreamShaper_8_pruned.safetensors` | SD 1.5 | 2.1 GB | Faster, lower-memory image generation. | +| CyberRealistic V8 | `CyberRealistic_V8_FP16.safetensors` | SD 1.5 | 2.0 GB | Realistic SD 1.5 images and lower-memory systems. | +| Rev Animated | `rev-animated-v1-2-2.safetensors` | SD 1.5 | 2.0 GB | Stylized/anime SD 1.5 images. | +| LCM DreamShaper OpenVINO | `OpenVINO/LCM_Dreamshaper_v7-fp16-ov` | OpenVINO | 2.7 GB | Intel Core Ultra NPU test model. | + +### Text, speech, and TTS + +| Workspace | Supported model files | Put files in | Notes | +| :--- | :--- | :--- | :--- | +| Text Chat | `.gguf` llama.cpp models | `app/llm-models/` | Use single-file GGUF chat/instruct models. Vision models may also require a matching `mmproj` file. | +| Speech-to-Text | whisper.cpp `.bin` models | `app/speech-models/` | Use Whisper GGML/whisper.cpp model files. | +| Text-to-Speech | Kokoro `.json` manifests and model assets | `app/tts-models/` / `app/tts-runtime/` | Use the built-in Kokoro setup and Model Manager entries. | + +> [!NOTE] +> Linux release binaries are built for Ubuntu 24.04-era systems and require `glibc 2.38+` plus `GLIBCXX_3.4.32+`. On older Ubuntu/Debian VMs, a model such as CyberRealistic may be valid but the backend can still fail before loading it. Upgrade the VM OS or build the backend from source. + +--- + ## 📁 Folder Architecture ``` @@ -195,6 +236,11 @@ Ensure you have a modern web browser installed. Follow the quick guide below for
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.
+On dual-GPU Linux systems, Vulkan device order can put the integrated Intel GPU at vulkan0 and the discrete AMD/NVIDIA GPU at vulkan1. The launcher now tries to prefer a discrete Vulkan device when vulkaninfo --summary is available. To force a device manually, start the app with SD_VULKAN_DEVICE=vulkan1 ./linux.sh or use another index such as vulkan0/vulkan2.
3221225781 (0xC0000135)This code means Windows could not locate a required backend DLL:
diff --git a/app/frontend/src/components/ModelManager.jsx b/app/frontend/src/components/ModelManager.jsx index 72a97c7..116d6ef 100644 --- a/app/frontend/src/components/ModelManager.jsx +++ b/app/frontend/src/components/ModelManager.jsx @@ -35,7 +35,8 @@ import { downloadTtsModel, deleteTtsModel, importTtsModel, - getTtsStatus + getTtsStatus, + isLocalServerMode } from "../services/api"; const MODEL_FILTERS = [ @@ -605,9 +606,8 @@ function ModelManager({ } const isTauriDesktop = typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined; - const isLocalServerMode = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); - if (!isTauriDesktop && !isLocalServerMode) { + if (!isTauriDesktop && !isLocalServerMode()) { showAlert({ title: "Server Required", message: "Model download requires the local server.", danger: true }); return; } @@ -959,11 +959,10 @@ function ModelManager({ const performLoadModel = async (modelId, forcedConstraints = null) => { const isTauriDesktop = typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined; - const isLocalServerMode = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); const backendStatus = await getBackendStatus(); const backendPort = backendStatus?.port || 8080; - if (!isTauriDesktop && !isLocalServerMode) { + if (!isTauriDesktop && !isLocalServerMode()) { const isAlreadyRunning = await pingServer(); const modelInfo = getLocalModelInfo(modelId); const isOpenVinoModel = modelInfo?.backendType === "openvino-npu"; diff --git a/app/frontend/src/services/api.js b/app/frontend/src/services/api.js index 3623892..a719f05 100644 --- a/app/frontend/src/services/api.js +++ b/app/frontend/src/services/api.js @@ -11,8 +11,9 @@ let cachedSpecs = null; let cachedBackendPort = null; 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"); +export const isLocalServerMode = () => { + if (typeof window === "undefined") return false; + return window.location.protocol === "http:" || window.location.protocol === "https:"; }; async function getBackendPort() { @@ -31,6 +32,9 @@ async function getBackendPort() { } async function getBackendBaseUrl() { + if (!isTauri() && isLocalServerMode()) { + return ""; + } return `http://127.0.0.1:${await getBackendPort()}`; } @@ -224,8 +228,7 @@ export async function listLocalModels() { } } - const isLocalServerMode = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); - if (isLocalServerMode) { + if (isLocalServerMode()) { return await listModelsFromDisk(); } @@ -1162,8 +1165,7 @@ export async function deleteModel(filename) { return await invoke("delete_model_file", { filename }); } - const isLocalServerMode = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); - if (isLocalServerMode) { + if (isLocalServerMode()) { try { const res = await fetch("/api/delete-model", { method: "POST", diff --git a/scripts/server/serve.cjs b/scripts/server/serve.cjs index 3eded97..8d79411 100644 --- a/scripts/server/serve.cjs +++ b/scripts/server/serve.cjs @@ -587,6 +587,50 @@ function isVirtualOrSoftwareGpu(name) { lowercase.includes("vmware"); } +let cachedPreferredVulkanBackend = null; +let cachedPreferredVulkanBackendChecked = false; +function getPreferredVulkanBackendName() { + const explicit = String(process.env.SD_VULKAN_DEVICE || process.env.UAIS_VULKAN_DEVICE || "").trim(); + if (/^vulkan\d+$/i.test(explicit)) return explicit.toLowerCase(); + if (/^\d+$/.test(explicit)) return `vulkan${explicit}`; + + if (cachedPreferredVulkanBackendChecked) return cachedPreferredVulkanBackend || "vulkan0"; + cachedPreferredVulkanBackendChecked = true; + cachedPreferredVulkanBackend = "vulkan0"; + + if (osPlatform !== "linux") return cachedPreferredVulkanBackend; + try { + const result = spawnSync("vulkaninfo", ["--summary"], { + encoding: "utf8", + timeout: 5000, + maxBuffer: 1024 * 1024, + }); + const output = `${result.stdout || ""}\n${result.stderr || ""}`; + const devices = []; + let current = null; + for (const line of output.split(/\r?\n/)) { + const gpuMatch = line.match(/^\s*GPU(\d+)\s*:/i); + if (gpuMatch) { + current = { index: Number(gpuMatch[1]), name: "", type: "" }; + devices.push(current); + continue; + } + if (!current) continue; + const nameMatch = line.match(/deviceName\s*=\s*(.+)$/i); + if (nameMatch) current.name = nameMatch[1].trim(); + const typeMatch = line.match(/deviceType\s*=\s*(.+)$/i); + if (typeMatch) current.type = typeMatch[1].trim(); + } + const usable = devices.filter((device) => Number.isInteger(device.index) && !isVirtualOrSoftwareGpu(device.name)); + const discrete = usable.find((device) => /discrete/i.test(device.type)); + const amd = usable.find((device) => /amd|radeon/i.test(device.name)); + const selected = discrete || amd || usable[0]; + if (selected) cachedPreferredVulkanBackend = `vulkan${selected.index}`; + } catch (_) {} + + return cachedPreferredVulkanBackend; +} + let cachedLlamaCudaGpu = null; let cachedLlamaCudaGpuChecked = false; function detectLlamaCudaGpu() { @@ -3285,6 +3329,36 @@ function requestJson(url, payload = null, timeoutMs = 120000) { }); } +function proxyImageBackendRequest(req, res) { + const headers = { ...req.headers }; + delete headers.host; + headers.host = `127.0.0.1:${PORT_BACKEND}`; + + const proxyReq = http.request({ + hostname: "127.0.0.1", + port: PORT_BACKEND, + path: req.url, + method: req.method, + headers, + timeout: 300000, + }, (proxyRes) => { + res.writeHead(proxyRes.statusCode || 502, { + ...proxyRes.headers, + "Access-Control-Allow-Origin": "*", + }); + proxyRes.pipe(res); + }); + + proxyReq.on("error", (err) => { + json(res, 502, { ok: false, error: `Image backend is not reachable: ${err.message}` }); + }); + proxyReq.on("timeout", () => { + proxyReq.destroy(new Error("Image backend request timed out")); + }); + + req.pipe(proxyReq); +} + function requestHttpsJson(url, timeoutMs = 30000) { return new Promise((resolve, reject) => { const req = https.get(url, { @@ -4303,6 +4377,7 @@ async function startBackend(settings = {}) { let args = []; const requestedBackend = resolveBackendType(currentSettings.useGpu, currentSettings.backendType, currentSettings.model); + const selectedVulkanBackend = requestedBackend === "vulkan" ? getPreferredVulkanBackendName() : ""; const supportsFlags = backendSupportsFlags[backendPath] !== false; @@ -4331,7 +4406,7 @@ async function startBackend(settings = {}) { args.push("--rng", "cpu", "--sampler-rng", "cpu"); } else if (requestedBackend === "vulkan") { if (supportsFlags) { - args.push("--backend", "vulkan0", "--params-backend", "vulkan0"); + args.push("--backend", selectedVulkanBackend, "--params-backend", selectedVulkanBackend); } args.push("--rng", "cpu", "--sampler-rng", "cpu"); } else if (requestedBackend === "cuda") { @@ -4350,6 +4425,11 @@ async function startBackend(settings = {}) { } + if (requestedBackend === "vulkan" && selectedVulkanBackend) { + currentSettings.backendDevice = selectedVulkanBackend; + backendLoadState.device = selectedVulkanBackend; + } + if (requestedBackend !== "apple-npu") { if (currentSettings.vaeTiling) { args.push("--vae-tiling"); @@ -4485,6 +4565,11 @@ async function startBackend(settings = {}) { process.stderr.write(" [sd-err] " + output); updateCoreMLLoadProgress(output); const cleanOutput = stripAnsi(output); + const runtimeLinkerError = describeLinuxRuntimeLinkerError(cleanOutput); + if (runtimeLinkerError) { + backendError = runtimeLinkerError; + backendLoadState.phase = "Linux runtime is too old for this backend"; + } const deviceMatch = cleanOutput.match(/Device\s+\d+:\s*([^,\r\n]+)/); if (deviceMatch) { backendLoadState.device = deviceMatch[1].trim(); @@ -5217,6 +5302,22 @@ function describeBackendError(rawError, modelPath) { return `${raw}\n\nThe backend could not create the model context. Common causes are a corrupt/incomplete model file, unsupported checkpoint type, or not enough free RAM/VRAM. Delete and re-download the model, then try CPU or Vulkan mode at 512x512.`; } +function describeLinuxRuntimeLinkerError(rawError) { + const raw = String(rawError || "").trim(); + const lower = raw.toLowerCase(); + if (osPlatform !== "linux") return null; + if (!lower.includes("glibc") && !lower.includes("libstdc++") && !lower.includes("libc.so.6")) return null; + + const needsGlibc = raw.match(/GLIBC_([0-9.]+)/)?.[1]; + const needsGlibcxx = raw.match(/GLIBCXX_([0-9.]+)/)?.[1]; + const requirements = []; + if (needsGlibc) requirements.push(`glibc ${needsGlibc}+`); + if (needsGlibcxx) requirements.push(`GLIBCXX_${needsGlibcxx}+`); + const requirementText = requirements.length ? requirements.join(" and ") : "newer glibc/libstdc++ runtime libraries"; + + return `${raw}\n\nThe selected model is not the problem. The Linux backend binary cannot start because this OS is missing ${requirementText}. The bundled Linux backends are built for Ubuntu 24.04-era systems. Use Ubuntu 24.04+, Fedora 40+, another glibc 2.38+ distro, or build stable-diffusion.cpp from source on this machine.`; +} + function describeBackendExitCode(code, backendPath) { const numericCode = Number(code); if (osPlatform === "win32" && numericCode === 3221225781) { @@ -5483,6 +5584,9 @@ const server = http.createServer(async (req, res) => { res.end(); return; } + if ((req.url.startsWith("/v1/") || req.url.startsWith("/sdapi/")) && ["GET", "POST"].includes(req.method)) { + return proxyImageBackendRequest(req, res); + } // ── Management API ──────────────────────────────────────────────────────── // GET /api/health if (req.url === "/api/health" && req.method === "GET") { diff --git a/scripts/setup/setup-llama.ps1 b/scripts/setup/setup-llama.ps1 index 4cf8112..c46c3b5 100644 --- a/scripts/setup/setup-llama.ps1 +++ b/scripts/setup/setup-llama.ps1 @@ -34,57 +34,82 @@ 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 + for ($attempt = 1; $attempt -le 3; $attempt++) { + $lastBytes = [long]0 + $lastTime = [DateTime]::Now + $resp = $null + $stream = $null + $out = $null - while ($true) { - $read = $stream.Read($buf, 0, $buf.Length) - if ($read -le 0) { break } - $out.Write($buf, 0, $read) - $done += $read + try { + if (Test-Path $DestPath) { Remove-Item $DestPath -Force } + Enable-Tls12 + $req = [System.Net.HttpWebRequest]::Create($Url) + $req.UserAgent = "Mozilla/5.0" + $req.Timeout = 300000 + $req.ReadWriteTimeout = 300000 + $resp = $req.GetResponse() + $total = [long]$resp.ContentLength + $stream = $resp.GetResponseStream() + $out = [System.IO.File]::Create($DestPath) + $buf = New-Object byte[] 65536 + $done = [long]0 - $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)) + while ($true) { + $read = $stream.Read($buf, 0, $buf.Length) + if ($read -le 0) { break } + $out.Write($buf, 0, $read) + $done += $read - $eta = "" - if ($speed -gt 0 -and $total -gt 0) { - $rem = [int](($total - $done) / $speed) - $eta = " ETA $([int]($rem/60))m$($rem%60)s" + $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 " } - - $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 " } + + if ($total -gt 0 -and $done -ne $total) { + throw "Download ended early: $(Format-Bytes $done) of $(Format-Bytes $total) received." + } + + Write-Host "`r [$("#" * $barWidth)] 100% $(Format-Bytes $done) Done! " -ForegroundColor Green + Write-Host "" + return + } catch { + Write-Host "" + if ($attempt -lt 3) { + Write-Host " !! Download attempt $attempt failed: $_" -ForegroundColor Yellow + Write-Host " >> Retrying download..." -ForegroundColor Cyan + Start-Sleep -Seconds (2 * $attempt) + } else { + throw "Download failed after $attempt attempts: $_" + } + } finally { + if ($out) { $out.Close() } + if ($stream) { $stream.Close() } + if ($resp) { $resp.Close() } } - $out.Close(); $stream.Close() - Write-Host "`r [$("#" * $barWidth)] 100% $(Format-Bytes $done) Done! " -ForegroundColor Green - Write-Host "" - } catch { - Write-Host "" - throw "Download failed: $_" + if (Test-Path $DestPath) { Remove-Item $DestPath -Force -ErrorAction SilentlyContinue } } } diff --git a/scripts/setup/setup.ps1 b/scripts/setup/setup.ps1 index 25a6afb..6a0fe68 100644 --- a/scripts/setup/setup.ps1 +++ b/scripts/setup/setup.ps1 @@ -64,58 +64,83 @@ function Invoke-RichDownload { Write-Host "" $barWidth = 48 - $lastBytes = [long]0 - $lastTime = [DateTime]::Now - 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($Dest) - $buf = New-Object byte[] 65536 - $done = [long]0 + for ($attempt = 1; $attempt -le 3; $attempt++) { + $lastBytes = [long]0 + $lastTime = [DateTime]::Now + $resp = $null + $stream = $null + $out = $null - while ($true) { - $read = $stream.Read($buf, 0, $buf.Length) - if ($read -le 0) { break } - $out.Write($buf, 0, $read) - $done += $read + try { + if (Test-Path $Dest) { Remove-Item $Dest -Force } + Enable-Tls12 + $req = [System.Net.HttpWebRequest]::Create($Url) + $req.UserAgent = "Mozilla/5.0" + $req.Timeout = 300000 + $req.ReadWriteTimeout = 300000 + $resp = $req.GetResponse() + $total = [long]$resp.ContentLength + $stream = $resp.GetResponseStream() + $out = [System.IO.File]::Create($Dest) + $buf = New-Object byte[] 65536 + $done = [long]0 - $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)) + while ($true) { + $read = $stream.Read($buf, 0, $buf.Length) + if ($read -le 0) { break } + $out.Write($buf, 0, $read) + $done += $read - $eta = "" - if ($speed -gt 0 -and $total -gt 0) { - $rem = [int](($total - $done) / $speed) - $eta = " ETA $([int]($rem/60))m$($rem%60)s" + $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 " } - - $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 " } + + if ($total -gt 0 -and $done -ne $total) { + throw "Download ended early: $(Format-Bytes $done) of $(Format-Bytes $total) received." + } + + Write-Host "`r [$("#" * $barWidth)] 100% $(Format-Bytes $done) Done! " -ForegroundColor Green + Write-Host "" + return $true + } catch { + Write-Host "" + if ($attempt -lt 3) { + Print-Warn "Download attempt $attempt failed: $_" + Print-Info "Retrying download..." + Start-Sleep -Seconds (2 * $attempt) + } else { + Print-Fail "Download failed after $attempt attempts: $_" + } + } finally { + if ($out) { $out.Close() } + if ($stream) { $stream.Close() } + if ($resp) { $resp.Close() } } - $out.Close(); $stream.Close() - Write-Host "`r [$("#" * $barWidth)] 100% $(Format-Bytes $done) Done! " -ForegroundColor Green - Write-Host "" - return $true - } catch { - Write-Host "" - Print-Fail "Download failed: $_" - return $false + if (Test-Path $Dest) { Remove-Item $Dest -Force -ErrorAction SilentlyContinue } } + + return $false } function Expand-WithProgress { @@ -297,9 +322,11 @@ if ($hasNvidia) { $backendDest = Join-Path $appDir "backend\win\cuda" $backendExe = Join-Path $backendDest "sd-cuda.exe" $backendDll = Join-Path $backendDest "stable-diffusion.dll" + $cudaBackendReady = $false if ((Test-Path $backendExe) -and (Test-Path $backendDll)) { Print-OK "CUDA GPU backend binaries already ready." + $cudaBackendReady = $true } else { $backendZip = Join-Path $toolsDir "sd-cuda.zip" New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null @@ -310,59 +337,63 @@ if ($hasNvidia) { -Dest $backendZip ` -Label "stable-diffusion.cpp CUDA Backend (Windows x64)" - if (-not $ok) { Print-Fail "Cannot download CUDA backend binaries."; Read-Host; exit 1 } - - $tempExt = Join-Path $toolsDir "sd-cuda-temp" - Expand-WithProgress -ZipPath $backendZip -Destination $tempExt -Label "CUDA Backend" - Remove-Item $backendZip -Force - - # Move files and rename sd.exe/sd-server.exe to sd-cuda.exe - 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 $backendExe -Force } - if (Test-Path $extractedDll) { Copy-Item $extractedDll $backendDll -Force } - - # Copy any other DLLs or EXEs - Get-ChildItem $tempExt -Filter "*.dll" -Recurse | ForEach-Object { Copy-Item $_.FullName $backendDest -Force } - Get-ChildItem $tempExt -Filter "*.exe" -Recurse | ForEach-Object { - if ($_.FullName -ne $extractedExe) { Copy-Item $_.FullName $backendDest -Force } - } - Remove-Item $tempExt -Recurse -Force - } - - if ((Test-Path $backendExe) -and (Test-Path $backendDll)) { - Print-OK "CUDA GPU backend binaries installed successfully!" + if (-not $ok) { + Print-Warn "Cannot download CUDA backend binaries. Continuing with the Vulkan backend fallback." } else { - Print-Fail "Failed to copy backend binaries to app/backend/win/cuda/." - Read-Host; exit 1 + $tempExt = Join-Path $toolsDir "sd-cuda-temp" + Expand-WithProgress -ZipPath $backendZip -Destination $tempExt -Label "CUDA Backend" + Remove-Item $backendZip -Force + + # Move files and rename sd.exe/sd-server.exe to sd-cuda.exe + 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 $backendExe -Force } + if (Test-Path $extractedDll) { Copy-Item $extractedDll $backendDll -Force } + + # Copy any other DLLs or EXEs + Get-ChildItem $tempExt -Filter "*.dll" -Recurse | ForEach-Object { Copy-Item $_.FullName $backendDest -Force } + Get-ChildItem $tempExt -Filter "*.exe" -Recurse | ForEach-Object { + if ($_.FullName -ne $extractedExe) { Copy-Item $_.FullName $backendDest -Force } + } + Remove-Item $tempExt -Recurse -Force + } + + if ((Test-Path $backendExe) -and (Test-Path $backendDll)) { + Print-OK "CUDA GPU backend binaries installed successfully!" + $cudaBackendReady = $true + } else { + Print-Warn "Failed to copy CUDA backend binaries. Continuing with the Vulkan backend fallback." + } } } - $cudaDllsExist = (Test-Path (Join-Path $backendDest "cublas64_12.dll")) -and ` - (Test-Path (Join-Path $backendDest "cublasLt64_12.dll")) -and ` - (Test-Path (Join-Path $backendDest "cudart64_12.dll")) + if ($cudaBackendReady) { + $cudaDllsExist = (Test-Path (Join-Path $backendDest "cublas64_12.dll")) -and ` + (Test-Path (Join-Path $backendDest "cublasLt64_12.dll")) -and ` + (Test-Path (Join-Path $backendDest "cudart64_12.dll")) - if (-not $cudaDllsExist) { - Print-Info "CUDA runtime DLLs are missing from backend folder. Downloading portable CUDA v12 runtime..." - $dllZip = Join-Path $toolsDir "cuda-dlls.zip" - $ok = Invoke-RichDownload ` - -Url "https://github.com/ggml-org/llama.cpp/releases/download/b9509/cudart-llama-bin-win-cuda-12.4-x64.zip" ` - -Dest $dllZip ` - -Label "CUDA v12 Runtime DLLs (llama.cpp)" + if (-not $cudaDllsExist) { + Print-Info "CUDA runtime DLLs are missing from backend folder. Downloading portable CUDA v12 runtime..." + $dllZip = Join-Path $toolsDir "cuda-dlls.zip" + $ok = Invoke-RichDownload ` + -Url "https://github.com/ggml-org/llama.cpp/releases/download/b9509/cudart-llama-bin-win-cuda-12.4-x64.zip" ` + -Dest $dllZip ` + -Label "CUDA v12 Runtime DLLs (llama.cpp)" - if ($ok) { - Expand-WithProgress -ZipPath $dllZip -Destination $backendDest -Label "CUDA Runtime DLLs" - Remove-Item $dllZip -Force - Print-OK "CUDA runtime DLLs set up successfully!" - } else { - Print-Warn "Could not download portable CUDA runtime DLLs automatically. If the app fails to start in CUDA mode, you may need to install the CUDA Toolkit manually." + if ($ok) { + Expand-WithProgress -ZipPath $dllZip -Destination $backendDest -Label "CUDA Runtime DLLs" + Remove-Item $dllZip -Force + Print-OK "CUDA runtime DLLs set up successfully!" + } else { + Print-Warn "Could not download portable CUDA runtime DLLs automatically. If the app fails to start in CUDA mode, you may need to install the CUDA Toolkit manually." + } } } diff --git a/scripts/setup/setup.sh b/scripts/setup/setup.sh index 079aa51..884d3e7 100644 --- a/scripts/setup/setup.sh +++ b/scripts/setup/setup.sh @@ -194,23 +194,69 @@ detect_gpu_vendor() { 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="" +# Official Linux binaries are built on Ubuntu 24.04 and link against glibc 2.38+ +# plus libstdc++ with GLIBCXX_3.4.32+. Stop early on older distributions so +# setup does not appear successful when the backend cannot start. +version_at_least() { + local current="$1" required="$2" + [[ "$(printf '%s\n' "$required" "$current" | sort -V | head -n1)" == "$required" ]] +} + +detect_glibcxx_version() { + local libstdcpp="" + if command -v ldconfig >/dev/null 2>&1; then + libstdcpp="$(ldconfig -p 2>/dev/null | awk '/libstdc\+\+\.so\.6/{print $NF; exit}')" + fi + if [[ -z "$libstdcpp" ]]; then + for candidate in /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /usr/lib64/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6; do + if [[ -f "$candidate" ]]; then + libstdcpp="$candidate" + break + fi + done + fi + if [[ -n "$libstdcpp" && -f "$libstdcpp" ]] && command -v strings >/dev/null 2>&1; then + strings "$libstdcpp" 2>/dev/null | grep -oE 'GLIBCXX_[0-9]+\.[0-9]+\.[0-9]+' | sed 's/GLIBCXX_//' | sort -V | tail -n1 + fi +} + +check_linux_runtime_abi() { + local required_glibc="2.38" + local required_glibcxx="3.4.32" + local current_glibc="" + local current_glibcxx="" + local unsupported=0 + 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)" + current_glibc="$(ldd --version 2>/dev/null | head -n1 | grep -oE '[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 + current_glibcxx="$(detect_glibcxx_version || true)" + + if [[ -z "$current_glibc" ]]; then + print_warn "Could not detect glibc version. Prebuilt Linux backends require glibc $required_glibc+ (Ubuntu 24.04)." + elif ! version_at_least "$current_glibc" "$required_glibc"; then + print_fail "Detected glibc $current_glibc. Prebuilt Linux backends require glibc $required_glibc+ (Ubuntu 24.04 or newer)." + unsupported=1 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)." + + if [[ -z "$current_glibcxx" ]]; then + print_warn "Could not detect GLIBCXX version. Prebuilt Linux backends require GLIBCXX_$required_glibcxx+." + elif ! version_at_least "$current_glibcxx" "$required_glibcxx"; then + print_fail "Detected GLIBCXX_$current_glibcxx. Prebuilt Linux backends require GLIBCXX_$required_glibcxx+." + unsupported=1 fi + + if [[ $unsupported -ne 0 ]]; then + print_info "CyberRealistic and other valid models will fail before loading on this OS because the backend binary cannot start." + print_info "Fix: use Ubuntu 24.04+, Fedora 40+, another glibc 2.38+ distro, or build stable-diffusion.cpp from source on this machine." + if [[ "${UAIS_ALLOW_UNSUPPORTED_LINUX:-0}" == "1" ]]; then + print_warn "Continuing anyway because UAIS_ALLOW_UNSUPPORTED_LINUX=1 is set." + return 0 + fi + return 1 + fi + + print_ok "Linux runtime ABI ready: glibc $current_glibc, GLIBCXX_$current_glibcxx" } copy_binaries_from_extracted() { @@ -255,7 +301,10 @@ copy_macos_backend_from_extracted() { print_header if [[ "$PLATFORM" == "Linux" ]]; then - check_glibc + if ! check_linux_runtime_abi; then + print_fail "Linux setup stopped before downloading incompatible backend binaries." + exit 1 + fi fi TOTAL_STEPS=7