Merge pull request #27 from techjarves/development

Development
This commit is contained in:
Tech Jarves authored and GitHub committed 2026-06-27 09:40:44 +05:30
commit 1cfd5efe16
15 files changed
+828 -184

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ app/config/
app/cache/
app/dist/
app/outputs/
app/chat-history/
app/transcriptions/
app/tts-outputs/
app/tts-cache/
+46
View File
@@ -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
---
## <a id="supported-models"></a>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.
---
## <a id="folder-architecture"></a>📁 Folder Architecture
```
@@ -195,6 +236,11 @@ Ensure you have a modern web browser installed. Follow the quick guide below for
<p>Ensure your AMD GPU hardware and host kernel are fully compatible with ROCm 7.13. If ROCm fails to initialize correctly, the application will automatically fall back to Vulkan acceleration.</p>
</details>
<details>
<summary><strong> Linux uses the integrated GPU instead of the discrete GPU</strong></summary>
<p>On dual-GPU Linux systems, Vulkan device order can put the integrated Intel GPU at <code>vulkan0</code> and the discrete AMD/NVIDIA GPU at <code>vulkan1</code>. The launcher now tries to prefer a discrete Vulkan device when <code>vulkaninfo --summary</code> is available. To force a device manually, start the app with <code>SD_VULKAN_DEVICE=vulkan1 ./linux.sh</code> or use another index such as <code>vulkan0</code>/<code>vulkan2</code>.</p>
</details>
<details>
<summary><strong> Windows exits with code <code>3221225781</code> (0xC0000135)</strong></summary>
<p>This code means Windows could not locate a required backend DLL:</p>
+57 -20
View File
@@ -7,7 +7,7 @@ import Settings from "./components/Settings";
import TextChat from "./components/TextChat";
import SpeechTranscriber from "./components/SpeechTranscriber";
import TextToSpeech from "./components/TextToSpeech";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listLlmConversations, saveLlmConversation, deleteLlmConversation, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api";
import "./App.css";
function App() {
@@ -240,17 +240,6 @@ function App() {
const [selectedTtsOutput, setSelectedTtsOutput] = useState(null);
const [showTtsHistory, setShowTtsHistory] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("chat_conversations");
if (saved) {
try {
setConversations(JSON.parse(saved));
} catch (_) {
localStorage.removeItem("chat_conversations");
}
}
}, []);
const sanitizeMessageForStorage = (message) => {
if (!Array.isArray(message?.content)) return message;
return {
@@ -272,37 +261,83 @@ function App() {
: [],
});
const persistConversations = (list) => {
const compactList = list.map(sanitizeConversationForStorage);
const persistConversation = (conversation) => {
const compactConversation = sanitizeConversationForStorage(conversation);
try {
localStorage.setItem("chat_conversations", JSON.stringify(compactList));
saveLlmConversation(compactConversation).catch((err) => {
console.warn("Could not save chat history to app folder:", err);
});
} catch (err) {
console.warn("Could not save chat history:", err);
}
};
useEffect(() => {
let cancelled = false;
async function loadConversations() {
let legacyConversations = [];
const legacySaved = localStorage.getItem("chat_conversations");
if (legacySaved) {
try {
legacyConversations = JSON.parse(legacySaved);
} catch (_) {
localStorage.removeItem("chat_conversations");
}
}
try {
const diskConversations = await listLlmConversations();
const diskIds = new Set(diskConversations.map((conversation) => conversation.id));
const missingLegacy = legacyConversations
.map(sanitizeConversationForStorage)
.filter((conversation) => conversation.id && !diskIds.has(conversation.id));
if (missingLegacy.length > 0) {
await Promise.all(missingLegacy.map((conversation) => saveLlmConversation(conversation)));
}
const merged = [...diskConversations, ...missingLegacy]
.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
if (!cancelled) setConversations(merged);
if (legacySaved) localStorage.removeItem("chat_conversations");
} catch (err) {
console.warn("Could not load chat history from app folder:", err);
if (!cancelled && legacyConversations.length > 0) {
setConversations(legacyConversations.map(sanitizeConversationForStorage));
}
}
}
loadConversations();
return () => {
cancelled = true;
};
}, []);
const saveConversationState = useCallback((id, msgs, modelName, newTitle = null) => {
setConversations((prev) => {
const list = [...prev];
const idx = list.findIndex(c => c.id === id);
let conversation;
if (idx !== -1) {
list[idx] = {
conversation = {
...list[idx],
messages: msgs,
timestamp: Date.now(),
model: modelName,
...(newTitle ? { title: newTitle } : {})
};
list[idx] = conversation;
} else {
list.unshift({
conversation = {
id,
title: newTitle || "Chat Session",
model: modelName,
messages: msgs,
timestamp: Date.now()
});
};
list.unshift(conversation);
}
persistConversations(list);
persistConversation(conversation);
return list;
});
}, []);
@@ -311,9 +346,11 @@ function App() {
if (e) e.stopPropagation();
setConversations((prev) => {
const filtered = prev.filter((c) => c.id !== id);
persistConversations(filtered);
return filtered;
});
deleteLlmConversation(id).catch((err) => {
console.warn("Could not delete chat history from app folder:", err);
});
if (activeConversationId === id) {
setActiveConversationId(null);
}
+14 -2
View File
@@ -294,6 +294,19 @@ function Generator({
const step = decoding ? (progress.steps || constraints.steps || 20) : (progress.step || 0);
const steps = progress.steps || constraints.steps || 20;
const speed = decoding ? "" : (progress.speed || "");
const numericSpeed = parseFloat(speed);
const backendMode = String(progress.backendMode || "").toLowerCase();
const backendDevice = String(progress.backendDevice || "").toLowerCase();
const isConfirmedGpuBackend = (
backendMode.includes("gpu") ||
backendMode.includes("vulkan") ||
backendMode.includes("cuda") ||
backendMode.includes("rocm") ||
backendMode.includes("metal") ||
backendDevice.includes("vulkan") ||
backendDevice.includes("cuda") ||
backendDevice.includes("rocm")
);
if (decoding || progress.steps > 0) {
hasRealGenerationStepRef.current = true;
@@ -324,7 +337,6 @@ function Generator({
if (decoding) {
remaining = 3;
} else {
const numericSpeed = parseFloat(speed);
if (!isNaN(numericSpeed) && numericSpeed > 0) {
const remainingSteps = Math.max(0, steps - step);
if (speed.includes("it/s")) {
@@ -339,7 +351,7 @@ function Generator({
}
// Detect CPU fallback / slow generation if GPU was requested but it runs slow
if (gpuSelected && !cpuFallbackDetected) {
if (gpuSelected && !cpuFallbackDetected && !isConfirmedGpuBackend) {
const isSlowItS = speed.includes("it/s") && numericSpeed < 0.2;
const isSlowSIt = speed.includes("s/it") && numericSpeed > 5.0;
if (isSlowItS || isSlowSIt) {
+4 -5
View File
@@ -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";
+33 -6
View File
@@ -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();
}
@@ -362,6 +365,31 @@ export async function listLlmModels() {
return (data.models || []).map(normalizeModel);
}
export async function listLlmConversations() {
const res = await fetch("/api/llm/conversations");
const data = await readJsonResponse(res, "The local server returned invalid chat history.");
return data.conversations || [];
}
export async function saveLlmConversation(conversation) {
const res = await fetch("/api/llm/save-conversation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ conversation }),
});
const data = await readJsonResponse(res, "The local server returned an invalid chat save response.");
return data.conversation;
}
export async function deleteLlmConversation(id) {
const res = await fetch("/api/llm/delete-conversation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
return await readJsonResponse(res, "The local server returned an invalid chat delete response.");
}
export async function searchHuggingFaceModels(query = "", filters = [], page = 1) {
const params = new URLSearchParams();
if (query.trim()) params.set("query", query.trim());
@@ -1162,8 +1190,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",
+13 -1
View File
@@ -78,6 +78,18 @@ if (Test-Path $ttsModelsDir) {
Write-Host " >> Preserving TTS models in app/tts-models." -ForegroundColor Cyan
}
# Preserve generated image outputs
$outputsDir = Join-Path $appDir "outputs"
if (Test-Path $outputsDir) {
Write-Host " >> Preserving generated image outputs in app/outputs." -ForegroundColor Cyan
}
# Preserve chat history
$chatHistoryDir = Join-Path $appDir "chat-history"
if (Test-Path $chatHistoryDir) {
Write-Host " >> Preserving chat history in app/chat-history." -ForegroundColor Cyan
}
# Preserve transcriptions
$transcriptionsDir = Join-Path $appDir "transcriptions"
if (Test-Path $transcriptionsDir) {
@@ -125,7 +137,7 @@ if (Test-Path $lockFile) {
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
Write-Host " Reset complete. Models, generated images, chat history, transcripts, TTS outputs, TTS cache, and OpenVINO models were preserved." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+11 -1
View File
@@ -72,6 +72,16 @@ if [[ -d "$APP_DIR/tts-models" ]]; then
echo " >> Preserving TTS models in app/tts-models."
fi
# Preserve generated image outputs
if [[ -d "$APP_DIR/outputs" ]]; then
echo " >> Preserving generated image outputs in app/outputs."
fi
# Preserve chat history
if [[ -d "$APP_DIR/chat-history" ]]; then
echo " >> Preserving chat history in app/chat-history."
fi
# Preserve transcriptions
if [[ -d "$APP_DIR/transcriptions" ]]; then
echo " >> Preserving speech transcripts in app/transcriptions."
@@ -111,7 +121,7 @@ fi
echo ""
echo " ============================================================"
echo " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved."
echo " Reset complete. Models, generated images, chat history, transcripts, TTS outputs, TTS cache, and OpenVINO models were preserved."
echo " ============================================================"
echo ""
read -rp " Press Enter to close..."
+265 -2
View File
@@ -94,6 +94,10 @@ const LLM_MODELS = path.join(ROOT, "app", "llm-models");
if (!fs.existsSync(LLM_MODELS)) {
fs.mkdirSync(LLM_MODELS, { recursive: true });
}
const CHAT_HISTORY = path.join(ROOT, "app", "chat-history");
if (!fs.existsSync(CHAT_HISTORY)) {
fs.mkdirSync(CHAT_HISTORY, { recursive: true });
}
const LLM_BACKEND_PATHS = {
winCuda: path.join(ROOT, "app", "llm-backend", "win", "cuda", "llama-server.exe"),
winVulkan: path.join(ROOT, "app", "llm-backend", "win", "vulkan", "llama-server.exe"),
@@ -587,6 +591,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() {
@@ -2662,6 +2710,9 @@ function getSetupPaths() {
cpuBackend: BACKEND_PATHS.cpu,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
} else if (osPlatform === "darwin") {
return {
@@ -2671,6 +2722,9 @@ function getSetupPaths() {
macBackend: BACKEND_PATHS.mac,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
} else {
// Linux / WSL
@@ -2683,6 +2737,9 @@ function getSetupPaths() {
linuxRocmBackend: BACKEND_PATHS.linuxRocm,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
}
}
@@ -2695,6 +2752,9 @@ async function getHealth() {
getPathInfo("Frontend build", paths.distIndex),
getDirInfo("Models folder", paths.models),
getDirInfo("Outputs folder", paths.outputs),
getDirInfo("Chat history folder", paths.chatHistory),
getDirInfo("Transcriptions folder", paths.transcriptions),
getDirInfo("TTS outputs folder", paths.ttsOutputs),
];
if (osPlatform === "win32") {
@@ -2767,6 +2827,16 @@ async function getHealth() {
totalBytes: getPathSize(OUTPUTS),
totalSize: formatBytes(getPathSize(OUTPUTS)),
},
chat: {
count: fs.existsSync(CHAT_HISTORY) ? fs.readdirSync(CHAT_HISTORY).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
totalBytes: getPathSize(CHAT_HISTORY),
totalSize: formatBytes(getPathSize(CHAT_HISTORY)),
},
speech: {
transcriptions: fs.existsSync(TRANSCRIPTIONS) ? fs.readdirSync(TRANSCRIPTIONS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
totalBytes: getPathSize(TRANSCRIPTIONS),
totalSize: formatBytes(getPathSize(TRANSCRIPTIONS)),
},
tts: {
models: fs.existsSync(TTS_MODELS) ? fs.readdirSync(TTS_MODELS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
outputs: fs.existsSync(TTS_OUTPUTS) ? fs.readdirSync(TTS_OUTPUTS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
@@ -2783,6 +2853,8 @@ function addCleanupCandidate(candidates, id, targetPath, reason, options = {}) {
if (!options.allowUserData && (
pathInside(targetPath, MODELS) ||
pathInside(targetPath, OUTPUTS) ||
pathInside(targetPath, CHAT_HISTORY) ||
pathInside(targetPath, TRANSCRIPTIONS) ||
pathInside(targetPath, TTS_MODELS) ||
pathInside(targetPath, TTS_OUTPUTS)
)) return;
@@ -3285,6 +3357,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 +4405,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 +4434,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 +4453,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 +4593,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();
@@ -4829,6 +4942,8 @@ let generationState = {
steps: 0,
speed: "",
decoding: false,
backendMode: "",
backendDevice: "",
};
function resetGenerationState() {
@@ -4838,6 +4953,8 @@ function resetGenerationState() {
steps: 0,
speed: "",
decoding: false,
backendMode: "",
backendDevice: "",
};
}
@@ -5217,6 +5334,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) {
@@ -5297,6 +5430,101 @@ function safeOutputName(value) {
.slice(0, 120);
}
function sanitizeChatMessageForStorage(message) {
if (!message || typeof message !== "object") return null;
const role = String(message.role || "").trim();
if (!["system", "user", "assistant"].includes(role)) return null;
let content = message.content;
if (Array.isArray(content)) {
content = content.map((item) => {
if (!item || typeof item !== "object") return item;
if (item.type === "image_url") {
return { type: "text", text: "[Attached image omitted from saved chat history]" };
}
return item;
});
} else if (typeof content !== "string") {
content = content === undefined || content === null ? "" : String(content);
}
return {
...message,
role,
content,
};
}
function sanitizeChatConversationForStorage(conversation = {}) {
const now = Date.now();
const id = safeOutputName(conversation.id || `chat_${now}`) || `chat_${now}`;
const messages = Array.isArray(conversation.messages)
? conversation.messages.map(sanitizeChatMessageForStorage).filter(Boolean)
: [];
const timestamp = Number(conversation.timestamp) || now;
return {
id,
title: String(conversation.title || "Chat Session").slice(0, 160),
model: String(conversation.model || ""),
messages,
timestamp,
createdAt: conversation.createdAt || new Date(timestamp).toISOString(),
updatedAt: new Date().toISOString(),
};
}
function getChatConversationPath(id) {
const safeId = safeOutputName(id);
if (!safeId) throw new Error("A chat id is required.");
const filePath = path.join(CHAT_HISTORY, `${safeId}.json`);
if (!pathInside(filePath, CHAT_HISTORY)) {
throw new Error("Invalid chat id.");
}
return filePath;
}
function saveChatConversation(conversation = {}) {
const saved = sanitizeChatConversationForStorage(conversation);
const filePath = getChatConversationPath(saved.id);
fs.writeFileSync(filePath, JSON.stringify(saved, null, 2), "utf8");
return saved;
}
function listChatConversations() {
try {
return fs.readdirSync(CHAT_HISTORY)
.filter((file) => file.toLowerCase().endsWith(".json"))
.map((file) => {
try {
const filePath = path.join(CHAT_HISTORY, file);
if (!pathInside(filePath, CHAT_HISTORY)) return null;
const conversation = JSON.parse(fs.readFileSync(filePath, "utf8"));
const stat = fs.statSync(filePath);
return {
...conversation,
filename: file,
modifiedAt: stat.mtime.toISOString(),
sizeBytes: stat.size,
size: formatBytes(stat.size),
};
} catch (_) {
return null;
}
})
.filter(Boolean)
.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
} catch (_) {
return [];
}
}
function deleteChatConversation(id) {
const filePath = getChatConversationPath(id);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
function saveGeneratedOutput(imageDataUrl, metadata = {}) {
const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/);
if (!match) {
@@ -5483,6 +5711,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") {
@@ -5591,6 +5822,34 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { ok: true, model: filename, settings: updated });
}
if (req.url === "/api/llm/conversations" && req.method === "GET") {
return json(res, 200, { ok: true, conversations: listChatConversations() });
}
if (req.url === "/api/llm/save-conversation" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
const conversation = saveChatConversation(body.conversation || body);
return json(res, 200, { ok: true, conversation });
} catch (err) {
console.error(" [api] Failed to save chat conversation:", err);
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
if (req.url === "/api/llm/delete-conversation" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
const deleted = deleteChatConversation(body.id);
return json(res, 200, { ok: true, deleted });
} catch (err) {
console.error(" [api] Failed to delete chat conversation:", err);
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
if (req.url === "/api/llm/models" && req.method === "GET") {
return json(res, 200, { models: getLlmModels() });
}
@@ -6589,7 +6848,11 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
// GET /api/generation-progress
if (req.url === "/api/generation-progress" && req.method === "GET") {
return json(res, 200, generationState);
return json(res, 200, {
...generationState,
backendMode: generationState.backendMode || currentSettings.backendMode || "",
backendDevice: generationState.backendDevice || currentSettings.backendDevice || "",
});
}
if (req.url === "/api/openvino-generate" && req.method === "POST") {
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import tarfile
import argparse
def main():
parser = argparse.ArgumentParser(description="Extract tar archives, resolving symlinks as copies if symlinks are not supported.")
parser.add_argument("--archive", required=True, help="Path to the tar archive")
parser.add_argument("--dest", required=True, help="Destination directory")
parser.add_argument("--strip-components", type=int, default=0, help="Number of leading components to strip")
args = parser.parse_args()
archive_path = args.archive
dest_dir = args.dest
strip_components = args.strip_components
if not os.path.exists(archive_path):
print(f"Error: Archive not found: {archive_path}", file=sys.stderr)
sys.exit(1)
os.makedirs(dest_dir, exist_ok=True)
# Check if filesystem supports symlinks in dest_dir
test_link = os.path.join(dest_dir, f".test_symlink_{os.getpid()}")
use_symlinks = True
try:
if os.path.lexists(test_link):
os.unlink(test_link)
os.symlink("test_target", test_link)
os.unlink(test_link)
except OSError:
use_symlinks = False
print(f"Extracting {archive_path} to {dest_dir} (strip-components={strip_components}, use_symlinks={use_symlinks})...")
try:
with tarfile.open(archive_path, "r") as tar:
members = tar.getmembers()
# Map of stripped_path -> target (for symlinks)
symlinks_dict = {}
regular_members = []
for member in members:
parts = member.name.split('/')
# Strip leading components
if len(parts) <= strip_components:
continue
stripped_name = '/'.join(parts[strip_components:])
if not stripped_name:
continue
# Update the member name in-place
member.name = stripped_name
if member.issym() or member.islnk():
symlinks_dict[member.name] = member.linkname
else:
regular_members.append(member)
# Extract regular files and directories first
tar.extractall(path=dest_dir, members=regular_members)
# Now handle symlinks
for link_path, link_target in symlinks_dict.items():
full_link_path = os.path.join(dest_dir, link_path)
# Resolve final target recursively in case of nested symlinks
link_dir = os.path.dirname(full_link_path)
# Recursive resolver helper
def resolve_final_target(current_path, current_target):
# target_abs is the absolute path to the target file/dir
target_abs = os.path.normpath(os.path.join(os.path.dirname(current_path), current_target))
# Check if target_abs is inside dest_dir
rel_to_dest = os.path.relpath(target_abs, dest_dir)
if rel_to_dest in symlinks_dict:
next_target = symlinks_dict[rel_to_dest]
return resolve_final_target(target_abs, next_target)
return target_abs
target_abs = resolve_final_target(full_link_path, link_target)
# Ensure parent directory exists
os.makedirs(link_dir, exist_ok=True)
if os.path.lexists(full_link_path):
try:
os.unlink(full_link_path)
except OSError:
shutil.rmtree(full_link_path, ignore_errors=True)
if use_symlinks:
try:
os.symlink(link_target, full_link_path)
except OSError:
# Fallback to copy if symlink creation fails unexpectedly
copy_fallback(target_abs, full_link_path)
else:
copy_fallback(target_abs, full_link_path)
print("Extraction completed successfully.")
except Exception as e:
print(f"Error extracting archive: {e}", file=sys.stderr)
sys.exit(1)
def copy_fallback(src, dst):
if not os.path.exists(src):
# Target doesn't exist. Maybe it's an external link or wasn't extracted.
print(f"Warning: Symlink target does not exist, skipping copy: {src}", file=sys.stderr)
return
try:
if os.path.isdir(src):
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
print(f"Created fallback copy: {dst} -> {src}")
except Exception as e:
print(f"Error copying {src} to {dst}: {e}", file=sys.stderr)
if __name__ == "__main__":
main()
+66 -41
View File
@@ -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 }
}
}
+5 -1
View File
@@ -25,7 +25,11 @@ download_and_extract() {
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
if command -v python3 >/dev/null 2>&1; then
python3 "$SCRIPT_DIR/extract_tar.py" --archive "$archive" --dest "$dest" --strip-components=1
else
tar -xzf "$archive" -C "$dest" --strip-components=1
fi
rm -f "$archive"
chmod +x "$dest"/llama-* 2>/dev/null || true
+5 -1
View File
@@ -25,7 +25,11 @@ download_and_extract() {
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
if command -v python3 >/dev/null 2>&1; then
python3 "$SCRIPT_DIR/extract_tar.py" --archive "$archive" --dest "$dest" --strip-components=1
else
tar -xzf "$archive" -C "$dest" --strip-components=1
fi
rm -f "$archive"
chmod +x "$dest"/whisper-* "$dest"/main "$dest"/server 2>/dev/null || true
+121 -90
View File
@@ -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."
}
}
}
+63 -14
View File
@@ -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