implement modular frontend components, backend services, and multi-platform configuration scripts for the local AI image generator

This commit is contained in:
techjarves committed 2026-06-23 05:48:26 +05:30
1 parent c280683a9d
commit 4a840679d6
14 files changed
+1114 -2089

No files matched your search

+3 -2
View File
@@ -11,10 +11,10 @@ Write-Host " Resetting Local AI Studio..." -ForegroundColor Yellow
Write-Host " ============================================================" -ForegroundColor Yellow
Write-Host ""
# Delete tools/node
# Delete portable tools/runtime folder
$toolsDir = Join-Path $appDir "tools"
if (Test-Path $toolsDir) {
Write-Host " >> Removing portable tools/ node folder..." -ForegroundColor Cyan
Write-Host " >> Removing portable tools/runtime folder..." -ForegroundColor Cyan
Remove-Item $toolsDir -Recurse -Force -ErrorAction SilentlyContinue
}
@@ -23,6 +23,7 @@ $backendDir = Join-Path $appDir "backend"
if (Test-Path $backendDir) {
Write-Host " >> Removing image backend binaries..." -ForegroundColor Cyan
Remove-Item $backendDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " Preserving source workers in scripts/workers." -ForegroundColor DarkGray
}
# Delete llama.cpp backend
+2 -2
View File
@@ -16,9 +16,9 @@ echo " Resetting Local AI Studio..."
echo " ============================================================"
echo ""
# Delete tools/node
# Delete portable tools/runtime folder
if [[ -d "$APP_DIR/tools" ]]; then
echo " >> Removing portable tools/ node folder..."
echo " >> Removing portable tools/runtime folder..."
rm -rf "$APP_DIR/tools"
fi
+399 -24
View File
@@ -41,10 +41,12 @@ const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
const SERVER_BUILD = "text-image-v1";
const ROOT = path.join(__dirname, "..", "..");
const DIST = path.join(ROOT, "app", "dist");
const TOOLS = path.join(ROOT, "app", "tools");
const osPlatform = process.platform;
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"),
cpu: path.join(ROOT, "app", "backend", "win", "cpu", "sd-cpu.exe"),
mac: path.join(ROOT, "app", "backend", "mac", "sd"),
linuxCpu: path.join(ROOT, "app", "backend", "linux", "cpu", "sd-server-cpu"),
linuxVulkan: path.join(ROOT, "app", "backend", "linux", "vulkan", "sd-server-vulkan"),
@@ -67,6 +69,8 @@ if (osPlatform === "win32") {
if (hasNvidia && fs.existsSync(BACKEND_PATHS.cuda)) {
BACKEND_PATH = BACKEND_PATHS.cuda;
} else if (fs.existsSync(BACKEND_PATHS.cpu)) {
BACKEND_PATH = BACKEND_PATHS.cpu;
} else {
BACKEND_PATH = BACKEND_PATHS.vulkan;
}
@@ -224,6 +228,7 @@ const OPENVINO_NPU_MODELS = [
// ── Backend process state ─────────────────────────────────────────────────────
let backendProc = null;
let backendProcSeq = 0;
let backendReady = false;
let backendError = null;
let openvinoProc = null;
@@ -404,6 +409,24 @@ function getCpuUsagePercent() {
return Math.max(0, Math.min(100, Math.round((1 - idleDelta / totalDelta) * 1000) / 10));
}
function hasVulkanRuntime() {
if (osPlatform !== "win32") return true;
try {
const sysPaths = [
path.join(process.env.SystemRoot || "C:\\Windows", "System32", "vulkan-1.dll"),
path.join(process.env.SystemRoot || "C:\\Windows", "SysWOW64", "vulkan-1.dll"),
];
for (const dllPath of sysPaths) {
if (fs.existsSync(dllPath)) return true;
}
try {
execSync("where vulkan-1.dll", { stdio: "ignore" });
return true;
} catch (_) {}
} catch (_) {}
return false;
}
function detectLinuxGpuFromSysfs() {
try {
const pciDir = "/sys/bus/pci/devices";
@@ -776,6 +799,74 @@ function getGpuInfo() {
// ── NPU Detection ─────────────────────────────────────────────────────────────
let cachedNpuInfo = null;
let cachedHasNpuHardware = null;
function hasNpuHardware() {
if (cachedHasNpuHardware !== null) return cachedHasNpuHardware;
if (osPlatform !== "win32" && osPlatform !== "linux") {
cachedHasNpuHardware = false;
return cachedHasNpuHardware;
}
try {
if (osPlatform === "win32") {
const deviceResult = spawnSync("wmic", [
"path", "win32_pnpentity", "where", "Name like '%NPU%' or Name like '%Intel%AI%' or Name like '%VPU%'", "get", "Name", "/value"
], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
if (deviceResult.status === 0 && deviceResult.stdout && /NPU|AI Boost|VPU/i.test(deviceResult.stdout)) {
cachedHasNpuHardware = true;
return cachedHasNpuHardware;
}
const cpuResult = spawnSync("wmic", ["cpu", "get", "Name", "/value"], {
encoding: "utf8",
timeout: 10000,
stdio: ["ignore", "pipe", "pipe"],
});
if (cpuResult.status === 0 && cpuResult.stdout) {
const cpuName = cpuResult.stdout.toLowerCase();
if (cpuName.includes("ultra") && (cpuName.match(/ultra\s*\d/) || cpuName.includes("core(tm) ultra"))) {
cachedHasNpuHardware = true;
return cachedHasNpuHardware;
}
}
} else if (osPlatform === "linux") {
const lspciResult = spawnSync("lspci", [], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
if (lspciResult.status === 0 && lspciResult.stdout && lspciResult.stdout.toLowerCase().includes("npu")) {
cachedHasNpuHardware = true;
return cachedHasNpuHardware;
}
try {
const sysPci = fs.readdirSync("/sys/bus/pci/devices");
for (const dev of sysPci) {
const vendorPath = `/sys/bus/pci/devices/${dev}/vendor`;
const classPath = `/sys/bus/pci/devices/${dev}/class`;
if (!fs.existsSync(vendorPath) || !fs.existsSync(classPath)) continue;
const vendor = fs.readFileSync(vendorPath, "utf8").trim();
const devClass = fs.readFileSync(classPath, "utf8").trim();
if (vendor === "0x8086" && (devClass.includes("1180") || devClass.includes("acce"))) {
cachedHasNpuHardware = true;
return cachedHasNpuHardware;
}
}
} catch (_) {}
try {
const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8").toLowerCase();
if (cpuInfo.includes("ultra") && cpuInfo.includes("intel")) {
cachedHasNpuHardware = true;
return cachedHasNpuHardware;
}
} catch (_) {}
}
} catch (_) {}
cachedHasNpuHardware = false;
return cachedHasNpuHardware;
}
function detectNpu() {
if (cachedNpuInfo !== null) return cachedNpuInfo;
cachedNpuInfo = { detected: false, vendor: null, name: null };
@@ -1372,15 +1463,10 @@ function getOpenVinoPythonCandidates() {
const candidates = [];
if (process.env.OPENVINO_PYTHON) candidates.push(process.env.OPENVINO_PYTHON);
if (osPlatform === "win32") {
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-win", "Scripts", "python.exe"));
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "Scripts", "python.exe")); // legacy fallback
candidates.push("C:\\tmp\\npu-test-venv\\Scripts\\python.exe");
candidates.push("python");
candidates.push(path.join(ROOT, "app", "tools", "python-win", "python.exe"));
} else {
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-linux", "bin", "python"));
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python")); // legacy fallback
candidates.push("python3");
candidates.push("python");
}
return [...new Set(candidates)];
}
@@ -2432,6 +2518,7 @@ function getSetupPaths() {
distIndex: path.join(DIST, "index.html"),
cudaBackend: BACKEND_PATHS.cuda,
vulkanBackend: BACKEND_PATHS.vulkan,
cpuBackend: BACKEND_PATHS.cpu,
models: MODELS,
outputs: OUTPUTS,
};
@@ -2472,6 +2559,7 @@ async function getHealth() {
if (osPlatform === "win32") {
checks.push(getPathInfo("CUDA backend", paths.cudaBackend));
checks.push(getPathInfo("Vulkan backend", paths.vulkanBackend));
checks.push(getPathInfo("CPU backend", paths.cpuBackend));
} else if (osPlatform === "darwin") {
checks.push(getPathInfo("Mac backend", paths.macBackend));
} else {
@@ -2483,7 +2571,8 @@ async function getHealth() {
let backendInstalled = false;
if (osPlatform === "win32") {
backendInstalled = checks.find((check) => check.label === "CUDA backend")?.exists ||
checks.find((check) => check.label === "Vulkan backend")?.exists;
checks.find((check) => check.label === "Vulkan backend")?.exists ||
checks.find((check) => check.label === "CPU backend")?.exists;
} else if (osPlatform === "darwin") {
backendInstalled = checks.find((check) => check.label === "Mac backend")?.exists;
} else {
@@ -2495,7 +2584,7 @@ async function getHealth() {
}
const criticalOk = checks
.filter((check) => !["CUDA backend", "Vulkan backend", "Linux CPU backend", "Linux Vulkan backend", "Linux ROCm backend", "Mac backend"].includes(check.label))
.filter((check) => !["CUDA backend", "Vulkan backend", "CPU backend", "Linux CPU backend", "Linux Vulkan backend", "Linux ROCm backend", "Mac backend"].includes(check.label))
.every((check) => check.ok) && backendInstalled;
const ports = {
@@ -2508,7 +2597,7 @@ async function getHealth() {
ports.backend.ok = true;
const issues = checks
.filter((check) => !check.ok && !["CUDA backend", "Vulkan backend", "Linux CPU backend", "Linux Vulkan backend", "Linux ROCm backend", "Mac backend"].includes(check.label))
.filter((check) => !check.ok && !["CUDA backend", "Vulkan backend", "CPU 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.`);
@@ -2645,6 +2734,9 @@ function isRunningInWSL() {
}
function getVulkanUnavailableReason() {
if (osPlatform === "win32" && !hasVulkanRuntime()) {
return "The Vulkan runtime (vulkan-1.dll) is missing. Install or update your GPU vendor driver with Vulkan support, then run setup again so the Vulkan backend folder is repaired.";
}
if (osPlatform === "linux" && isRunningInWSL()) {
const gpuName = getGpuInfo().name;
const lowerGpu = gpuName.toLowerCase();
@@ -2764,13 +2856,15 @@ function getBackendOptions() {
);
const vulkanInstalled = (osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.vulkan)) ||
(osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxVulkan));
const vulkanAvailable = vulkanInstalled && backendAccepts(
const vulkanRuntimeAvailable = osPlatform !== "win32" || hasVulkanRuntime();
const vulkanAvailable = vulkanInstalled && vulkanRuntimeAvailable && backendAccepts(
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 cpuInstalled = (osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxCpu)) ||
(osPlatform === "win32" && fs.existsSync(BACKEND_PATHS.cpu));
const metalInstalled = osPlatform === "darwin" && fs.existsSync(BACKEND_PATHS.mac);
const metalAvailable = metalInstalled;
const coremlNpu = getCoreMLNpuInfo();
@@ -2778,7 +2872,8 @@ function getBackendOptions() {
const openvinoModels = getOpenVinoModelInfo();
const openvinoNpuAvailable = openvinoNpu.supported && openvinoModels.some((model) => model.installed);
const options = [{ id: "cpu", label: "CPU", available: true }];
const options = [];
if (cpuInstalled) options.push({ id: "cpu", label: "CPU", available: true });
if (metalAvailable) options.push({ id: "metal", label: "Metal GPU", available: true });
if (coremlNpu.supported && coremlNpu.ready) options.push({ id: "apple-npu", label: "Apple Neural Engine (NPU)", available: true });
if (vulkanAvailable) options.push({ id: "vulkan", label: "Vulkan GPU", available: true });
@@ -2802,9 +2897,18 @@ function getBackendOptions() {
if (coremlNpu.supported && !coremlNpu.ready) {
unavailable.push({ id: "apple-npu", label: "Apple Neural Engine (NPU)", reason: coremlNpu.reason });
}
if (!cpuInstalled && (osPlatform === "win32" || osPlatform === "linux")) {
unavailable.push({
id: "cpu",
label: "CPU",
reason: osPlatform === "win32"
? "Windows CPU backend is not installed. Run scripts/setup/setup.ps1 to install app/backend/win/cpu."
: "Linux CPU backend is not installed. Run setup again to install app/backend/linux/cpu.",
});
}
if (openvinoNpu.supported && !openvinoNpuAvailable) {
unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: "Runtime is ready, but no OpenVINO NPU model is downloaded." });
} else if (!openvinoNpu.supported && (osPlatform === "win32" || osPlatform === "linux")) {
} else if (!openvinoNpu.supported && hasNpuHardware() && (osPlatform === "win32" || osPlatform === "linux")) {
unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: openvinoNpu.reason });
}
@@ -2908,6 +3012,10 @@ function selectBackendPath(useGpu, backendType = "auto", modelPath = "") {
const resolvedType = resolveBackendType(useGpu, backendType, modelPath);
if (osPlatform === "win32") {
if (resolvedType === "cuda" && fs.existsSync(BACKEND_PATHS.cuda)) return BACKEND_PATHS.cuda;
if (resolvedType === "cpu" && fs.existsSync(BACKEND_PATHS.cpu)) return BACKEND_PATHS.cpu;
if (resolvedType === "vulkan" && fs.existsSync(BACKEND_PATHS.vulkan)) return BACKEND_PATHS.vulkan;
if (fs.existsSync(BACKEND_PATHS.cuda)) return BACKEND_PATHS.cuda;
if (fs.existsSync(BACKEND_PATHS.cpu)) return BACKEND_PATHS.cpu;
if (fs.existsSync(BACKEND_PATHS.vulkan)) return BACKEND_PATHS.vulkan;
return BACKEND_PATH;
}
@@ -3347,7 +3455,11 @@ function getOpenVinoResolution(settings = {}) {
async function startOpenVinoWorker(settings = {}) {
const npuInfo = getOpenVinoNpuInfo();
if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU is not available.");
const model = findOpenVinoModel(settings.model) || getOpenVinoModelInfo().find((item) => item.installed);
const requestedModel = settings.model ? findOpenVinoModel(settings.model) : null;
if (settings.model && !requestedModel) {
throw new Error("OpenVINO NPU requires a downloaded OpenVINO model. Standard .safetensors/.ckpt weights must use Vulkan, CUDA, or CPU.");
}
const model = requestedModel || getOpenVinoModelInfo().find((item) => item.installed);
if (!model || !model.installed) {
throw new Error("OpenVINO NPU model is not downloaded. Download an OpenVINO NPU model from Model Manager first.");
}
@@ -3512,18 +3624,19 @@ function getDefaultModel() {
function killBackend() {
return new Promise(resolve => {
resetGenerationState();
if (!backendProc) {
const proc = backendProc;
if (!proc) {
backendUnloadState = { active: false, phase: "", progress: 0 };
resolve();
return;
}
backendUnloadState = { active: true, phase: "Stopping backend process...", progress: 10 };
backendReady = false;
backendProc.kill("SIGTERM");
try { proc.kill("SIGTERM"); } catch (_) {}
backendUnloadState = { active: true, phase: "Waiting for process exit...", progress: 50 };
setTimeout(() => {
try { backendProc.kill("SIGKILL"); } catch (_) {}
backendProc = null;
try { proc.kill("SIGKILL"); } catch (_) {}
if (backendProc === proc) backendProc = null;
backendUnloadState = { active: false, phase: "Backend unloaded", progress: 100 };
resolve();
}, 2000);
@@ -3958,6 +4071,14 @@ async function startBackend(settings = {}) {
currentSettings.backendType = resolvedBackendType;
currentSettings.useGpu = resolvedBackendType !== "cpu";
const backendPath = selectBackendPath(currentSettings.useGpu, currentSettings.backendType, currentSettings.model);
if (!backendPath) {
const setupHint = resolvedBackendType === "apple-npu"
? "CoreML Python environment is not set up. Run scripts/setup/setup-coreml-npu.sh first."
: resolvedBackendType === "cpu" && osPlatform === "win32"
? "Windows CPU backend is not installed. Run scripts/setup.ps1 again so app/backend/win/cpu is created."
: "No compatible backend executable was found.";
throw new Error(setupHint);
}
const backendMode = getBackendMode(backendPath, currentSettings.useGpu, currentSettings.backendType);
currentSettings.backendMode = backendMode;
currentSettings.backendBinary = path.basename(backendPath);
@@ -3987,7 +4108,7 @@ async function startBackend(settings = {}) {
if (requestedBackend === "apple-npu") {
args = [
path.join(ROOT, "app", "backend", "mac", "coreml_server.py"),
path.join(ROOT, "scripts", "workers", "coreml_server.py"),
"--listen-port", String(PORT_BACKEND),
"--model", currentSettings.model,
"--steps", String(currentSettings.steps),
@@ -4072,10 +4193,12 @@ async function startBackend(settings = {}) {
console.log(" [backend] Starting:", path.basename(backendPath), args.join(" "));
backendReady = false;
backendProc = spawn(backendPath, args, { stdio: "pipe", env: spawnEnv });
const procSeq = ++backendProcSeq;
const proc = spawn(backendPath, args, { stdio: "pipe", env: spawnEnv });
backendProc = proc;
startBackendReadyPoll();
backendProc.stdout.on("data", d => {
proc.stdout.on("data", d => {
const output = d.toString();
process.stdout.write(" [sd] " + output);
const cleanOutput = stripAnsi(output);
@@ -4155,7 +4278,7 @@ async function startBackend(settings = {}) {
}
});
backendProc.stderr.on("data", d => {
proc.stderr.on("data", d => {
const output = d.toString();
process.stderr.write(" [sd-err] " + output);
const cleanOutput = stripAnsi(output);
@@ -4194,10 +4317,14 @@ async function startBackend(settings = {}) {
}
}
});
backendProc.on("exit", code => {
proc.on("exit", (code, signal) => {
if (backendProc !== proc) {
console.log(" [backend] stale process exited with code", code, signal ? `(signal ${signal})` : "", `(process ${procSeq})`);
return;
}
backendReady = false;
backendProc = null;
console.log(" [backend] exited with code", code);
console.log(" [backend] exited with code", code, signal ? `(signal ${signal})` : "", `(process ${procSeq})`);
if (code !== null && code !== 0) {
if (!backendError) {
backendError = describeBackendExitCode(code, currentSettings.backendBinary || BACKEND_PATH);
@@ -4276,6 +4403,221 @@ function startOpenVinoModelDownload(modelId) {
});
}
const IMAGE_BACKEND_DOWNLOADS = {
"cpu": {
id: "cpu",
label: "CPU",
url: "https://github.com/leejet/stable-diffusion.cpp/releases/download/master-669-2d40a8b/sd-master-2d40a8b-bin-win-avx2-x64.zip",
destDir: path.join(ROOT, "app", "backend", "win", "cpu"),
exeName: "sd-cpu.exe",
requiredDll: "stable-diffusion.dll",
},
"vulkan": {
id: "vulkan",
label: "Vulkan GPU",
url: "https://github.com/leejet/stable-diffusion.cpp/releases/download/master-669-2d40a8b/sd-master-2d40a8b-bin-win-vulkan-x64.zip",
destDir: path.join(ROOT, "app", "backend", "win", "vulkan"),
exeName: "sd-vulkan.exe",
requiredDll: "stable-diffusion.dll",
},
"cuda": {
id: "cuda",
label: "CUDA GPU",
url: "https://github.com/leejet/stable-diffusion.cpp/releases/download/master-669-2d40a8b/sd-master-2d40a8b-bin-win-cuda12-x64.zip",
destDir: path.join(ROOT, "app", "backend", "win", "cuda"),
exeName: "sd-cuda.exe",
requiredDll: "stable-diffusion.dll",
},
};
function listFilesRecursive(dir) {
const out = [];
if (!fs.existsSync(dir)) return out;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...listFilesRecursive(full));
} else if (entry.isFile()) {
out.push(full);
}
}
return out;
}
function installImageBackendArchive(zipPath, backend) {
const tempDir = path.join(TOOLS, `image-backend-${backend.id}-extract-${Date.now()}`);
try {
fs.rmSync(tempDir, { recursive: true, force: true });
fs.mkdirSync(tempDir, { recursive: true });
fs.mkdirSync(backend.destDir, { recursive: true });
const tar = spawnSync("tar.exe", ["-xf", zipPath, "-C", tempDir], { encoding: "utf8" });
if (tar.status !== 0) {
throw new Error((tar.stderr || tar.stdout || "Could not extract backend archive.").trim());
}
const files = listFilesRecursive(tempDir);
const exeSource = files.find((file) => ["sd-server.exe", "sd.exe"].includes(path.basename(file).toLowerCase()));
const dllSource = files.find((file) => path.basename(file).toLowerCase() === backend.requiredDll.toLowerCase());
if (!exeSource || !dllSource) {
throw new Error("Backend archive did not contain the expected stable-diffusion executable and DLL.");
}
fs.copyFileSync(exeSource, path.join(backend.destDir, backend.exeName));
fs.copyFileSync(dllSource, path.join(backend.destDir, backend.requiredDll));
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (ext !== ".dll" && ext !== ".exe") continue;
if (file === exeSource) continue;
fs.copyFileSync(file, path.join(backend.destDir, path.basename(file)));
}
if (!fs.existsSync(path.join(backend.destDir, backend.exeName)) || !fs.existsSync(path.join(backend.destDir, backend.requiredDll))) {
throw new Error(`Backend install did not create ${backend.exeName} and ${backend.requiredDll}.`);
}
cachedBackendOptions = null;
} finally {
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (_) {}
}
}
function startImageBackendDownload(backendId, redirectCount = 0, redirectUrl = "") {
if (downloadState.active && redirectCount === 0) {
throw new Error("Another download is already active.");
}
if (osPlatform !== "win32") {
throw new Error("In-app backend downloads are currently available for Windows image backends.");
}
const backend = IMAGE_BACKEND_DOWNLOADS[String(backendId || "").toLowerCase()];
if (!backend) {
throw new Error("This backend cannot be installed from the app yet. Use the platform setup script.");
}
fs.mkdirSync(TOOLS, { recursive: true });
const url = redirectUrl || backend.url;
const filename = `${backend.id}-backend.zip`;
const destPath = path.join(TOOLS, filename);
const tempPath = `${destPath}.part`;
if (redirectCount === 0) {
try { fs.unlinkSync(tempPath); } catch (_) {}
downloadState = {
active: true,
filename: backend.label,
progress: 0,
speed: "0 MB/s",
eta: 0,
totalBytes: 0,
downloadedBytes: 0,
error: null,
kind: "backend",
backendId: backend.id,
};
}
console.log(` [backend-download] Downloading ${backend.label} backend from ${url}`);
let downloadFinalized = false;
const fileStream = fs.createWriteStream(tempPath);
const failDownload = (message, err = null) => {
if (downloadFinalized) return;
downloadFinalized = true;
downloadState.active = false;
downloadState.error = message;
if (err) console.error(" [backend-download]", message, err);
else console.error(" [backend-download] Failed:", message);
try { fileStream.close(); } catch (_) {}
try { fs.unlinkSync(tempPath); } catch (_) {}
activeDownload = null;
};
fileStream.on("error", (err) => failDownload(`Could not write ${filename}: ${err.message}`, err));
const client = url.startsWith("https") ? https : http;
const request = client.get(url, {
headers: {
"User-Agent": "Local-AI-Image-Generator/1.0 (+https://github.com/techjarves/Local-AI-Image-Generator)",
"Accept": "application/zip, application/octet-stream, */*",
},
}, (response) => {
activeDownload = { request, fileStream, destPath, tempPath };
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
const nextUrl = response.headers.location ? new URL(response.headers.location, url).toString() : "";
if (!nextUrl) return failDownload("Redirect response did not include a Location header.");
if (redirectCount > 10) return failDownload("Too many redirects.");
downloadState.speed = "Following redirect";
request.removeAllListeners("error");
request.destroy();
response.resume();
fileStream.close();
try { fs.unlinkSync(tempPath); } catch (_) {}
activeDownload = null;
startImageBackendDownload(backend.id, redirectCount + 1, nextUrl);
return;
}
if (response.statusCode !== 200) {
return failDownload(describeDownloadHttpError(response.statusCode, url, response.headers));
}
const totalBytes = parseInt(response.headers["content-length"], 10) || 0;
downloadState.totalBytes = totalBytes;
let downloadedBytes = 0;
let lastTime = Date.now();
let lastDownloaded = 0;
response.on("data", (chunk) => {
downloadedBytes += chunk.length;
downloadState.downloadedBytes = downloadedBytes;
fileStream.write(chunk);
const now = Date.now();
const elapsed = (now - lastTime) / 1000;
if (elapsed >= 0.5) {
const chunkSpeed = (downloadedBytes - lastDownloaded) / elapsed;
lastDownloaded = downloadedBytes;
lastTime = now;
downloadState.speed = `${(chunkSpeed / (1024 * 1024)).toFixed(1)} MB/s`;
if (totalBytes > 0) {
downloadState.progress = Math.round((downloadedBytes / totalBytes) * 100);
downloadState.eta = Math.round((totalBytes - downloadedBytes) / Math.max(1, chunkSpeed));
} else {
downloadState.progress = -1;
downloadState.eta = -1;
}
}
});
response.on("aborted", () => failDownload(`Download interrupted before ${backend.label} backend finished.`));
response.on("error", (err) => failDownload(`Download stream failed before ${backend.label} backend finished.`, err));
response.on("end", () => {
if (downloadFinalized) return;
if (totalBytes > 0 && downloadedBytes !== totalBytes) {
failDownload(`Download incomplete for ${backend.label}: received ${formatBytes(downloadedBytes)} of ${formatBytes(totalBytes)}.`);
return;
}
fileStream.end(() => {
try {
try { fs.unlinkSync(destPath); } catch (_) {}
fs.renameSync(tempPath, destPath);
downloadState.speed = "Installing";
downloadState.progress = 95;
installImageBackendArchive(destPath, backend);
try { fs.unlinkSync(destPath); } catch (_) {}
downloadFinalized = true;
downloadState.active = false;
downloadState.progress = 100;
downloadState.downloadedBytes = downloadedBytes;
downloadState.error = null;
downloadState.speed = "Complete";
activeDownload = null;
console.log(` [backend-download] Installed ${backend.label} backend`);
} catch (err) {
failDownload(`Could not install ${backend.label} backend: ${err.message}`, err);
}
});
});
});
request.on("error", (err) => failDownload(err.message, err));
activeDownload = { request, fileStream, destPath, tempPath };
}
// ── Generation State (Real-time progress parser) ─────────────────────────────
let generationState = {
active: false,
@@ -5903,6 +6245,20 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
return json(res, 200, { ok: true });
}
// POST /api/download-backend
if (req.url === "/api/download-backend" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
if (downloadState.active) return json(res, 409, { ok: false, error: "Another download is already active." });
try {
const backendId = String(body.backend_id || body.backendId || body.id || "");
startImageBackendDownload(backendId);
return json(res, 200, { ok: true, message: "Backend download started", backendId });
} catch (err) {
return json(res, 400, { ok: false, error: err.message || String(err) });
}
}
// POST /api/download-model
if (req.url === "/api/download-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
@@ -5958,6 +6314,25 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
// GET /api/download-progress
if (req.url === "/api/download-progress" && req.method === "GET") {
if (!downloadState.active && downloadState.kind === "backend" && downloadState.backendId) {
const backend = IMAGE_BACKEND_DOWNLOADS[downloadState.backendId];
const installed = backend &&
fs.existsSync(path.join(backend.destDir, backend.exeName)) &&
fs.existsSync(path.join(backend.destDir, backend.requiredDll));
if (installed && !downloadState.error) {
cachedBackendOptions = null;
downloadState = {
active: false,
filename: "",
progress: 0,
speed: "0 MB/s",
eta: 0,
totalBytes: 0,
downloadedBytes: 0,
error: null,
};
}
}
return json(res, 200, downloadState);
}
+43 -4
View File
@@ -11,6 +11,37 @@ ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
VENV_DIR="$ROOT_DIR/app/backend/mac/coreml_venv"
PYTHON_BIN="$VENV_DIR/bin/python"
is_coreml_python_supported() {
"$1" - <<'PY' >/dev/null 2>&1
import sys
raise SystemExit(0 if (3, 9) <= sys.version_info[:2] < (3, 12) else 1)
PY
}
find_coreml_python() {
local candidate
if [[ -n "${COREML_SETUP_PYTHON:-}" ]]; then
if is_coreml_python_supported "$COREML_SETUP_PYTHON"; then
echo "$COREML_SETUP_PYTHON"
return 0
fi
return 1
fi
for candidate in python3.11 python3.10 python3.9 python3; do
if command -v "$candidate" >/dev/null 2>&1 && is_coreml_python_supported "$candidate"; then
command -v "$candidate"
return 0
fi
done
return 1
}
coreml_venv_ready() {
[[ -x "$PYTHON_BIN" ]] && "$PYTHON_BIN" -c "import sys; assert (3, 9) <= sys.version_info[:2] < (3, 12); import python_coreml_stable_diffusion, diffusers, transformers" >/dev/null 2>&1
}
echo ""
echo " ============================================================"
echo " Local AI Image Generator - Apple Silicon CoreML NPU Setup"
@@ -27,14 +58,22 @@ if [[ "$(uname -m)" != "arm64" ]]; then
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo " [ERROR] python3 is required. Install Python 3 first (via Homebrew or Xcode)." >&2
SETUP_PYTHON="$(find_coreml_python || true)"
if [[ -z "$SETUP_PYTHON" ]]; then
echo " [ERROR] CoreML setup requires Python 3.9, 3.10, or 3.11." >&2
echo " Apple's ml-stable-diffusion dependency stack is not compatible with Python 3.12+." >&2
echo " Install python@3.11 with Homebrew, or set COREML_SETUP_PYTHON to a supported Python." >&2
exit 1
fi
if [[ -x "$PYTHON_BIN" ]] && ! coreml_venv_ready; then
echo " Existing CoreML environment is incomplete or uses Python 3.12+. Recreating it..."
rm -rf "$VENV_DIR"
fi
if [[ ! -x "$PYTHON_BIN" ]]; then
echo " Creating Python environment: $VENV_DIR"
if ! python3 -m venv "$VENV_DIR"; then
if ! "$SETUP_PYTHON" -m venv "$VENV_DIR"; then
echo " [ERROR] Could not create the virtual environment."
exit 1
fi
@@ -42,7 +81,7 @@ fi
echo " Installing CoreML Stable Diffusion dependencies (numpy, coremltools, diffusers)..."
"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install numpy coremltools diffusers transformers huggingface-hub pillow
"$PYTHON_BIN" -m pip install "numpy<1.24" coremltools diffusers transformers huggingface-hub pillow
echo " Installing Apple's python-coreml-stable-diffusion package..."
if ! "$PYTHON_BIN" -m pip install "git+https://github.com/apple/ml-stable-diffusion.git"; then
+100 -10
View File
@@ -1,16 +1,105 @@
# Optional OpenVINO NPU setup for Intel AI Boost systems.
# This does not modify the existing stable-diffusion.cpp backends.
# Installs a portable Python runtime under app/tools/python-win.
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$venvDir = Join-Path $rootDir "app\tools\openvino-venv-win"
$pythonExe = Join-Path $venvDir "Scripts\python.exe"
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$pythonDir = Join-Path $toolsDir "python-win"
$pythonExe = Join-Path $pythonDir "python.exe"
$pythonZip = Join-Path $toolsDir "python-win.zip"
$getPip = Join-Path $toolsDir "get-pip.py"
$pythonVersion = "3.11.9"
$pythonZipUrl = "https://www.python.org/ftp/python/$pythonVersion/python-$pythonVersion-embed-amd64.zip"
$getPipUrl = "https://bootstrap.pypa.io/get-pip.py"
function Enable-Tls12 {
try {
$tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor $tls12
} catch {}
}
function Download-File {
param([string]$Url, [string]$Dest, [string]$Label)
Write-Host " Downloading $Label..."
Enable-Tls12
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Dest) | Out-Null
$client = New-Object System.Net.WebClient
$client.Headers.Add("User-Agent", "Local-AI-Image-Generator-Setup")
$client.DownloadFile($Url, $Dest)
}
function Expand-Zip {
param([string]$ZipPath, [string]$Destination)
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($ZipPath, $Destination)
}
function Enable-EmbeddedPythonSitePackages {
$pth = Get-ChildItem $pythonDir -Filter "python*._pth" | Select-Object -First 1
if (-not $pth) { throw "Embedded Python ._pth file not found in $pythonDir" }
$content = Get-Content $pth.FullName
$hasSitePackages = $content | Where-Object { $_.Trim() -eq "Lib\site-packages" } | Select-Object -First 1
$addedSitePackages = $false
$next = foreach ($line in $content) {
if (-not $hasSitePackages -and -not $addedSitePackages -and $line.Trim() -eq "import site") {
"Lib\site-packages"
$addedSitePackages = $true
}
if ($line.Trim() -eq "#import site") {
if (-not $hasSitePackages -and -not $addedSitePackages) {
"Lib\site-packages"
$addedSitePackages = $true
}
"import site"
} else {
$line
}
}
Set-Content -Path $pth.FullName -Value $next -Encoding ASCII
}
function Ensure-PortablePython {
if (Test-Path $pythonExe) {
try {
& $pythonExe -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 11) else 1)"
if ($LASTEXITCODE -eq 0) {
Write-Host " Portable Python already ready: $pythonExe"
return
}
} catch {}
Write-Host " Existing portable Python is invalid. Reinstalling..."
Remove-Item $pythonDir -Recurse -Force -ErrorAction SilentlyContinue
}
Download-File -Url $pythonZipUrl -Dest $pythonZip -Label "Portable Python $pythonVersion"
Expand-Zip -ZipPath $pythonZip -Destination $pythonDir
Remove-Item $pythonZip -Force -ErrorAction SilentlyContinue
Enable-EmbeddedPythonSitePackages
Download-File -Url $getPipUrl -Dest $getPip -Label "pip bootstrap"
& $pythonExe $getPip --no-warn-script-location
if ($LASTEXITCODE -ne 0) { throw "pip bootstrap failed for portable Python." }
Remove-Item $getPip -Force -ErrorAction SilentlyContinue
& $pythonExe -m pip --version | Out-Host
}
Write-Host ""
Write-Host " ============================================================"
Write-Host " Local AI Image Generator - OpenVINO NPU Setup"
Write-Host " Portable Python runtime: app/tools/python-win"
Write-Host " ============================================================"
Write-Host ""
@@ -25,22 +114,23 @@ if (-not $npu) {
Write-Host " Detected: $($npu.Name)"
if (-not (Test-Path $pythonExe)) {
Write-Host " Creating Python environment: $venvDir"
python -m venv $venvDir
}
Ensure-PortablePython
Write-Host " Installing OpenVINO GenAI runtime..."
& $pythonExe -m pip install --upgrade pip
& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub
Write-Host " Installing OpenVINO GenAI runtime into portable Python..."
& $pythonExe -m pip install --upgrade pip --no-warn-script-location --progress-bar off
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed." }
& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub --no-warn-script-location --progress-bar off
if ($LASTEXITCODE -ne 0) { throw "OpenVINO package install failed." }
Write-Host ""
Write-Host " Verifying OpenVINO NPU device..."
& $pythonExe -c "import openvino as ov; core=ov.Core(); print('Available devices:', core.available_devices); raise SystemExit(0 if 'NPU' in core.available_devices else 2)"
if ($LASTEXITCODE -ne 0) { throw "OpenVINO installed, but NPU was not available to the portable runtime." }
Write-Host ""
Write-Host " ============================================================"
Write-Host " OpenVINO NPU setup complete."
Write-Host " Portable Python: $pythonExe"
Write-Host " Restart windows.bat, then download an OpenVINO NPU model."
Write-Host " ============================================================"
Write-Host ""
+1 -1
View File
@@ -39,7 +39,7 @@ $oldPath = $env:PATH
try {
$env:PATH = "$nodeDir;$env:PATH"
Print-Info "Installing kokoro-js into app/tts-runtime..."
& $npmCmd install --prefer-offline 2>&1
& $npmCmd install --prefer-offline --loglevel=error
if ($LASTEXITCODE -ne 0) {
Print-Fail "kokoro-js install failed."
exit 1
+70 -5
View File
@@ -158,7 +158,7 @@ function Expand-WithProgress {
# ══════════════════════════════════════════════════════════════════════════════
Print-Header
$steps = 7
$steps = 8
# ── Step 1: Portable Node.js ──────────────────────────────────────────────────
Print-Step 1 $steps "Setting up portable Node.js (app/tools/node-win/)"
@@ -242,6 +242,56 @@ if (-not $hasNvidia) {
} catch {}
}
Print-Step 2 $steps "Setting up stable-diffusion.cpp CPU backend (app/backend/win/cpu/)"
$cpuBackendDest = Join-Path $appDir "backend\win\cpu"
$cpuBackendExe = Join-Path $cpuBackendDest "sd-cpu.exe"
$cpuBackendDll = Join-Path $cpuBackendDest "stable-diffusion.dll"
if ((Test-Path $cpuBackendExe) -and (Test-Path $cpuBackendDll)) {
Print-OK "CPU backend binaries already ready."
} else {
$cpuBackendZip = Join-Path $toolsDir "sd-cpu.zip"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
New-Item -ItemType Directory -Force -Path $cpuBackendDest | Out-Null
$ok = Invoke-RichDownload `
-Url "https://github.com/leejet/stable-diffusion.cpp/releases/download/master-669-2d40a8b/sd-master-2d40a8b-bin-win-avx2-x64.zip" `
-Dest $cpuBackendZip `
-Label "stable-diffusion.cpp CPU Backend (Windows x64 AVX2)"
if (-not $ok) { Print-Fail "Cannot download CPU backend binaries."; Read-Host; exit 1 }
$tempExt = Join-Path $toolsDir "sd-cpu-temp"
Expand-WithProgress -ZipPath $cpuBackendZip -Destination $tempExt -Label "CPU Backend"
Remove-Item $cpuBackendZip -Force
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 $cpuBackendExe -Force }
if (Test-Path $extractedDll) { Copy-Item $extractedDll $cpuBackendDll -Force }
Get-ChildItem $tempExt -Filter "*.dll" -Recurse | ForEach-Object { Copy-Item $_.FullName $cpuBackendDest -Force }
Get-ChildItem $tempExt -Filter "*.exe" -Recurse | ForEach-Object {
if ($_.FullName -ne $extractedExe) { Copy-Item $_.FullName $cpuBackendDest -Force }
}
Remove-Item $tempExt -Recurse -Force
}
if ((Test-Path $cpuBackendExe) -and (Test-Path $cpuBackendDll)) {
Print-OK "CPU backend binaries installed successfully!"
} else {
Print-Fail "Failed to copy backend binaries to app/backend/win/cpu/."
Read-Host; exit 1
}
}
if ($hasNvidia) {
Print-Step 2 $steps "Setting up stable-diffusion.cpp CUDA GPU backend (app/backend/win/cuda/)"
$backendDest = Join-Path $appDir "backend\win\cuda"
@@ -441,7 +491,22 @@ if (-not $?) {
Read-Host; exit 1
}
Print-Step 6 $steps "Installing frontend dependencies (app/frontend/)"
$npu = Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "*Intel(R) AI Boost*" -or ($_.Name -match "NPU" -and $_.PNPClass -eq "ComputeAccelerator") } |
Select-Object -First 1
Print-Step 6 $steps "Setting up portable OpenVINO NPU runtime"
if ($npu) {
& (Join-Path $scriptDir "setup-openvino-npu.ps1")
if (-not $?) {
Print-Fail "OpenVINO NPU setup failed."
Read-Host; exit 1
}
} else {
Print-Info "Intel AI Boost NPU not detected. Skipping OpenVINO NPU runtime."
}
Print-Step 7 $steps "Installing frontend dependencies (app/frontend/)"
Write-Host ""
if (-not (Test-Path $npmCmd)) {
@@ -486,7 +551,7 @@ Push-Location $frontendDir
$oldPath = $env:PATH
try {
$env:PATH = "$nodeDir;$env:PATH"
& $npmCmd install --prefer-offline 2>&1
& $npmCmd install --prefer-offline --loglevel=error
if ($LASTEXITCODE -ne 0) {
Print-Fail "npm install failed."
Read-Host; exit 1
@@ -495,10 +560,10 @@ try {
Print-OK "Dependencies installed!"
# ── Step 4: Build frontend ────────────────────────────────────────────────
Print-Step 7 $steps "Building frontend -> app/dist/"
Print-Step 8 $steps "Building frontend -> app/dist/"
Write-Host ""
& $npmCmd run build 2>&1
& $npmCmd run build
if ($LASTEXITCODE -ne 0) {
Print-Fail "Frontend build failed."
Read-Host; exit 1
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
import argparse
import base64
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
MODEL_VERSION_BY_NAME = {
"coreml-stable-diffusion-v1-5": "runwayml/stable-diffusion-v1-5",
"stable-diffusion-v1-5": "runwayml/stable-diffusion-v1-5",
"stable-diffusion-v1-4": "CompVis/stable-diffusion-v1-4",
"cyberrealistic": "runwayml/stable-diffusion-v1-5",
}
def json_response(handler, code, payload):
body = json.dumps(payload).encode("utf-8")
handler.send_response(code)
handler.send_header("Content-Type", "application/json")
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
def find_coreml_resource_dir(model_path: Path) -> Path:
candidates = [
model_path,
model_path / "Resources",
model_path / "compiled",
model_path / "packages",
model_path / "split_einsum_v2" / "compiled",
model_path / "split_einsum" / "compiled",
model_path / "original" / "compiled",
model_path / "split_einsum_v2" / "packages",
model_path / "split_einsum" / "packages",
model_path / "original" / "packages",
]
required_names = {
"text_encoder.mlmodelc",
"text_encoder.mlpackage",
"textencoder.mlmodelc",
"textencoder.mlpackage",
}
for candidate in candidates:
if not candidate.is_dir():
continue
names = {entry.name.lower() for entry in candidate.iterdir()}
has_text_encoder = bool(names & required_names)
has_unet = "unet.mlmodelc" in names or "unet.mlpackage" in names
if has_text_encoder and has_unet:
return candidate
for candidate in model_path.rglob("*"):
if not candidate.is_dir():
continue
names = {entry.name.lower() for entry in candidate.iterdir()}
has_text_encoder = bool(names & required_names)
has_unet = "unet.mlmodelc" in names or "unet.mlpackage" in names
if has_text_encoder and has_unet:
return candidate
raise FileNotFoundError(
f"Could not find CoreML resources under {model_path}. "
"Expected a folder containing text_encoder and unet .mlmodelc/.mlpackage files."
)
def infer_model_version(model_path: Path) -> str:
override = os.environ.get("COREML_MODEL_VERSION")
if override:
return override
lower = model_path.name.lower()
for needle, version in MODEL_VERSION_BY_NAME.items():
if needle in lower:
return version
return "runwayml/stable-diffusion-v1-5"
def infer_model_sources(resource_dir: Path) -> str:
names = {entry.name.lower() for entry in resource_dir.iterdir()}
if any(name.endswith(".mlmodelc") for name in names):
return "compiled"
return "packages"
def latest_png(root: Path) -> Path:
images = sorted(root.rglob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
if not images:
raise FileNotFoundError("CoreML generation finished but did not write a PNG output.")
return images[0]
class CoreMLServerState:
def __init__(self, model: Path, steps: int, cfg_scale: float):
self.model = model
self.resources = find_coreml_resource_dir(model)
self.model_version = infer_model_version(model)
self.steps = steps
self.cfg_scale = cfg_scale
self.started_at = time.time()
def make_handler(state: CoreMLServerState):
class Handler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
self.end_headers()
def do_GET(self):
if self.path == "/health":
json_response(self, 200, {
"ok": True,
"ready": True,
"model": str(state.model),
"resources": str(state.resources),
"model_version": state.model_version,
"uptime_sec": round(time.time() - state.started_at, 1),
})
return
if self.path == "/v1/models":
json_response(self, 200, {
"object": "list",
"data": [{
"id": state.model.name,
"object": "model",
"owned_by": "local-coreml",
}],
})
return
json_response(self, 404, {"ok": False, "error": "Unknown endpoint"})
def do_POST(self):
if self.path != "/v1/images/generations":
json_response(self, 404, {"ok": False, "error": "Unknown endpoint"})
return
try:
length = int(self.headers.get("Content-Length", "0") or "0")
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
prompt = str(payload.get("prompt") or "").strip()
if not prompt:
raise ValueError("Prompt is required")
size = str(payload.get("size") or "512x512").lower().split("x")
if len(size) == 2 and size != ["512", "512"]:
raise ValueError("CoreML models in this app currently generate at their compiled resolution, normally 512x512.")
steps = max(1, int(payload.get("steps") or state.steps or 30))
guidance = float(payload.get("cfg_scale") or state.cfg_scale or 7.0)
seed = int(payload.get("seed")) if payload.get("seed") not in (None, -1) else int(time.time_ns() % (2 ** 32))
negative_prompt = str(payload.get("negative_prompt") or "").strip()
started = time.time()
out_dir = Path(tempfile.mkdtemp(prefix="portable-diffusion-coreml-"))
cmd = [
sys.executable,
"-m",
"python_coreml_stable_diffusion.pipeline",
"--prompt",
prompt,
"-i",
str(state.resources),
"-o",
str(out_dir),
"--compute-unit",
os.environ.get("COREML_COMPUTE_UNIT", "CPU_AND_NE"),
"--seed",
str(seed),
"--model-version",
state.model_version,
"--num-inference-steps",
str(steps),
"--guidance-scale",
str(guidance),
"--model-sources",
infer_model_sources(state.resources),
]
if negative_prompt:
cmd.extend(["--negative-prompt", negative_prompt])
print("[coreml-npu] generating image", flush=True)
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
output_lines = []
assert proc.stdout is not None
for line in proc.stdout:
output_lines.append(line.rstrip())
print(line, end="", flush=True)
code = proc.wait()
if code != 0:
raise RuntimeError("CoreML generation failed with code %s:\n%s" % (code, "\n".join(output_lines[-30:])))
image_path = latest_png(out_dir)
encoded = base64.b64encode(image_path.read_bytes()).decode("ascii")
shutil.rmtree(out_dir, ignore_errors=True)
print("[coreml-npu] decoding complete", flush=True)
json_response(self, 200, {
"created": int(time.time()),
"data": [{
"b64_json": encoded,
"seed": seed,
}],
"duration_sec": round(time.time() - started, 2),
})
except Exception as exc:
json_response(self, 500, {"ok": False, "error": str(exc)})
def log_message(self, fmt, *args):
print("[coreml-npu-http] " + fmt % args, flush=True)
return Handler
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--listen-port", required=True, type=int)
parser.add_argument("--model", required=True, type=Path)
parser.add_argument("--steps", type=int, default=30)
parser.add_argument("--cfg-scale", type=float, default=7.0)
args = parser.parse_args()
state = CoreMLServerState(args.model, args.steps, args.cfg_scale)
print(f"[coreml-npu] Model: {state.model}", flush=True)
print(f"[coreml-npu] Resources: {state.resources}", flush=True)
print(f"[coreml-npu] Model version: {state.model_version}", flush=True)
print("| 1/1 - CoreML resources ready", flush=True)
server = ThreadingHTTPServer(("127.0.0.1", args.listen_port), make_handler(state))
print(f"[coreml-npu] listening on http://127.0.0.1:{args.listen_port}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()