add cross-platform setup scripts and frontend boilerplate for local AI image generation

This commit is contained in:
techjarves committed 2026-06-14 17:14:20 +05:30
1 parent 9dfc0f38c4
commit 43a85f8d24
17 files changed
+1163 -76

No files matched your search

+33 -18
View File
@@ -7,7 +7,7 @@ $appDir = Join-Path $rootDir "app"
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host " Resetting Local-AI-Image-Generator..." -ForegroundColor Yellow
Write-Host " Resetting Local AI Studio..." -ForegroundColor Yellow
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host ""
@@ -21,10 +21,17 @@ if (Test-Path $toolsDir) {
# Delete backend
$backendDir = Join-Path $appDir "backend"
if (Test-Path $backendDir) {
Write-Host " >> Removing backend binaries..." -ForegroundColor Cyan
Write-Host " >> Removing image backend binaries..." -ForegroundColor Cyan
Remove-Item $backendDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete llama.cpp backend
$llmBackendDir = Join-Path $appDir "llm-backend"
if (Test-Path $llmBackendDir) {
Write-Host " >> Removing llama.cpp text backend binaries..." -ForegroundColor Cyan
Remove-Item $llmBackendDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete dist
$distDir = Join-Path $appDir "dist"
if (Test-Path $distDir) {
@@ -32,29 +39,37 @@ if (Test-Path $distDir) {
Remove-Item $distDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Preserve models
# Preserve image models
$modelsDir = Join-Path $appDir "models"
if (Test-Path $modelsDir) {
Write-Host " >> Preserving downloaded models in app/models." -ForegroundColor Cyan
Write-Host " >> Preserving image models in app/models." -ForegroundColor Cyan
}
# Delete node_modules in frontend
$nodeModulesDir = Join-Path $appDir "frontend\node_modules"
if (Test-Path $nodeModulesDir) {
Write-Host " >> Removing frontend node_modules..." -ForegroundColor Cyan
Remove-Item $nodeModulesDir -Recurse -Force -ErrorAction SilentlyContinue
# Preserve text models
$llmModelsDir = Join-Path $appDir "llm-models"
if (Test-Path $llmModelsDir) {
Write-Host " >> Preserving text models in app/llm-models." -ForegroundColor Cyan
}
$nodeModulesMac = Join-Path $appDir "frontend\node_modules_mac"
if (Test-Path $nodeModulesMac) {
Write-Host " >> Removing frontend node_modules_mac..." -ForegroundColor Cyan
Remove-Item $nodeModulesMac -Recurse -Force -ErrorAction SilentlyContinue
# Preserve OpenVINO models
$openVinoModelsDir = Join-Path $appDir "openvino-models"
if (Test-Path $openVinoModelsDir) {
Write-Host " >> Preserving OpenVINO models in app/openvino-models." -ForegroundColor Cyan
}
$nodeModulesLinux = Join-Path $appDir "frontend\node_modules_linux"
if (Test-Path $nodeModulesLinux) {
Write-Host " >> Removing frontend node_modules_linux..." -ForegroundColor Cyan
Remove-Item $nodeModulesLinux -Recurse -Force -ErrorAction SilentlyContinue
# Delete all frontend dependency folders, including platform-specific copies
$frontendDir = Join-Path $appDir "frontend"
Get-ChildItem $frontendDir -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq "node_modules" -or $_.Name -like "node_modules_*" } |
ForEach-Object {
Write-Host " >> Removing frontend $($_.Name)..." -ForegroundColor Cyan
Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
$activeOsFile = Join-Path $frontendDir ".active_modules_os"
if (Test-Path $activeOsFile) {
Write-Host " >> Removing frontend platform marker..." -ForegroundColor Cyan
Remove-Item $activeOsFile -Force -ErrorAction SilentlyContinue
}
@@ -67,7 +82,7 @@ if (Test-Path $lockFile) {
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Reset complete. Models and generated outputs were preserved." -ForegroundColor Green
Write-Host " Reset complete. Image models, text models, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+29 -16
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Local AI Image Generator - Linux/macOS Reset Script
# Local AI Studio - Linux/macOS Reset Script
# Resets portable app dependencies/builds while preserving user models and outputs.
#
@@ -12,7 +12,7 @@ APP_DIR="$ROOT_DIR/app"
echo ""
echo " ============================================================"
echo " Resetting Local-AI-Image-Generator..."
echo " Resetting Local AI Studio..."
echo " ============================================================"
echo ""
@@ -24,35 +24,48 @@ fi
# Delete backend
if [[ -d "$APP_DIR/backend" ]]; then
echo " >> Removing backend binaries..."
echo " >> Removing image backend binaries..."
rm -rf "$APP_DIR/backend"
fi
# Delete llama.cpp backend
if [[ -d "$APP_DIR/llm-backend" ]]; then
echo " >> Removing llama.cpp text backend binaries..."
rm -rf "$APP_DIR/llm-backend"
fi
# Delete dist
if [[ -d "$APP_DIR/dist" ]]; then
echo " >> Removing dist/ build folder..."
rm -rf "$APP_DIR/dist"
fi
# Preserve models
# Preserve image models
if [[ -d "$APP_DIR/models" ]]; then
echo " >> Preserving downloaded models in app/models."
echo " >> Preserving image models in app/models."
fi
# Delete node_modules in frontend
if [[ -L "$APP_DIR/frontend/node_modules" || -d "$APP_DIR/frontend/node_modules" ]]; then
echo " >> Removing frontend node_modules..."
rm -rf "$APP_DIR/frontend/node_modules"
# Preserve text models
if [[ -d "$APP_DIR/llm-models" ]]; then
echo " >> Preserving text models in app/llm-models."
fi
if [[ -d "$APP_DIR/frontend/node_modules_mac" ]]; then
echo " >> Removing frontend node_modules_mac..."
rm -rf "$APP_DIR/frontend/node_modules_mac"
# Preserve OpenVINO models
if [[ -d "$APP_DIR/openvino-models" ]]; then
echo " >> Preserving OpenVINO models in app/openvino-models."
fi
if [[ -d "$APP_DIR/frontend/node_modules_linux" ]]; then
echo " >> Removing frontend node_modules_linux..."
rm -rf "$APP_DIR/frontend/node_modules_linux"
# Delete all frontend dependency folders, including platform-specific copies
for modules_dir in "$APP_DIR/frontend/node_modules" "$APP_DIR/frontend"/node_modules_*; do
if [[ -L "$modules_dir" || -d "$modules_dir" ]]; then
echo " >> Removing frontend $(basename "$modules_dir")..."
rm -rf "$modules_dir"
fi
done
if [[ -f "$APP_DIR/frontend/.active_modules_os" ]]; then
echo " >> Removing frontend platform marker..."
rm -f "$APP_DIR/frontend/.active_modules_os"
fi
# Delete package-lock.json in frontend
@@ -63,7 +76,7 @@ fi
echo ""
echo " ============================================================"
echo " Reset complete. Models and generated outputs were preserved."
echo " Reset complete. Image models, text models, OpenVINO models, and generated outputs were preserved."
echo " ============================================================"
echo ""
read -rp " Press Enter to close..."
+326 -14
View File
@@ -18,9 +18,11 @@ function readPort(value, fallback) {
const PORT_FRONTEND = readPort(process.env.PORT || process.env.FRONTEND_PORT, 1420);
const PREFERRED_BACKEND_PORT = readPort(process.env.BACKEND_PORT || process.env.SD_BACKEND_PORT, 8080);
const PREFERRED_LLM_PORT = readPort(process.env.LLM_PORT, 10086);
let PORT_BACKEND = PREFERRED_BACKEND_PORT;
let PORT_LLM = PREFERRED_LLM_PORT;
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
const SERVER_BUILD = "polish-setup-v1";
const SERVER_BUILD = "text-image-v1";
const ROOT = path.join(__dirname, "..");
const DIST = path.join(ROOT, "app", "dist");
const osPlatform = process.platform;
@@ -68,6 +70,18 @@ const MODELS = path.join(ROOT, "app", "models");
if (!fs.existsSync(MODELS)) {
fs.mkdirSync(MODELS, { recursive: true });
}
const LLM_MODELS = path.join(ROOT, "app", "llm-models");
if (!fs.existsSync(LLM_MODELS)) {
fs.mkdirSync(LLM_MODELS, { recursive: true });
}
const LLM_BACKEND_PATHS = {
winVulkan: path.join(ROOT, "app", "llm-backend", "win", "vulkan", "llama-server.exe"),
winCpu: path.join(ROOT, "app", "llm-backend", "win", "cpu", "llama-server.exe"),
linuxVulkan: path.join(ROOT, "app", "llm-backend", "linux", "vulkan", "llama-server"),
linuxCpu: path.join(ROOT, "app", "llm-backend", "linux", "cpu", "llama-server"),
macArm64: path.join(ROOT, "app", "llm-backend", "mac", "arm64", "llama-server"),
macX64: path.join(ROOT, "app", "llm-backend", "mac", "x64", "llama-server"),
};
const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models");
if (!fs.existsSync(OPENVINO_MODELS)) {
fs.mkdirSync(OPENVINO_MODELS, { recursive: true });
@@ -100,6 +114,17 @@ let openvinoPort = null;
let openvinoModel = null;
let openvinoWidth = null;
let openvinoHeight = null;
let llmProc = null;
let llmReady = false;
let llmError = null;
let llmSettings = {
model: null,
threads: Math.max(1, Math.min(16, os.cpus().length || 4)),
contextSize: 4096,
gpuLayers: -1,
backendMode: "",
backendBinary: "",
};
let backendLoadState = {
active: false,
phase: "",
@@ -557,6 +582,102 @@ async function findAvailableBackendPort() {
throw new Error(`No free backend port found. Tried ${PREFERRED_BACKEND_PORT} and 28088-28120.`);
}
async function findAvailableLlmPort() {
const preferred = await checkPort(PREFERRED_LLM_PORT);
if (preferred.available) return PREFERRED_LLM_PORT;
for (let port = 28121; port <= 28160; port += 1) {
const candidate = await checkPort(port);
if (candidate.available) {
console.log(` [llm] Preferred port ${PREFERRED_LLM_PORT} is busy; using ${port} instead.`);
return port;
}
}
throw new Error(`No free LLM port found. Tried ${PREFERRED_LLM_PORT} and 28121-28160.`);
}
function getLlmBackend() {
const candidates = osPlatform === "win32"
? [
{ path: LLM_BACKEND_PATHS.winVulkan, mode: "Auto (Vulkan/CPU)" },
{ path: LLM_BACKEND_PATHS.winCpu, mode: "CPU" },
]
: osPlatform === "darwin"
? [{
path: process.arch === "arm64" ? LLM_BACKEND_PATHS.macArm64 : LLM_BACKEND_PATHS.macX64,
mode: process.arch === "arm64" ? "Metal GPU" : "CPU",
}]
: [
{ path: LLM_BACKEND_PATHS.linuxVulkan, mode: "Auto (Vulkan/CPU)" },
{ path: LLM_BACKEND_PATHS.linuxCpu, mode: "CPU" },
];
return candidates.find((candidate) => fs.existsSync(candidate.path)) || null;
}
function getLlmModels() {
try {
return fs.readdirSync(LLM_MODELS)
.filter((filename) => filename.toLowerCase().endsWith(".gguf"))
.map((filename) => {
const stats = fs.statSync(path.join(LLM_MODELS, filename));
return {
filename,
name: filename,
sizeBytes: stats.size,
size: formatBytes(stats.size),
format: "GGUF",
};
});
} catch (_) {
return [];
}
}
function pingLlmReady() {
return new Promise((resolve) => {
const req = http.get(`http://127.0.0.1:${PORT_LLM}/v1/models`, (res) => {
res.resume();
resolve(res.statusCode >= 200 && res.statusCode < 500);
});
req.setTimeout(1000, () => {
req.destroy();
resolve(false);
});
req.on("error", () => resolve(false));
});
}
async function waitForLlmReady(maxAttempts = 240) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
if (!llmProc) throw new Error(llmError || "llama.cpp exited during startup.");
if (await pingLlmReady()) {
llmReady = true;
return;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error("llama.cpp did not become ready within 2 minutes.");
}
function killLlm() {
return new Promise((resolve) => {
llmReady = false;
if (!llmProc) {
resolve();
return;
}
const proc = llmProc;
llmProc = null;
try { proc.kill("SIGTERM"); } catch (_) {}
setTimeout(() => {
try { proc.kill("SIGKILL"); } catch (_) {}
resolve();
}, 1200);
});
}
function getSetupPaths() {
const appDir = path.join(ROOT, "app");
if (osPlatform === "win32") {
@@ -1367,7 +1488,77 @@ function killBackend() {
});
}
async function startLlm(settings = {}) {
const filename = path.basename(String(settings.model || ""));
const modelPath = path.join(LLM_MODELS, filename);
if (!filename || !pathInside(modelPath, LLM_MODELS) || !fs.existsSync(modelPath)) {
throw new Error("Select a downloaded GGUF text model first.");
}
if (!filename.toLowerCase().endsWith(".gguf")) {
throw new Error("Text generation requires a .gguf model.");
}
const backend = getLlmBackend();
if (!backend) {
throw new Error("llama.cpp is not installed. Run the platform setup script to install the text backend.");
}
await killBackend();
await killOpenVinoWorker();
await killLlm();
PORT_LLM = await findAvailableLlmPort();
llmError = null;
llmSettings = {
...llmSettings,
model: filename,
threads: Math.max(1, Math.min(64, Number(settings.threads) || llmSettings.threads)),
contextSize: Math.max(512, Math.min(32768, Number(settings.contextSize) || 4096)),
gpuLayers: Number.isFinite(Number(settings.gpuLayers)) ? Number(settings.gpuLayers) : -1,
backendMode: backend.mode,
backendBinary: path.basename(backend.path),
};
const args = [
"--model", modelPath,
"--host", "127.0.0.1",
"--port", String(PORT_LLM),
"--ctx-size", String(llmSettings.contextSize),
"--threads", String(llmSettings.threads),
"--n-gpu-layers", String(llmSettings.gpuLayers),
];
const spawnEnv = { ...process.env };
const backendDir = path.dirname(backend.path);
if (osPlatform === "linux") {
spawnEnv.LD_LIBRARY_PATH = backendDir + (spawnEnv.LD_LIBRARY_PATH ? `:${spawnEnv.LD_LIBRARY_PATH}` : "");
} else if (osPlatform === "darwin") {
spawnEnv.DYLD_LIBRARY_PATH = backendDir + (spawnEnv.DYLD_LIBRARY_PATH ? `:${spawnEnv.DYLD_LIBRARY_PATH}` : "");
}
console.log(" [llm] Starting:", backend.path, args.join(" "));
llmProc = spawn(backend.path, args, { stdio: "pipe", env: spawnEnv });
llmProc.stdout.on("data", (data) => process.stdout.write(" [llm] " + data.toString()));
llmProc.stderr.on("data", (data) => {
const output = data.toString();
process.stderr.write(" [llm-err] " + output);
if (/Vulkan\d+\s*:/i.test(output)) llmSettings.backendMode = "Vulkan GPU";
else if (/Metal/i.test(output) && /GPU|device/i.test(output)) llmSettings.backendMode = "Metal GPU";
else if (/\-\s+CPU\s+:/i.test(output) && llmSettings.backendMode.startsWith("Auto")) llmSettings.backendMode = "CPU";
if (/error|failed/i.test(output) && !/no error/i.test(output)) {
llmError = output.trim().slice(-1200);
}
});
llmProc.on("exit", (code) => {
llmReady = false;
llmProc = null;
if (code !== 0 && code !== null && !llmError) llmError = `llama.cpp exited with code ${code}`;
console.log(" [llm] exited with code", code);
});
await waitForLlmReady();
}
async function startBackend(settings = {}) {
await killLlm();
if (settings.backendType === "openvino-npu") {
await startOpenVinoWorker(settings);
return;
@@ -1768,7 +1959,7 @@ function describeDownloadHttpError(statusCode, url, headers = {}) {
return `HTTP ${statusCode}`;
}
function startModelDownload(url, overrideFilename = null) {
function startModelDownload(url, overrideFilename = null, targetDir = MODELS, kind = "image") {
if (downloadState.active && !overrideFilename) {
console.log(" [download] Already downloading a model");
return;
@@ -1790,7 +1981,7 @@ function startModelDownload(url, overrideFilename = null) {
}
}
const destPath = path.join(MODELS, filename);
const destPath = path.join(targetDir, filename);
const tempPath = `${destPath}.part`;
try { fs.unlinkSync(tempPath); } catch (_) {}
downloadState = {
@@ -1801,7 +1992,8 @@ function startModelDownload(url, overrideFilename = null) {
eta: 0,
totalBytes: 0,
downloadedBytes: 0,
error: null
error: null,
kind,
};
console.log(` [download] Starting download of ${filename} from ${url}`);
@@ -1853,7 +2045,7 @@ function startModelDownload(url, overrideFilename = null) {
try { fs.unlinkSync(tempPath); } catch (_) {}
activeDownload = null;
downloadState.active = false;
startModelDownload(redirectUrl, filename);
startModelDownload(redirectUrl, filename, targetDir, kind);
return;
}
@@ -1967,7 +2159,8 @@ function cancelModelDownload() {
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
}
} else if (filename) {
try { fs.unlinkSync(`${path.join(MODELS, filename)}.part`); } catch (_) {}
const targetDir = downloadState.kind === "text" ? LLM_MODELS : MODELS;
try { fs.unlinkSync(`${path.join(targetDir, filename)}.part`); } catch (_) {}
}
activeDownload = null;
downloadState = {
@@ -1979,6 +2172,7 @@ function cancelModelDownload() {
totalBytes: 0,
downloadedBytes: 0,
error: "Download cancelled",
kind: downloadState.kind || "image",
};
console.log(` [download] Cancelled download of ${filename}`);
return true;
@@ -2282,16 +2476,16 @@ function deleteGeneratedOutputs(outputs = []) {
return deleted;
}
function streamModelUpload(req, filename) {
function streamModelUpload(req, filename, targetDir = MODELS, textOnly = false) {
return new Promise((resolve, reject) => {
const safeFilename = path.basename(filename || "");
const lowerName = safeFilename.toLowerCase();
if (!safeFilename || !isModelFile(lowerName)) {
reject(new Error("Filename must end with .gguf, .safetensors, or .ckpt"));
if (!safeFilename || (textOnly ? !lowerName.endsWith(".gguf") : !isModelFile(lowerName))) {
reject(new Error(textOnly ? "Filename must end with .gguf" : "Filename must end with .gguf, .safetensors, or .ckpt"));
return;
}
const destPath = path.join(MODELS, safeFilename);
const destPath = path.join(targetDir, safeFilename);
const tempPath = `${destPath}.part`;
const out = fs.createWriteStream(tempPath);
let finished = false;
@@ -2325,7 +2519,12 @@ function streamModelUpload(req, filename) {
finished = true;
fs.renameSync(tempPath, destPath);
console.log(` [api] Imported model file: ${safeFilename}`);
resolve(getModelInfo(safeFilename));
if (textOnly) {
const stats = fs.statSync(destPath);
resolve({ filename: safeFilename, name: safeFilename, sizeBytes: stats.size, size: formatBytes(stats.size), format: "GGUF" });
} else {
resolve(getModelInfo(safeFilename));
}
} catch (err) {
try { fs.unlinkSync(tempPath); } catch (_) {}
reject(err);
@@ -2362,6 +2561,13 @@ const server = http.createServer(async (req, res) => {
cleanupCandidates: getCleanupCandidates(),
download: downloadState,
generation: generationState,
llm: {
ready: llmReady,
running: llmProc !== null,
error: llmError,
settings: llmSettings,
port: PORT_LLM,
},
hardware: getHardwareSpecs(),
telemetry: getTelemetry(),
});
@@ -2400,6 +2606,61 @@ const server = http.createServer(async (req, res) => {
});
}
if (req.url === "/api/llm/status" && req.method === "GET") {
const backend = getLlmBackend();
return json(res, 200, {
ready: llmReady,
running: llmProc !== null,
port: PORT_LLM,
preferredPort: PREFERRED_LLM_PORT,
error: llmError,
settings: llmSettings,
backendInstalled: Boolean(backend),
backendMode: backend?.mode || "",
backendPath: backend?.path || "",
});
}
if (req.url === "/api/llm/models" && req.method === "GET") {
return json(res, 200, { models: getLlmModels() });
}
if (req.url === "/api/llm/start" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
await startLlm(body);
return json(res, 200, { ok: true, ready: llmReady, port: PORT_LLM, settings: llmSettings });
} catch (err) {
llmError = err.message || String(err);
await killLlm();
return json(res, 500, { ok: false, error: llmError });
}
}
if (req.url === "/api/llm/stop" && req.method === "POST") {
await killLlm();
return json(res, 200, { ok: true });
}
if (req.url === "/api/llm/chat" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
if (!llmReady) return json(res, 409, { ok: false, error: "Load a text model before sending a message." });
try {
const result = await requestJson(`http://127.0.0.1:${PORT_LLM}/v1/chat/completions`, {
model: llmSettings.model || "local-model",
messages: Array.isArray(body.messages) ? body.messages : [],
temperature: Number.isFinite(Number(body.temperature)) ? Number(body.temperature) : 0.7,
max_tokens: Math.max(1, Math.min(4096, Number(body.max_tokens) || 512)),
stream: false,
}, 300000);
return json(res, 200, result);
} catch (err) {
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
// GET /api/hardware-specs
if (req.url === "/api/hardware-specs" && req.method === "GET") {
return json(res, 200, getHardwareSpecs());
@@ -2499,6 +2760,25 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { ok: true, message: "Download started" });
}
if (req.url === "/api/llm/download-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
const url = String(body.url || "");
if (!url) return json(res, 400, { ok: false, error: "URL is required" });
if (downloadState.active) return json(res, 409, { ok: false, error: "Another model download is already active." });
try {
const directUrl = url.includes("huggingface.co") ? url.replace("/blob/", "/resolve/") : url;
const filename = path.basename(new URL(directUrl).pathname);
if (!filename.toLowerCase().endsWith(".gguf")) {
return json(res, 400, { ok: false, error: "Text model URL must point to a .gguf file." });
}
startModelDownload(directUrl, filename, LLM_MODELS, "text");
return json(res, 200, { ok: true, message: "Text model download started", filename });
} catch (err) {
return json(res, 400, { ok: false, error: err.message || String(err) });
}
}
if (req.url === "/api/download-openvino-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
@@ -2598,6 +2878,37 @@ const server = http.createServer(async (req, res) => {
}
}
if (req.url.startsWith("/api/llm/import-model") && req.method === "POST") {
try {
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const result = await streamModelUpload(req, parsed.searchParams.get("filename"), LLM_MODELS, true);
return json(res, 200, { ok: true, message: `Imported ${result.filename}`, model: result });
} catch (err) {
console.error(" [api] Failed to import text model:", err);
return json(res, 500, { ok: false, error: err.message });
}
}
if (req.url === "/api/llm/delete-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
const filename = path.basename(String(body.filename || ""));
const modelPath = path.join(LLM_MODELS, filename);
if (!filename || !pathInside(modelPath, LLM_MODELS)) {
return json(res, 400, { ok: false, error: "Invalid filename" });
}
if (llmSettings.model === filename && llmProc) await killLlm();
try {
fs.unlinkSync(modelPath);
return json(res, 200, { ok: true });
} catch (err) {
return json(res, err.code === "ENOENT" ? 404 : 500, {
ok: false,
error: err.code === "ENOENT" ? "Text model not found" : err.message,
});
}
}
// POST /api/delete-model
if (req.url === "/api/delete-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
@@ -2655,7 +2966,8 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
console.log(" LOCAL AI IMAGE GENERATOR | Running");
console.log(" Server Build: " + SERVER_BUILD);
console.log(" Frontend : http://localhost:" + PORT_FRONTEND);
console.log(" Backend : http://127.0.0.1:" + PORT_BACKEND);
console.log(" Image API: http://127.0.0.1:" + PORT_BACKEND);
console.log(" Text API : starts on http://127.0.0.1:" + PREFERRED_LLM_PORT);
console.log(" ============================================================");
console.log("");
@@ -2664,5 +2976,5 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
});
// Graceful shutdown
process.on("SIGINT", async () => { await killBackend(); process.exit(0); });
process.on("SIGTERM", async () => { await killBackend(); process.exit(0); });
process.on("SIGINT", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); process.exit(0); });
process.on("SIGTERM", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); process.exit(0); });
+42
View File
@@ -0,0 +1,42 @@
param([string]$Release = "b9631")
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$llmRoot = Join-Path $appDir "llm-backend\win"
function Install-LlamaArchive {
param([string]$Variant, [string]$AssetName)
$dest = Join-Path $llmRoot $Variant
$server = Join-Path $dest "llama-server.exe"
if (Test-Path $server) {
Write-Host " OK llama.cpp $Variant backend already ready."
return
}
$archive = Join-Path $toolsDir $AssetName
$extract = Join-Path $toolsDir "llama-$Variant-extract"
$url = "https://github.com/ggml-org/llama.cpp/releases/download/$Release/$AssetName"
New-Item -ItemType Directory -Force -Path $toolsDir, $dest | Out-Null
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " >> Downloading llama.cpp $Variant backend ($Release)..."
Invoke-WebRequest -UseBasicParsing -Headers @{ "User-Agent" = "Local-AI-Studio" } -Uri $url -OutFile $archive
Expand-Archive -Path $archive -DestinationPath $extract -Force
Get-ChildItem $extract -Recurse -File | ForEach-Object {
Copy-Item $_.FullName (Join-Path $dest $_.Name) -Force
}
Remove-Item $archive, $extract -Recurse -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $server)) {
throw "llama-server.exe was not found after extracting $AssetName"
}
Write-Host " OK llama.cpp $Variant backend installed."
}
Install-LlamaArchive -Variant "vulkan" -AssetName "llama-$Release-bin-win-vulkan-x64.zip"
Install-LlamaArchive -Variant "cpu" -AssetName "llama-$Release-bin-win-cpu-x64.zip"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${LLAMA_RELEASE:-b9631}"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
download_and_extract() {
local asset="$1"
local dest="$2"
local archive="$TOOLS_DIR/$asset"
local url="https://github.com/ggml-org/llama.cpp/releases/download/$RELEASE/$asset"
if [[ -x "$dest/llama-server" ]]; then
echo " OK llama.cpp backend already ready: $dest"
return
fi
mkdir -p "$TOOLS_DIR" "$dest"
rm -f "$archive" "$archive.part"
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
rm -f "$archive"
chmod +x "$dest"/llama-* 2>/dev/null || true
if [[ ! -x "$dest/llama-server" ]]; then
echo " XX llama-server was not found after extracting $asset" >&2
exit 1
fi
}
if [[ "$PLATFORM" == "Darwin" ]]; then
if [[ "$ARCH" == "arm64" ]]; then
download_and_extract "llama-$RELEASE-bin-macos-arm64.tar.gz" "$APP_DIR/llm-backend/mac/arm64"
else
download_and_extract "llama-$RELEASE-bin-macos-x64.tar.gz" "$APP_DIR/llm-backend/mac/x64"
fi
elif [[ "$PLATFORM" == "Linux" ]]; then
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
download_and_extract "llama-$RELEASE-bin-ubuntu-vulkan-arm64.tar.gz" "$APP_DIR/llm-backend/linux/vulkan"
download_and_extract "llama-$RELEASE-bin-ubuntu-arm64.tar.gz" "$APP_DIR/llm-backend/linux/cpu"
else
download_and_extract "llama-$RELEASE-bin-ubuntu-vulkan-x64.tar.gz" "$APP_DIR/llm-backend/linux/vulkan"
download_and_extract "llama-$RELEASE-bin-ubuntu-x64.tar.gz" "$APP_DIR/llm-backend/linux/cpu"
fi
else
echo "Unsupported platform: $PLATFORM" >&2
exit 1
fi
+10 -3
View File
@@ -158,7 +158,7 @@ function Expand-WithProgress {
# ══════════════════════════════════════════════════════════════════════════════
Print-Header
$steps = 4
$steps = 5
# ── Step 1: Portable Node.js ──────────────────────────────────────────────────
Print-Step 1 $steps "Setting up portable Node.js (app/tools/node-win/)"
@@ -420,7 +420,14 @@ if ($hasNvidia) {
}
# ── Step 3: npm install ───────────────────────────────────────────────────────
Print-Step 3 $steps "Installing frontend dependencies (app/frontend/)"
Print-Step 3 $steps "Setting up llama.cpp text backends (Vulkan + CPU)"
& (Join-Path $scriptDir "setup-llama.ps1")
if (-not $?) {
Print-Fail "llama.cpp setup failed."
Read-Host; exit 1
}
Print-Step 4 $steps "Installing frontend dependencies (app/frontend/)"
Write-Host ""
if (-not (Test-Path $npmCmd)) {
@@ -474,7 +481,7 @@ try {
Print-OK "Dependencies installed!"
# ── Step 4: Build frontend ────────────────────────────────────────────────
Print-Step 4 $steps "Building frontend -> app/dist/"
Print-Step 5 $steps "Building frontend -> app/dist/"
Write-Host ""
& $npmCmd run build 2>&1
+6 -3
View File
@@ -258,7 +258,7 @@ if [[ "$PLATFORM" == "Linux" ]]; then
check_glibc
fi
TOTAL_STEPS=4
TOTAL_STEPS=5
# ── Step 1: Portable Node.js ────────────────────────────────────────────────
print_step 1 $TOTAL_STEPS "Setting up portable Node.js ($NODE_DIR/)"
@@ -541,7 +541,10 @@ fi
fi
# ── Step 3: npm install ─────────────────────────────────────────────────────
print_step 3 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)"
print_step 3 $TOTAL_STEPS "Setting up llama.cpp text backend"
bash "$SCRIPT_DIR/setup-llama.sh"
print_step 4 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)"
if [[ ! -x "$NPM_BIN" ]]; then
print_fail "Portable npm was not found at $NPM_BIN"
@@ -639,7 +642,7 @@ else
fi
# ── Step 4: Build frontend ──────────────────────────────────────────────────
print_step 4 $TOTAL_STEPS "Building frontend -> app/dist/"
print_step 5 $TOTAL_STEPS "Building frontend -> app/dist/"
if [ "$USE_SYMLINKS" = false ]; then
# If symlinks are disabled, run vite directly using the local node executable.