feat: implement portable static file server and backend process manager in serve.cjs

This commit is contained in:
techjarves committed 2026-06-07 21:46:55 +05:30
1 parent c78960c6f4
commit ee3e55ead2
1 file changed
+140 -30
+140 -30
View File
@@ -687,6 +687,24 @@ async function startBackend(settings = {}) {
return;
}
const modelLoadIssue = getModelLoadIssue(currentSettings.model);
if (modelLoadIssue) {
backendError = modelLoadIssue;
backendLoadState = {
active: false,
phase: "Model load blocked",
progress: 0,
current: 0,
total: 0,
speed: "",
model: path.basename(currentSettings.model),
backendMode: "",
backendBinary: "",
device: "",
};
throw new Error(modelLoadIssue);
}
PORT_BACKEND = await findAvailableBackendPort();
const resolvedBackendType = resolveBackendType(currentSettings.useGpu, currentSettings.backendType);
@@ -856,7 +874,12 @@ async function startBackend(settings = {}) {
currentSettings.backendMode = "Vulkan GPU";
}
if (cleanOutput.includes("[ERROR]")) {
backendError = cleanOutput.trim();
const nextError = describeBackendError(cleanOutput.trim(), currentSettings.model);
const hasSpecificFileError = String(backendError || "").includes("incomplete or corrupted");
const isGenericContextError = cleanOutput.includes("new_sd_ctx_t failed");
if (!(hasSpecificFileError && isGenericContextError)) {
backendError = nextError;
}
}
});
backendProc.on("exit", code => {
@@ -961,6 +984,8 @@ function startModelDownload(url, overrideFilename = null) {
}
const destPath = path.join(MODELS, filename);
const tempPath = `${destPath}.part`;
try { fs.unlinkSync(tempPath); } catch (_) {}
downloadState = {
active: true,
filename: filename,
@@ -974,7 +999,26 @@ function startModelDownload(url, overrideFilename = null) {
console.log(` [download] Starting download of ${filename} from ${url}`);
const fileStream = fs.createWriteStream(destPath);
let downloadFinalized = false;
const failDownload = (message, err = null) => {
if (downloadFinalized) return;
downloadFinalized = true;
downloadState.active = false;
downloadState.error = message;
if (err) {
console.error(" [download]", message, err);
} else {
console.error(" [download] Failed:", message);
}
try { fileStream.close(); } catch (_) {}
try { fs.unlinkSync(tempPath); } catch (_) {}
activeDownload = null;
};
const fileStream = fs.createWriteStream(tempPath);
fileStream.on("error", (err) => {
failDownload(`Could not write ${filename}: ${err.message}`, err);
});
const client = url.startsWith("https") ? https : http;
const request = client.get(url, {
@@ -984,16 +1028,12 @@ function startModelDownload(url, overrideFilename = null) {
"Referer": "https://huggingface.co/",
},
}, (response) => {
activeDownload = { request, fileStream, destPath };
activeDownload = { request, fileStream, destPath, tempPath };
// Handle redirects (HuggingFace resolve URLs redirect to Cloudfront/S3)
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
const redirectUrl = response.headers.location ? new URL(response.headers.location, url).toString() : "";
if (!redirectUrl) {
downloadState.active = false;
downloadState.error = "Redirect response did not include a Location header";
fileStream.close();
try { fs.unlinkSync(destPath); } catch (_) {}
activeDownload = null;
failDownload("Redirect response did not include a Location header");
return;
}
console.log(` [download] Redirected to ${redirectUrl}`);
@@ -1003,7 +1043,7 @@ function startModelDownload(url, overrideFilename = null) {
request.destroy();
fileStream.close();
try { fs.unlinkSync(destPath); } catch (_) {}
try { fs.unlinkSync(tempPath); } catch (_) {}
activeDownload = null;
downloadState.active = false;
startModelDownload(redirectUrl, filename);
@@ -1011,12 +1051,7 @@ function startModelDownload(url, overrideFilename = null) {
}
if (response.statusCode !== 200) {
downloadState.active = false;
downloadState.error = describeDownloadHttpError(response.statusCode, url, response.headers);
console.error(` [download] Failed: ${downloadState.error}`);
fileStream.close();
try { fs.unlinkSync(destPath); } catch (_) {}
activeDownload = null;
failDownload(describeDownloadHttpError(response.statusCode, url, response.headers));
return;
}
@@ -1053,25 +1088,50 @@ function startModelDownload(url, overrideFilename = null) {
}
});
response.on("aborted", () => {
failDownload(`Download interrupted before ${filename} finished. Delete and retry the model download.`);
});
response.on("error", (err) => {
failDownload(`Download stream failed before ${filename} finished. Delete and retry the model download.`, err);
});
response.on("end", () => {
fileStream.end();
downloadState.active = false;
downloadState.progress = 100;
downloadState.downloadedBytes = downloadedBytes;
activeDownload = null;
console.log(` [download] Completed download of ${filename}`);
if (downloadFinalized) return;
if (totalBytes > 0 && downloadedBytes !== totalBytes) {
failDownload(`Download incomplete for ${filename}: received ${formatBytes(downloadedBytes)} of ${formatBytes(totalBytes)}. Delete and retry the model download.`);
return;
}
fileStream.end(() => {
try {
if (totalBytes > 0) {
const writtenBytes = fs.statSync(tempPath).size;
if (writtenBytes !== totalBytes) {
failDownload(`Download incomplete for ${filename}: wrote ${formatBytes(writtenBytes)} of ${formatBytes(totalBytes)}. Delete and retry the model download.`);
return;
}
}
try { fs.unlinkSync(destPath); } catch (_) {}
fs.renameSync(tempPath, destPath);
downloadState.active = false;
downloadFinalized = true;
downloadState.progress = 100;
downloadState.downloadedBytes = downloadedBytes;
downloadState.error = null;
activeDownload = null;
console.log(` [download] Completed download of ${filename}`);
} catch (err) {
failDownload(`Could not finalize ${filename}: ${err.message}`);
}
});
});
});
request.on("error", (err) => {
downloadState.active = false;
downloadState.error = err.message;
console.error(" [download] Request error:", err);
fileStream.close();
try { fs.unlinkSync(destPath); } catch (_) {}
activeDownload = null;
failDownload(err.message, err);
});
activeDownload = { request, fileStream, destPath };
activeDownload = { request, fileStream, destPath, tempPath };
}
function cancelModelDownload() {
@@ -1082,9 +1142,9 @@ function cancelModelDownload() {
if (activeDownload) {
try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {}
try { activeDownload.fileStream.destroy(); } catch (_) {}
try { fs.unlinkSync(activeDownload.destPath); } catch (_) {}
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
} else if (filename) {
try { fs.unlinkSync(path.join(MODELS, filename)); } catch (_) {}
try { fs.unlinkSync(`${path.join(MODELS, filename)}.part`); } catch (_) {}
}
activeDownload = null;
downloadState = {
@@ -1123,6 +1183,56 @@ function formatBytes(bytes) {
return `${(value / (1024 ** idx)).toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
}
function getModelLoadIssue(modelPath) {
const filename = path.basename(modelPath || "");
const lower = filename.toLowerCase();
const ext = path.extname(lower);
if (!modelPath || !fs.existsSync(modelPath)) {
return `Model file not found: ${filename || "unknown model"}`;
}
const stats = fs.statSync(modelPath);
if (stats.size < 128 * 1024 * 1024) {
return `${filename} is too small to be a complete image model (${formatBytes(stats.size)}). Delete it and download/import it again.`;
}
if (ext === ".gguf") {
const requiresSeparateComponents =
lower.includes("z_image") ||
lower.includes("z-image") ||
lower.includes("zimage") ||
lower.includes("qwen") ||
lower.includes("hidream") ||
lower.includes("hunyuan") ||
lower.includes("wan") ||
lower.includes("flux");
if (requiresSeparateComponents) {
return `${filename} looks like a multi-file diffusion GGUF. This app currently loads single-file SD 1.5/SDXL checkpoints directly. Models like Z-Image, Qwen, Flux, HiDream, Hunyuan, and Wan usually need extra files such as VAE/text encoders and must be launched with --diffusion-model instead of --model.`;
}
}
return null;
}
function describeBackendError(rawError, modelPath) {
const raw = String(rawError || "").trim();
const filename = path.basename(modelPath || "");
const lower = filename.toLowerCase();
if (raw.includes("read tensor data failed") || raw.includes("load tensors from file failed")) {
return `${raw}\n\n${filename || "The selected model"} is present but appears incomplete or corrupted. Delete it from Local Models and download/import it again.`;
}
if (!raw.includes("new_sd_ctx_t failed")) return raw;
if (lower.endsWith(".gguf")) {
return `${raw}\n\n${filename} could not be loaded as a single checkpoint. Some GGUF files are only the diffusion part of a larger workflow and need separate VAE/text encoder files. Try a recommended SD 1.5/SDXL model, or re-download/import the file if this is meant to be a single-file SD/SDXL GGUF.`;
}
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 getModelInfo(filename) {
const safeFilename = path.basename(filename || "");
const stats = fs.statSync(path.join(MODELS, safeFilename));