{section.items.map((model) => {
const installed = modelNames.includes(model.filename);
- const downloading = downloadingModelId === model.filename;
+ const downloading = downloadingModelId === model.filename || downloadingModelId === `${model.filename}.zip`;
return (
diff --git a/linux.sh b/linux.sh
index f7ee797..baae599 100755
--- a/linux.sh
+++ b/linux.sh
@@ -102,9 +102,12 @@ if [[ -n "$SETUP_REASON" ]]; then
echo ""
read -rp " Press Enter to continue, or Ctrl+C to cancel."
- # Clear any existing frontend server process
- if command -v fuser >/dev/null 2>&1; then
+ # Clear any existing frontend and backend server processes
+ if command -v lsof >/dev/null 2>&1; then
+ lsof -t -i:"${FRONTEND_PORT}" -i:8080 | xargs kill -9 >/dev/null 2>&1 || true
+ elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
+ fuser -k "8080/tcp" >/dev/null 2>&1 || true
fi
if ! bash "$SETUP_SCRIPT" $MAX_PERF_FLAG; then
@@ -123,9 +126,12 @@ echo " LOCAL AI IMAGE GENERATOR | Launching..."
echo " ============================================================"
echo ""
-# Clear frontend port
-if command -v fuser >/dev/null 2>&1; then
+# Clear frontend and backend ports
+if command -v lsof >/dev/null 2>&1; then
+ lsof -t -i:"${FRONTEND_PORT}" -i:8080 | xargs kill -9 >/dev/null 2>&1 || true
+elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
+ fuser -k "8080/tcp" >/dev/null 2>&1 || true
fi
# Start the server
diff --git a/mac.sh b/mac.sh
index 594de33..aa603c0 100755
--- a/mac.sh
+++ b/mac.sh
@@ -74,9 +74,12 @@ if [[ -n "$SETUP_REASON" ]]; then
echo ""
read -rp " Press Enter to continue, or Ctrl+C to cancel."
- # Clear any existing frontend server process
- if command -v fuser >/dev/null 2>&1; then
+ # Clear any existing frontend and backend server processes
+ if command -v lsof >/dev/null 2>&1; then
+ lsof -t -i:"${FRONTEND_PORT}" -i:8080 | xargs kill -9 >/dev/null 2>&1 || true
+ elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
+ fuser -k "8080/tcp" >/dev/null 2>&1 || true
fi
if ! bash "$SETUP_SCRIPT"; then
@@ -95,9 +98,12 @@ echo " LOCAL AI IMAGE GENERATOR | Launching..."
echo " ============================================================"
echo ""
-# Clear frontend port
-if command -v fuser >/dev/null 2>&1; then
+# Clear frontend and backend ports
+if command -v lsof >/dev/null 2>&1; then
+ lsof -t -i:"${FRONTEND_PORT}" -i:8080 | xargs kill -9 >/dev/null 2>&1 || true
+elif command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
+ fuser -k "8080/tcp" >/dev/null 2>&1 || true
fi
# Start the server
diff --git a/scripts/serve.cjs b/scripts/serve.cjs
index 40e0b52..da7f0f1 100644
--- a/scripts/serve.cjs
+++ b/scripts/serve.cjs
@@ -778,6 +778,103 @@ function getVulkanUnavailableReason() {
return "Installed, but this binary did not register a Vulkan backend on this machine.";
}
+let cachedCoreMLPythonPath = null;
+let cachedCoreMLAvailable = null;
+
+function getCoreMLPythonPath() {
+ if (cachedCoreMLAvailable !== null) {
+ return cachedCoreMLPythonPath;
+ }
+
+ if (osPlatform !== "darwin") {
+ cachedCoreMLAvailable = false;
+ cachedCoreMLPythonPath = null;
+ return null;
+ }
+
+ // 1. Check environment variable COREML_PYTHON
+ if (process.env.COREML_PYTHON && fs.existsSync(process.env.COREML_PYTHON)) {
+ cachedCoreMLPythonPath = process.env.COREML_PYTHON;
+ cachedCoreMLAvailable = true;
+ return cachedCoreMLPythonPath;
+ }
+
+ // 2. Check local/in-project venvs
+ const localPaths = [
+ path.join(ROOT, "app", "backend", "mac", "coreml_venv", "bin", "python"),
+ path.join(ROOT, "app", "backend", "mac", "venv", "bin", "python"),
+ path.join(ROOT, "venv", "bin", "python"),
+ ];
+
+ for (const p of localPaths) {
+ if (fs.existsSync(p)) {
+ cachedCoreMLPythonPath = p;
+ cachedCoreMLAvailable = true;
+ return p;
+ }
+ }
+
+ // 3. Check dynamically in home directory (as fallback to user's setup)
+ const homeDir = os.homedir();
+ const homePaths = [
+ path.join(homeDir, "workspace", "coreml_conversion", "venv", "bin", "python"),
+ path.join(homeDir, "ml-stable-diffusion", "venv", "bin", "python"),
+ ];
+
+ for (const p of homePaths) {
+ if (fs.existsSync(p)) {
+ cachedCoreMLPythonPath = p;
+ cachedCoreMLAvailable = true;
+ return p;
+ }
+ }
+
+ // 4. Fallback: Probe system python3 if it has required imports
+ try {
+ const probeResult = spawnSync("python3", [
+ "-c",
+ "import python_coreml_stable_diffusion, diffusers, transformers"
+ ], { timeout: 2000 });
+
+ if (probeResult.status === 0) {
+ // Find absolute path of python3
+ const whichResult = spawnSync("which", ["python3"], { encoding: "utf8" });
+ const python3Path = whichResult.stdout ? whichResult.stdout.trim() : "python3";
+ cachedCoreMLPythonPath = python3Path;
+ cachedCoreMLAvailable = true;
+ return python3Path;
+ }
+ } catch (err) {
+ // python3 not in path or crashed
+ }
+
+ cachedCoreMLAvailable = false;
+ cachedCoreMLPythonPath = null;
+ return null;
+}
+
+function getCoreMLNpuInfo() {
+ const isAppleSilicon = osPlatform === "darwin" && os.arch() === "arm64";
+ if (!isAppleSilicon) {
+ return { supported: false, reason: "Apple Silicon ANE (NPU) is only available on Apple Silicon macOS devices." };
+ }
+
+ const pythonPath = getCoreMLPythonPath();
+ if (!pythonPath) {
+ return {
+ supported: true,
+ ready: false,
+ reason: "CoreML Python environment is not set up. Run scripts/setup-coreml-npu.sh first.",
+ };
+ }
+
+ return {
+ supported: true,
+ ready: true,
+ python: pythonPath,
+ };
+}
+
function getBackendOptions() {
if (cachedBackendOptions) return cachedBackendOptions;
@@ -798,14 +895,14 @@ function getBackendOptions() {
const cpuInstalled = osPlatform === "linux" && fs.existsSync(BACKEND_PATHS.linuxCpu);
const metalInstalled = osPlatform === "darwin" && fs.existsSync(BACKEND_PATHS.mac);
const metalAvailable = metalInstalled;
- const coremlAvailable = osPlatform === "darwin" && fs.existsSync("/Users/orailnoor/workspace/coreml_conversion/venv/bin/python");
+ const coremlNpu = getCoreMLNpuInfo();
const openvinoNpu = getOpenVinoNpuInfo();
const openvinoModels = getOpenVinoModelInfo();
const openvinoNpuAvailable = openvinoNpu.supported && openvinoModels.some((model) => model.installed);
const options = [{ id: "cpu", label: "CPU", available: true }];
if (metalAvailable) options.push({ id: "metal", label: "Metal GPU", available: true });
- if (coremlAvailable) options.push({ id: "apple-npu", label: "Apple Neural Engine (NPU)", 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 });
if (rocmAvailable) options.push({ id: "rocm", label: "ROCm GPU (AMD)", available: true });
if (cudaAvailable) options.push({ id: "cuda", label: "CUDA GPU", available: true });
@@ -824,6 +921,9 @@ function getBackendOptions() {
if (metalInstalled && !metalAvailable) {
unavailable.push({ id: "metal", label: "Metal GPU", reason: "Installed, but Metal backend validation failed." });
}
+ if (coremlNpu.supported && !coremlNpu.ready) {
+ unavailable.push({ id: "apple-npu", label: "Apple Neural Engine (NPU)", reason: coremlNpu.reason });
+ }
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")) {
@@ -831,7 +931,7 @@ function getBackendOptions() {
}
let defaultBackend = "cpu";
- if (coremlAvailable) {
+ if (coremlNpu.supported && coremlNpu.ready) {
defaultBackend = "apple-npu";
} else if (metalAvailable) {
defaultBackend = "metal";
@@ -859,6 +959,7 @@ function getBackendOptions() {
openvinoNpuAvailable,
openvinoNpu,
openvinoModels,
+ coremlNpu,
defaultBackendType: defaultBackend,
};
return cachedBackendOptions;
@@ -948,7 +1049,7 @@ function selectBackendPath(useGpu, backendType = "auto", modelPath = "") {
return BACKEND_PATH;
}
if (osPlatform === "darwin") {
- if (resolvedType === "apple-npu") return "/Users/orailnoor/workspace/coreml_conversion/venv/bin/python";
+ if (resolvedType === "apple-npu") return getCoreMLPythonPath();
if (fs.existsSync(BACKEND_PATHS.mac)) return BACKEND_PATHS.mac;
return BACKEND_PATH;
}
@@ -963,9 +1064,10 @@ function resolveBackendType(useGpu, backendType = "auto", modelPath = "") {
if (modelPath && osPlatform === "darwin") {
const lower = modelPath.toLowerCase();
+ const isDir = fs.existsSync(modelPath) && fs.statSync(modelPath).isDirectory();
if (lower.endsWith(".safetensors") || lower.endsWith(".gguf")) {
if (requestedType === "apple-npu") requestedType = "metal";
- } else if (lower.endsWith(".coreml") || lower.endsWith(".mlpackage") || lower.endsWith(".mlmodelc")) {
+ } else if (lower.endsWith(".coreml") || lower.endsWith(".mlpackage") || lower.endsWith(".mlmodelc") || isDir) {
if (requestedType === "metal") requestedType = "apple-npu";
}
}
@@ -1222,9 +1324,11 @@ async function generateWithOpenVino(body) {
function startBackendReadyPoll() {
let attempts = 0;
+ const isNpu = currentSettings.backendType === "apple-npu" || currentSettings.backendType === "openvino-npu";
+ const maxAttempts = isNpu ? 1200 : 240;
const interval = setInterval(async () => {
attempts += 1;
- if (!backendProc || backendReady || attempts > 240) {
+ if (!backendProc || backendReady || attempts > maxAttempts) {
clearInterval(interval);
return;
}
@@ -1367,9 +1471,6 @@ async function startBackend(settings = {}) {
}
args.push("--rng", "cpu", "--sampler-rng", "cpu");
} else if (requestedBackend === "metal") {
- if (supportsFlags) {
- args.push("--backend", "metal0", "--params-backend", "metal0");
- }
args.push("--rng", "cpu", "--sampler-rng", "cpu");
}
@@ -1451,14 +1552,17 @@ async function startBackend(settings = {}) {
const current = parseInt(loadMatch[1], 10);
const total = parseInt(loadMatch[2], 10);
const progress = total > 0 ? Math.round((current / total) * 100) : 0;
+ const isCoreML = currentSettings.backendType === "apple-npu" || total <= 10;
+ const phaseDesc = isCoreML ? stripAnsi(loadMatch[3]).trim() : "Loading model weights...";
+ const speedDesc = isCoreML ? "" : stripAnsi(loadMatch[3]).trim();
backendLoadState = {
...backendLoadState,
active: !backendReady,
- phase: "Loading model weights...",
+ phase: phaseDesc,
progress: Math.max(backendLoadState.progress, Math.min(99, progress)),
current,
total,
- speed: stripAnsi(loadMatch[3]).trim(),
+ speed: speedDesc,
};
}
@@ -1887,7 +1991,41 @@ const MIME = {
function isModelFile(filename) {
const lower = String(filename || "").toLowerCase();
- return lower.endsWith(".safetensors") || lower.endsWith(".gguf") || lower.endsWith(".ckpt") || lower.endsWith(".coreml") || lower.endsWith(".coreml.zip");
+ if (lower.endsWith(".safetensors") || lower.endsWith(".gguf") || lower.endsWith(".ckpt") || lower.endsWith(".coreml") || lower.endsWith(".coreml.zip")) {
+ return true;
+ }
+ const fullPath = path.join(MODELS, filename);
+ try {
+ const stat = fs.statSync(fullPath);
+ if (stat.isDirectory()) {
+ const files = fs.readdirSync(fullPath).map(f => f.toLowerCase());
+ const hasTextEncoder = files.includes("text_encoder.mlpackage") ||
+ files.includes("text_encoder.mlmodelc") ||
+ files.includes("textencoder.mlpackage") ||
+ files.includes("textencoder.mlmodelc");
+
+ const hasUnet = files.includes("unet.mlpackage") ||
+ files.includes("unet.mlmodelc");
+
+ if (hasTextEncoder && hasUnet) return true;
+
+ const checkSubdirs = ["split_einsum/packages", "split_einsum/compiled", "original/packages", "original/compiled", "split_einsum", "original"];
+ for (const subdir of checkSubdirs) {
+ const subPath = path.join(fullPath, subdir);
+ if (fs.existsSync(subPath)) {
+ const subFiles = fs.readdirSync(subPath).map(f => f.toLowerCase());
+ const subTextEncoder = subFiles.includes("text_encoder.mlpackage") ||
+ subFiles.includes("text_encoder.mlmodelc") ||
+ subFiles.includes("textencoder.mlpackage") ||
+ subFiles.includes("textencoder.mlmodelc");
+ const subUnet = subFiles.includes("unet.mlpackage") ||
+ subFiles.includes("unet.mlmodelc");
+ if (subTextEncoder && subUnet) return true;
+ }
+ }
+ }
+ } catch (_) {}
+ return false;
}
function formatBytes(bytes) {
@@ -1984,7 +2122,19 @@ function describeBackendExitCode(code, backendPath) {
function getModelInfo(filename) {
const safeFilename = path.basename(filename || "");
- const stats = fs.statSync(path.join(MODELS, safeFilename));
+ const fullPath = path.join(MODELS, safeFilename);
+ const stats = fs.statSync(fullPath);
+
+ if (stats.isDirectory()) {
+ const dirSize = getPathSize(fullPath);
+ return {
+ filename: safeFilename,
+ sizeBytes: dirSize,
+ size: formatBytes(dirSize),
+ format: "CoreML",
+ };
+ }
+
return {
filename: safeFilename,
sizeBytes: stats.size,
diff --git a/scripts/setup-coreml-npu.sh b/scripts/setup-coreml-npu.sh
new file mode 100755
index 0000000..4a2b4c9
--- /dev/null
+++ b/scripts/setup-coreml-npu.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+#
+# Optional CoreML NPU setup for Apple Silicon macOS systems.
+# Sets up a virtual environment and installs Apple's ml-stable-diffusion.
+#
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+VENV_DIR="$ROOT_DIR/app/backend/mac/coreml_venv"
+PYTHON_BIN="$VENV_DIR/bin/python"
+
+echo ""
+echo " ============================================================"
+echo " Local AI Image Generator - Apple Silicon CoreML NPU Setup"
+echo " ============================================================"
+echo ""
+
+if [[ "$(uname -s)" != "Darwin" ]]; then
+ echo " [ERROR] This setup script is for macOS only." >&2
+ exit 1
+fi
+
+if [[ "$(uname -m)" != "arm64" ]]; then
+ echo " [ERROR] CoreML NPU support requires Apple Silicon (M1/M2/M3/M4/etc. arm64)." >&2
+ 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
+ exit 1
+fi
+
+if [[ ! -x "$PYTHON_BIN" ]]; then
+ echo " Creating Python environment: $VENV_DIR"
+ if ! python3 -m venv "$VENV_DIR"; then
+ echo " [ERROR] Could not create the virtual environment."
+ exit 1
+ fi
+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
+
+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
+ echo " [ERROR] Failed to install Apple's ml-stable-diffusion repository."
+ echo " Make sure 'git' is installed on your system."
+ exit 1
+fi
+
+echo ""
+echo " Verifying CoreML environment..."
+if ! "$PYTHON_BIN" -c "from python_coreml_stable_diffusion.pipeline import CoreMLStableDiffusionPipeline; print(' ANE (CoreML) Pipeline verified successfully!')"; then
+ echo " [ERROR] Verification failed. Please check the error details above."
+ exit 1
+fi
+
+echo ""
+echo " ============================================================"
+echo " CoreML NPU setup complete."
+echo " Restart the launcher (./mac.sh) and download a CoreML model!"
+echo " ============================================================"
+echo ""
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 0caff8a..eff71ba 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -285,6 +285,29 @@ if [[ "$PLATFORM" == "Darwin" ]]; then
rm -rf "$BACKEND_DIR/extracted"
print_ok "macOS Metal backend installed."
fi
+
+ # CoreML NPU Environment setup (macOS Apple Silicon only)
+ print_info "Setting up CoreML Python virtual environment for Apple Silicon ANE (NPU)..."
+ VENV_DIR="$BACKEND_DIR/coreml_venv"
+ PYTHON_BIN="$VENV_DIR/bin/python"
+
+ if [[ ! -x "$PYTHON_BIN" ]]; then
+ print_info "Creating Python virtual environment at $VENV_DIR..."
+ if ! python3 -m venv "$VENV_DIR"; then
+ print_warn "Could not create the virtual environment for CoreML. CoreML NPU mode will be unavailable."
+ fi
+ fi
+
+ if [[ -x "$PYTHON_BIN" ]]; then
+ print_info "Installing CoreML dependencies (this may take a couple of minutes)..."
+ if "$PYTHON_BIN" -m pip install --upgrade pip >/dev/null 2>&1 && \
+ "$PYTHON_BIN" -m pip install numpy coremltools diffusers transformers huggingface-hub pillow >/dev/null 2>&1 && \
+ "$PYTHON_BIN" -m pip install "git+https://github.com/apple/ml-stable-diffusion.git" >/dev/null 2>&1; then
+ print_ok "CoreML ANE (NPU) environment ready."
+ else
+ print_warn "CoreML dependencies installation failed. CoreML NPU mode will be unavailable."
+ fi
+ fi
else
VENDOR="$(detect_gpu_vendor)"
print_step 2 $TOTAL_STEPS "Detecting GPU vendor: ${VENDOR:-none}"
diff --git a/windows.bat b/windows.bat
index 342bcad..341ea51 100644
--- a/windows.bat
+++ b/windows.bat
@@ -50,8 +50,9 @@ echo.
echo Press any key to continue, or Ctrl+C to cancel.
pause >nul
-:: Clear old frontend server process before setup so app/tools/node-win can be replaced
+:: Clear old frontend and backend server processes before setup so app/tools/node-win can be replaced
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%FRONTEND_PORT% "') do taskkill /f /pid %%a >nul 2>nul
+for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":8080 "') do taskkill /f /pid %%a >nul 2>nul
powershell -ExecutionPolicy Bypass -File "%SETUP%"
if errorlevel 1 (
@@ -73,9 +74,10 @@ echo LOCAL AI IMAGE GENERATOR ^| Launching...
echo ============================================================
echo.
-:: Clear only the frontend port. The backend auto-selects a free port.
-echo Clearing frontend port %FRONTEND_PORT%...
+:: Clear frontend and backend ports to prevent address conflicts.
+echo Clearing frontend port %FRONTEND_PORT% and backend port 8080...
for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":%FRONTEND_PORT% "') do taskkill /f /pid %%a >nul 2>nul
+for /f "tokens=5" %%a in ('netstat -aon 2^>nul ^| findstr ":8080 "') do taskkill /f /pid %%a >nul 2>nul
:: Start frontend server + backend manager (serve.cjs manages sd-vulkan.exe)
echo Starting Local AI Image Generator...