add linux
This commit is contained in:
1 parent
c3bc49c1d9
commit
25b9a5c965
13 files changed
+1255
-451
No files matched your search
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local AI Image Generator - Compile backends from source
|
||||
# For systems with GLIBC < 2.38 (e.g. Ubuntu 22.04)
|
||||
#
|
||||
|
||||
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"
|
||||
BUILD_DIR="$APP_DIR/tools/build-sd"
|
||||
|
||||
# Pinned release tag
|
||||
PINNED_TAG="master-685-19bdfe2"
|
||||
|
||||
echo "=== Local AI Image Generator - 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
|
||||
|
||||
# 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)
|
||||
|
||||
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."
|
||||
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$(nproc); 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
|
||||
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
|
||||
cmake --build . --config Release -j$(nproc)
|
||||
|
||||
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."
|
||||
else
|
||||
echo "WARNING: libstable-diffusion.so not found for CUDA build!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Build from source completed successfully! ==="
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local AI Image Generator - Linux 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 "$SCRIPT_DIR")"
|
||||
APP_DIR="$ROOT_DIR/app"
|
||||
|
||||
echo ""
|
||||
echo " ============================================================"
|
||||
echo " Resetting Local-AI-Image-Generator..."
|
||||
echo " ============================================================"
|
||||
echo ""
|
||||
|
||||
# Delete tools/node
|
||||
if [[ -d "$APP_DIR/tools" ]]; then
|
||||
echo " >> Removing portable tools/ node folder..."
|
||||
rm -rf "$APP_DIR/tools"
|
||||
fi
|
||||
|
||||
# Delete backend
|
||||
if [[ -d "$APP_DIR/backend" ]]; then
|
||||
echo " >> Removing backend binaries..."
|
||||
rm -rf "$APP_DIR/backend"
|
||||
fi
|
||||
|
||||
# Delete dist
|
||||
if [[ -d "$APP_DIR/dist" ]]; then
|
||||
echo " >> Removing dist/ build folder..."
|
||||
rm -rf "$APP_DIR/dist"
|
||||
fi
|
||||
|
||||
# Preserve models
|
||||
if [[ -d "$APP_DIR/models" ]]; then
|
||||
echo " >> Preserving downloaded models in app/models."
|
||||
fi
|
||||
|
||||
# Delete node_modules in frontend
|
||||
if [[ -d "$APP_DIR/frontend/node_modules" ]]; then
|
||||
echo " >> Removing frontend node_modules..."
|
||||
rm -rf "$APP_DIR/frontend/node_modules"
|
||||
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. Models and generated outputs were preserved."
|
||||
echo " ============================================================"
|
||||
echo ""
|
||||
read -rp " Press Enter to close..."
|
||||
+163
-21
@@ -28,7 +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"),
|
||||
linux: path.join(ROOT, "app", "backend", "linux", "sd-vulkan"),
|
||||
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"),
|
||||
};
|
||||
let BACKEND_PATH = "";
|
||||
if (osPlatform === "win32") {
|
||||
@@ -51,7 +54,14 @@ if (osPlatform === "win32") {
|
||||
} else if (osPlatform === "darwin") {
|
||||
BACKEND_PATH = BACKEND_PATHS.mac;
|
||||
} else {
|
||||
BACKEND_PATH = BACKEND_PATHS.linux;
|
||||
// Linux: default to Vulkan if available, otherwise CPU
|
||||
if (fs.existsSync(BACKEND_PATHS.linuxVulkan)) {
|
||||
BACKEND_PATH = BACKEND_PATHS.linuxVulkan;
|
||||
} else if (fs.existsSync(BACKEND_PATHS.linuxCpu)) {
|
||||
BACKEND_PATH = BACKEND_PATHS.linuxCpu;
|
||||
} else {
|
||||
BACKEND_PATH = BACKEND_PATHS.linuxVulkan;
|
||||
}
|
||||
}
|
||||
const MODELS = path.join(ROOT, "app", "models");
|
||||
if (!fs.existsSync(MODELS)) {
|
||||
@@ -128,6 +138,32 @@ function getCpuUsagePercent() {
|
||||
return Math.max(0, Math.min(100, Math.round((1 - idleDelta / totalDelta) * 1000) / 10));
|
||||
}
|
||||
|
||||
function detectLinuxGpuFromSysfs() {
|
||||
try {
|
||||
const pciDir = "/sys/bus/pci/devices";
|
||||
if (!fs.existsSync(pciDir)) return null;
|
||||
const entries = fs.readdirSync(pciDir);
|
||||
for (const entry of entries) {
|
||||
const vendorFile = path.join(pciDir, entry, "vendor");
|
||||
const deviceFile = path.join(pciDir, entry, "device");
|
||||
if (!fs.existsSync(vendorFile)) continue;
|
||||
const vendor = fs.readFileSync(vendorFile, "utf8").trim();
|
||||
let device = "";
|
||||
try { device = fs.readFileSync(deviceFile, "utf8").trim(); } catch (_) {}
|
||||
if (vendor === "0x10de") {
|
||||
return { name: `NVIDIA GPU (${device || entry})` };
|
||||
}
|
||||
if (vendor === "0x1002") {
|
||||
return { name: `AMD GPU (${device || entry})` };
|
||||
}
|
||||
if (vendor === "0x8086") {
|
||||
return { name: `Intel GPU (${device || entry})` };
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGpuInfo() {
|
||||
if (cachedGpuInfo) return cachedGpuInfo;
|
||||
|
||||
@@ -144,6 +180,14 @@ function getGpuInfo() {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (osPlatform === "linux") {
|
||||
const linuxGpu = detectLinuxGpuFromSysfs();
|
||||
if (linuxGpu) {
|
||||
cachedGpuInfo = linuxGpu;
|
||||
return cachedGpuInfo;
|
||||
}
|
||||
}
|
||||
|
||||
cachedGpuInfo = { name: "Unavailable" };
|
||||
return cachedGpuInfo;
|
||||
}
|
||||
@@ -358,7 +402,9 @@ function getSetupPaths() {
|
||||
node: path.join(appDir, "tools", "node-linux", "bin", "node"),
|
||||
npm: path.join(appDir, "tools", "node-linux", "bin", "npm"),
|
||||
distIndex: path.join(DIST, "index.html"),
|
||||
linuxBackend: BACKEND_PATHS.linux,
|
||||
linuxCpuBackend: BACKEND_PATHS.linuxCpu,
|
||||
linuxVulkanBackend: BACKEND_PATHS.linuxVulkan,
|
||||
linuxRocmBackend: BACKEND_PATHS.linuxRocm,
|
||||
models: MODELS,
|
||||
outputs: OUTPUTS,
|
||||
};
|
||||
@@ -381,7 +427,9 @@ async function getHealth() {
|
||||
} else if (osPlatform === "darwin") {
|
||||
checks.push(getPathInfo("Mac backend", paths.macBackend));
|
||||
} else {
|
||||
checks.push(getPathInfo("Linux backend", paths.linuxBackend));
|
||||
checks.push(getPathInfo("Linux CPU backend", paths.linuxCpuBackend));
|
||||
checks.push(getPathInfo("Linux Vulkan backend", paths.linuxVulkanBackend));
|
||||
checks.push(getPathInfo("Linux ROCm backend", paths.linuxRocmBackend));
|
||||
}
|
||||
|
||||
let backendInstalled = false;
|
||||
@@ -391,11 +439,15 @@ async function getHealth() {
|
||||
} else if (osPlatform === "darwin") {
|
||||
backendInstalled = checks.find((check) => check.label === "Mac backend")?.exists;
|
||||
} else {
|
||||
backendInstalled = checks.find((check) => check.label === "Linux backend")?.exists;
|
||||
backendInstalled =
|
||||
checks.find((check) => check.label === "Linux CPU backend")?.exists ||
|
||||
checks.find((check) => check.label === "Linux CPU backend")?.exists ||
|
||||
checks.find((check) => check.label === "Linux Vulkan backend")?.exists ||
|
||||
checks.find((check) => check.label === "Linux ROCm backend")?.exists;
|
||||
}
|
||||
|
||||
const criticalOk = checks
|
||||
.filter((check) => !["CUDA backend", "Vulkan backend", "Linux backend", "Mac backend"].includes(check.label))
|
||||
.filter((check) => !["CUDA backend", "Vulkan backend", "Linux CPU backend", "Linux Vulkan backend", "Linux ROCm backend", "Mac backend"].includes(check.label))
|
||||
.every((check) => check.ok) && backendInstalled;
|
||||
|
||||
const ports = {
|
||||
@@ -408,7 +460,7 @@ async function getHealth() {
|
||||
ports.backend.ok = true;
|
||||
|
||||
const issues = checks
|
||||
.filter((check) => !check.ok && !["CUDA backend", "Vulkan backend", "Linux backend", "Mac backend"].includes(check.label))
|
||||
.filter((check) => !check.ok && !["CUDA backend", "Vulkan backend", "Linux CPU backend", "Linux Vulkan backend", "Linux ROCm backend", "Mac backend"].includes(check.label))
|
||||
.map((check) => `${check.label} is missing or not writable.`);
|
||||
if (!backendInstalled) {
|
||||
issues.push(`No ${osPlatform === "win32" ? "Windows" : osPlatform === "darwin" ? "macOS" : "Linux"} backend binary is installed.`);
|
||||
@@ -513,20 +565,35 @@ function hasNvidiaGpu() {
|
||||
}
|
||||
}
|
||||
|
||||
function hasAmdGpu() {
|
||||
const info = getGpuInfo().name.toLowerCase();
|
||||
return info.includes("amd") || info.includes("advanced micro devices") || info.includes("radeon");
|
||||
}
|
||||
|
||||
function getBackendOptions() {
|
||||
if (cachedBackendOptions) return cachedBackendOptions;
|
||||
|
||||
const cudaAvailable = osPlatform === "win32" && hasNvidiaGpu() && backendAccepts(BACKEND_PATHS.cuda, "cuda");
|
||||
const cudaInstalled = osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.cuda);
|
||||
const cudaInstalled = (osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.cuda)) ||
|
||||
(osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxCuda));
|
||||
const cudaAvailable = cudaInstalled && hasNvidiaGpu() && backendAccepts(
|
||||
osPlatform === "win32" ? BACKEND_PATHS.cuda : BACKEND_PATHS.linuxCuda,
|
||||
"cuda"
|
||||
);
|
||||
const vulkanInstalled = (osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.vulkan)) ||
|
||||
(osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linux));
|
||||
(osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxVulkan));
|
||||
const vulkanAvailable = vulkanInstalled && backendAccepts(
|
||||
osPlatform === "win32" ? BACKEND_PATHS.vulkan : BACKEND_PATHS.linux,
|
||||
osPlatform === "win32" ? BACKEND_PATHS.vulkan : BACKEND_PATHS.linuxVulkan,
|
||||
"vulkan"
|
||||
);
|
||||
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 options = [{ id: "cpu", label: "CPU", 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 });
|
||||
|
||||
const unavailable = [];
|
||||
if (vulkanInstalled && !vulkanAvailable) {
|
||||
unavailable.push({ id: "vulkan", label: "Vulkan GPU", reason: "Installed, but this binary did not register a Vulkan backend on this machine." });
|
||||
@@ -534,6 +601,10 @@ function getBackendOptions() {
|
||||
if (cudaInstalled && !cudaAvailable) {
|
||||
unavailable.push({ id: "cuda", label: "CUDA GPU", reason: "Installed, but CUDA backend validation failed." });
|
||||
}
|
||||
if (rocmInstalled && !rocmAvailable) {
|
||||
unavailable.push({ id: "rocm", label: "ROCm GPU (AMD)", reason: "Installed, but ROCm backend validation failed." });
|
||||
}
|
||||
|
||||
let defaultBackend = "cpu";
|
||||
if (cudaAvailable) {
|
||||
const gpuName = String(getGpuInfo().name).toLowerCase();
|
||||
@@ -543,6 +614,8 @@ function getBackendOptions() {
|
||||
} else {
|
||||
defaultBackend = "cuda";
|
||||
}
|
||||
} else if (rocmAvailable) {
|
||||
defaultBackend = "rocm";
|
||||
} else if (vulkanAvailable) {
|
||||
defaultBackend = "vulkan";
|
||||
}
|
||||
@@ -552,6 +625,7 @@ function getBackendOptions() {
|
||||
unavailable,
|
||||
cudaAvailable,
|
||||
vulkanAvailable,
|
||||
rocmAvailable,
|
||||
defaultBackendType: defaultBackend,
|
||||
};
|
||||
return cachedBackendOptions;
|
||||
@@ -560,17 +634,36 @@ function getBackendOptions() {
|
||||
function backendAccepts(binaryPath, backendName) {
|
||||
if (!binaryPath || !fs.existsSync(binaryPath)) return false;
|
||||
try {
|
||||
const result = spawnSync(binaryPath, [
|
||||
const probeArgs = [
|
||||
"--backend", backendName,
|
||||
"--params-backend", backendName,
|
||||
"--model", path.join(MODELS, "__backend_probe_missing__.safetensors"),
|
||||
"--listen-port", "18082",
|
||||
], { encoding: "utf8", timeout: 5000 });
|
||||
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
||||
if (output.includes("backend config failed") || output.includes(`backend '${backendName}' was not found`)) {
|
||||
];
|
||||
let result = spawnSync(binaryPath, probeArgs, { encoding: "utf8", timeout: 5000 });
|
||||
let output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
||||
|
||||
// Some binaries do not support --backend. If we see "unknown argument",
|
||||
// retry without backend flags so we can still verify the binary launches.
|
||||
if (output.includes("unknown argument") && output.includes("--backend")) {
|
||||
const fallbackArgs = [
|
||||
"--model", path.join(MODELS, "__backend_probe_missing__.safetensors"),
|
||||
"--listen-port", "18082",
|
||||
];
|
||||
result = spawnSync(binaryPath, fallbackArgs, { encoding: "utf8", timeout: 5000 });
|
||||
output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
||||
}
|
||||
|
||||
const lower = output.toLowerCase();
|
||||
if (lower.includes("backend config failed") || output.includes(`backend '${backendName}' was not found`)) {
|
||||
return false;
|
||||
}
|
||||
return output.includes("file") || output.includes("not found") || output.includes("loading model");
|
||||
// Reject binaries that fail at the dynamic linker / glibc level.
|
||||
if (lower.includes("glibc") || lower.includes("libc.so") || lower.includes("libstdc++") || lower.includes("cannot open shared object")) {
|
||||
return false;
|
||||
}
|
||||
// A healthy binary prints project-specific log lines when it tries to load the model.
|
||||
return lower.includes("stable-diffusion.cpp") || lower.includes("loading model");
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -578,12 +671,28 @@ function backendAccepts(binaryPath, backendName) {
|
||||
|
||||
function selectBackendPath(useGpu, backendType = "auto") {
|
||||
const resolvedType = resolveBackendType(useGpu, backendType);
|
||||
if (osPlatform === "win32" && resolvedType === "cuda" && fs.existsSync(BACKEND_PATHS.cuda)) {
|
||||
return BACKEND_PATHS.cuda;
|
||||
if (osPlatform === "win32") {
|
||||
if (resolvedType === "cuda" && fs.existsSync(BACKEND_PATHS.cuda)) return BACKEND_PATHS.cuda;
|
||||
if (fs.existsSync(BACKEND_PATHS.vulkan)) return BACKEND_PATHS.vulkan;
|
||||
return BACKEND_PATH;
|
||||
}
|
||||
if (osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.vulkan)) {
|
||||
return BACKEND_PATHS.vulkan;
|
||||
if (osPlatform === "linux") {
|
||||
let finalType = resolvedType;
|
||||
if (finalType === "cuda") {
|
||||
if (fs.existsSync(BACKEND_PATHS.linuxCuda)) {
|
||||
return BACKEND_PATHS.linuxCuda;
|
||||
}
|
||||
finalType = "vulkan";
|
||||
}
|
||||
if (finalType === "rocm" && fs.existsSync(BACKEND_PATHS.linuxRocm)) return BACKEND_PATHS.linuxRocm;
|
||||
if (resolvedType === "vulkan" && fs.existsSync(BACKEND_PATHS.linuxVulkan)) return BACKEND_PATHS.linuxVulkan;
|
||||
if (resolvedType === "cpu" && fs.existsSync(BACKEND_PATHS.linuxCpu)) return BACKEND_PATHS.linuxCpu;
|
||||
if (fs.existsSync(BACKEND_PATHS.linuxVulkan)) return BACKEND_PATHS.linuxVulkan;
|
||||
if (fs.existsSync(BACKEND_PATHS.linuxCpu)) return BACKEND_PATHS.linuxCpu;
|
||||
return BACKEND_PATH;
|
||||
}
|
||||
// macOS fallback
|
||||
if (fs.existsSync(BACKEND_PATHS.mac)) return BACKEND_PATHS.mac;
|
||||
return BACKEND_PATH;
|
||||
}
|
||||
|
||||
@@ -598,6 +707,7 @@ function getBackendMode(backendPath, useGpu, backendType = "auto") {
|
||||
if (useGpu === false || backendType === "cpu") return "CPU";
|
||||
const name = path.basename(backendPath || "").toLowerCase();
|
||||
if (name.includes("cuda")) return "CUDA GPU";
|
||||
if (name.includes("rocm")) return "ROCm GPU";
|
||||
if (name.includes("vulkan")) return "Vulkan GPU";
|
||||
return "GPU";
|
||||
}
|
||||
@@ -758,12 +868,20 @@ async function startBackend(settings = {}) {
|
||||
"--sampler-rng", "cpu",
|
||||
);
|
||||
} else if (requestedBackend === "cuda") {
|
||||
// CUDA is only supported on Windows. Linux has no reliable prebuilt CUDA binary.
|
||||
args.push(
|
||||
"--backend", "cuda0",
|
||||
"--params-backend", "cuda0",
|
||||
"--rng", "cuda",
|
||||
"--sampler-rng", "cuda",
|
||||
);
|
||||
} else if (requestedBackend === "rocm") {
|
||||
args.push(
|
||||
"--backend", "rocm0",
|
||||
"--params-backend", "rocm0",
|
||||
"--rng", "cpu",
|
||||
"--sampler-rng", "cpu",
|
||||
);
|
||||
}
|
||||
|
||||
if (currentSettings.vaeTiling) {
|
||||
@@ -773,10 +891,30 @@ async function startBackend(settings = {}) {
|
||||
args.push("--vae-on-cpu");
|
||||
}
|
||||
|
||||
// Build environment for Linux backends so bundled .so libraries are found.
|
||||
// Official Linux releases ship libstable-diffusion.so next to the executable.
|
||||
const spawnEnv = { ...process.env };
|
||||
if (osPlatform === "linux") {
|
||||
const extraLibs = [];
|
||||
if (requestedBackend === "rocm") {
|
||||
extraLibs.push(path.dirname(BACKEND_PATHS.linuxRocm));
|
||||
} else if (requestedBackend === "vulkan") {
|
||||
extraLibs.push(path.dirname(BACKEND_PATHS.linuxVulkan));
|
||||
} else if (requestedBackend === "cpu") {
|
||||
extraLibs.push(path.dirname(BACKEND_PATHS.linuxCpu));
|
||||
} else if (requestedBackend === "cuda") {
|
||||
extraLibs.push(path.dirname(BACKEND_PATHS.linuxCuda));
|
||||
}
|
||||
if (extraLibs.length > 0) {
|
||||
const existing = spawnEnv.LD_LIBRARY_PATH || "";
|
||||
spawnEnv.LD_LIBRARY_PATH = extraLibs.join(":") + (existing ? ":" + existing : "");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(" [backend] Starting:", path.basename(backendPath), args.join(" "));
|
||||
backendReady = false;
|
||||
|
||||
backendProc = spawn(backendPath, args, { stdio: "pipe" });
|
||||
backendProc = spawn(backendPath, args, { stdio: "pipe", env: spawnEnv });
|
||||
startBackendReadyPoll();
|
||||
|
||||
backendProc.stdout.on("data", d => {
|
||||
@@ -873,6 +1011,10 @@ async function startBackend(settings = {}) {
|
||||
backendLoadState.backendMode = "Vulkan GPU";
|
||||
currentSettings.backendMode = "Vulkan GPU";
|
||||
}
|
||||
if (cleanOutput.includes("ggml_hip") || cleanOutput.includes("ggml_rocm")) {
|
||||
backendLoadState.backendMode = "ROCm GPU";
|
||||
currentSettings.backendMode = "ROCm GPU";
|
||||
}
|
||||
if (cleanOutput.includes("[ERROR]")) {
|
||||
const nextError = describeBackendError(cleanOutput.trim(), currentSettings.model);
|
||||
const hasSpecificFileError = String(backendError || "").includes("incomplete or corrupted");
|
||||
|
||||
Executable
+327
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local AI Image Generator - Linux 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 "$SCRIPT_DIR")"
|
||||
APP_DIR="$ROOT_DIR/app"
|
||||
FRONTEND_DIR="$APP_DIR/frontend"
|
||||
TOOLS_DIR="$APP_DIR/tools"
|
||||
NODE_DIR="$TOOLS_DIR/node-linux"
|
||||
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"
|
||||
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() {
|
||||
clear 2>/dev/null || true
|
||||
echo ""
|
||||
echo " ============================================================"
|
||||
echo " LOCAL AI IMAGE GENERATOR - Linux 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"
|
||||
tar -xf "$tar_path" -C "$dest"
|
||||
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
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
print_header
|
||||
|
||||
check_glibc
|
||||
|
||||
TOTAL_STEPS=4
|
||||
|
||||
# ── Step 1: Portable Node.js ────────────────────────────────────────────────
|
||||
print_step 1 $TOTAL_STEPS "Setting up portable Node.js (app/tools/node-linux/)"
|
||||
|
||||
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*-linux-x64' | head -n 1)"
|
||||
if [[ -d "$EXTRACTED_DIR" ]]; then
|
||||
rm -rf "$NODE_DIR"
|
||||
mv "$EXTRACTED_DIR" "$NODE_DIR"
|
||||
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 ───────────────────────────────────
|
||||
VENDOR="$(detect_gpu_vendor)"
|
||||
print_step 2 $TOTAL_STEPS "Detecting GPU vendor: ${VENDOR:-none}"
|
||||
|
||||
mkdir -p "$BACKEND_DIR"
|
||||
|
||||
# CPU backend (always)
|
||||
CPU_BACKEND_DIR="$BACKEND_DIR/cpu"
|
||||
if [[ ! -f "$CPU_BACKEND_DIR/sd-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"
|
||||
chmod +x "$CPU_BACKEND_DIR/sd-cpu" 2>/dev/null || true
|
||||
print_ok "CPU backend installed."
|
||||
else
|
||||
print_ok "CPU backend already ready."
|
||||
fi
|
||||
|
||||
# Vulkan backend (always - cross-vendor GPU fallback)
|
||||
VULKAN_BACKEND_DIR="$BACKEND_DIR/vulkan"
|
||||
if [[ ! -f "$VULKAN_BACKEND_DIR/sd-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"
|
||||
chmod +x "$VULKAN_BACKEND_DIR/sd-vulkan" 2>/dev/null || true
|
||||
print_ok "Vulkan backend installed."
|
||||
else
|
||||
print_ok "Vulkan backend already ready."
|
||||
fi
|
||||
|
||||
# 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" ]]; 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"
|
||||
chmod +x "$ROCM_BACKEND_DIR/sd-rocm" 2>/dev/null || true
|
||||
print_ok "ROCm backend installed."
|
||||
else
|
||||
print_ok "ROCm backend already ready."
|
||||
fi
|
||||
fi
|
||||
|
||||
# CUDA backend on Linux is intentionally disabled.
|
||||
# upstream leejet/stable-diffusion.cpp does not publish official Linux CUDA binaries
|
||||
# (see https://github.com/leejet/stable-diffusion.cpp/issues/1291), and the only
|
||||
# third-party build we evaluated (leaxer-ai/leaxer-stable-diffusion v0.1.0) loads
|
||||
# models but segfaults during generation. Until a reliable Linux CUDA binary is
|
||||
# available, Linux NVIDIA systems fall back to the Vulkan backend, which uses the
|
||||
# same GPU via the vendor's Vulkan driver.
|
||||
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
|
||||
|
||||
# ── Step 3: npm install ─────────────────────────────────────────────────────
|
||||
print_step 3 $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
|
||||
|
||||
cd "$FRONTEND_DIR"
|
||||
export PATH="$NODE_DIR/bin:$PATH"
|
||||
|
||||
if "$NPM_BIN" install --prefer-offline; then
|
||||
print_ok "Dependencies installed!"
|
||||
else
|
||||
print_fail "npm install failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Step 4: Build frontend ──────────────────────────────────────────────────
|
||||
print_step 4 $TOTAL_STEPS "Building frontend -> app/dist/"
|
||||
|
||||
if "$NPM_BIN" run build; then
|
||||
print_ok "Frontend built!"
|
||||
else
|
||||
print_fail "Frontend build failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Done ────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ============================================================"
|
||||
echo " Setup complete! Run ./start.sh to launch."
|
||||
echo " ============================================================"
|
||||
echo ""
|
||||
|
||||
if [[ $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 ""
|
||||
fi
|
||||
Reference in new issue
Block a user