- {MODEL_LIBRARY.map((section) => (
+ {visibleModelLibrary.map((section) => (
{section.group}
diff --git a/app/frontend/src/services/api.js b/app/frontend/src/services/api.js
index 035982b..88ea70e 100644
--- a/app/frontend/src/services/api.js
+++ b/app/frontend/src/services/api.js
@@ -48,8 +48,12 @@ export function normalizeModel(model) {
}
return {
filename: model?.filename || model?.name || "",
+ name: model?.name || model?.filename || "",
sizeBytes: Number(model?.sizeBytes || 0),
size: model?.size || (model?.sizeBytes ? formatBytes(model.sizeBytes) : "Unknown"),
+ format: model?.format || "Local Weights File",
+ backendType: model?.backendType || "",
+ resolution: model?.resolution || "",
};
}
@@ -254,6 +258,8 @@ export async function startServer(modelPath, constraints) {
threads: constraints.threads || 8,
use_gpu: constraints.useGpu !== false,
backend_type: constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"),
+ width: constraints.width || 512,
+ height: constraints.height || 512,
vae_tiling: constraints.vaeTiling !== false,
vae_on_cpu: constraints.vaeOnCpu === true,
}),
@@ -330,14 +336,34 @@ export async function deleteGeneratedOutputs(outputs) {
// List model files from the models folder (via management API in web mode)
export async function listModelsFromDisk() {
try {
- const r = await fetch("/api/models");
- const data = await r.json();
- return (data.models || []).map(normalizeModel);
+ const [modelRes, openvino] = await Promise.all([
+ fetch("/api/models"),
+ listOpenVinoModels().catch(() => ({ supported: false, models: [] })),
+ ]);
+ const data = await modelRes.json();
+ const normalModels = (data.models || []).map(normalizeModel);
+ const openvinoModels = openvino.supported
+ ? (openvino.models || []).filter((model) => model.installed).map((model) => normalizeModel({
+ filename: model.id,
+ name: model.name,
+ sizeBytes: model.sizeBytes,
+ size: model.size,
+ format: "OpenVINO",
+ backendType: "openvino-npu",
+ resolution: model.resolution,
+ }))
+ : [];
+ return [...normalModels, ...openvinoModels];
} catch (_) {
return [];
}
}
+export async function listOpenVinoModels() {
+ const res = await fetch("/api/openvino-models");
+ return await readJsonResponse(res, "The local server returned invalid OpenVINO model data.");
+}
+
// Generate image (T2I / I2I)
// Handles API calls to sd-server. If the server is unreachable or returns an error,
// we surface that error to the UI instead of silently returning a fake placeholder.
@@ -345,8 +371,6 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
console.log("Initiating image generation:", { prompt, negativePrompt, constraints, activeModelName });
const startTime = Date.now();
- const baseUrl = await getBackendBaseUrl();
-
// Prepare payload based on standard stable-diffusion.cpp REST endpoint schemas
const payload = {
prompt: prompt,
@@ -361,6 +385,45 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
denoising_strength: constraints.denoisingStrength || 0.7,
};
+ if (constraints.backendType === "openvino-npu") {
+ if (inputImageBase64) {
+ throw new Error("OpenVINO NPU test mode currently supports text-to-image only.");
+ }
+ const response = await fetch("/api/openvino-generate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ signal,
+ body: JSON.stringify({
+ prompt: payload.prompt,
+ negative_prompt: payload.negative_prompt,
+ steps: payload.steps,
+ cfg_scale: payload.cfg_scale,
+ seed: payload.seed,
+ width: payload.width,
+ height: payload.height,
+ }),
+ });
+ const data = await readJsonResponse(response, "The local server returned an invalid OpenVINO generation response.");
+ const imgB64 = data?.data?.[0]?.b64_json;
+ if (!imgB64) throw new Error("OpenVINO generation did not return an image.");
+ const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, "");
+ const header = atob(normalizedB64.slice(0, 24));
+ const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG";
+ const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff;
+ const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP";
+ if (!isPng && !isJpeg && !isWebp) {
+ throw new Error("OpenVINO generation returned an invalid image payload instead of a real PNG/JPEG/WebP.");
+ }
+ const durationSec = Number(data.duration_sec) || parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
+ return {
+ image: `data:image/png;base64,${normalizedB64}`,
+ seed: data.data?.[0]?.seed ?? payload.seed,
+ duration_sec: durationSec,
+ };
+ }
+
+ const baseUrl = await getBackendBaseUrl();
+
// txt2img uses /v1/images/generations; img2img uses /sdapi/v1/img2img.
const isImg2Img = !!payload.image;
@@ -416,9 +479,17 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
// Response: { data: [{ b64_json: "..." }] }
const imgB64 = data?.data?.[0]?.b64_json ?? data?.images?.[0];
if (imgB64) {
+ const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, "");
+ const header = atob(normalizedB64.slice(0, 24));
+ const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG";
+ const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff;
+ const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP";
+ if (!isPng && !isJpeg && !isWebp) {
+ throw new Error("Generation returned an invalid image payload instead of a real PNG/JPEG/WebP.");
+ }
const durationSec = parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
return {
- image: `data:image/png;base64,${imgB64}`,
+ image: `data:image/png;base64,${normalizedB64}`,
seed: data.data?.[0]?.seed ?? payload.seed,
duration_sec: durationSec,
};
@@ -654,6 +725,20 @@ export async function downloadModel(url) {
}
}
+export async function downloadOpenVinoModel(modelId) {
+ try {
+ const res = await fetch("/api/download-openvino-model", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model_id: modelId })
+ });
+ return await readJsonResponse(res, "The local server returned an invalid OpenVINO download response.");
+ } catch (e) {
+ console.error("Failed to start OpenVINO model download:", e);
+ return { ok: false, error: e.message };
+ }
+}
+
export async function cancelModelDownload() {
try {
const res = await fetch("/api/cancel-download", { method: "POST" });
diff --git a/linux.sh b/linux.sh
index 5eccac2..f7ee797 100755
--- a/linux.sh
+++ b/linux.sh
@@ -30,10 +30,27 @@ FRONTEND_PORT="${FRONTEND_PORT:-1420}"
SETUP_REASON=""
SETUP_MODE="Repair"
MAX_PERF_FLAG=""
+SETUP_OPENVINO=0
# Parse args
-if [[ "${1:-}" == "--max-perf" ]]; then
- MAX_PERF_FLAG="--max-perf"
+for arg in "$@"; do
+ case "$arg" in
+ --max-perf)
+ MAX_PERF_FLAG="--max-perf"
+ ;;
+ --setup-openvino)
+ SETUP_OPENVINO=1
+ ;;
+ *)
+ echo "[ERROR] Unknown option: $arg" >&2
+ echo "Usage: ./linux.sh [--max-perf] [--setup-openvino]" >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ $SETUP_OPENVINO -eq 1 ]]; then
+ bash "$SCRIPT_DIR/scripts/setup-openvino-npu.sh"
fi
# ── Symlink node_modules to avoid OS conflicts ──────────────────────────────
diff --git a/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc b/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc
new file mode 100644
index 0000000..4ca397f
Binary files /dev/null and b/scripts/__pycache__/openvino_npu_worker.cpython-313.pyc differ
diff --git a/scripts/openvino_npu_worker.py b/scripts/openvino_npu_worker.py
new file mode 100644
index 0000000..e5a0309
--- /dev/null
+++ b/scripts/openvino_npu_worker.py
@@ -0,0 +1,187 @@
+import argparse
+import base64
+import json
+import sys
+import time
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from io import BytesIO
+from pathlib import Path
+
+import openvino as ov
+import openvino_genai as genai
+from PIL import Image
+
+
+class WorkerState:
+ def __init__(self, model_dir: Path, width: int, height: int, cache_dir: Path):
+ self.model_dir = model_dir
+ self.width = width
+ self.height = height
+ self.cache_dir = cache_dir
+ self.ready = False
+ self.error = None
+ self.pipeline = None
+ self.devices = {}
+ self.started_at = time.time()
+
+ def load(self) -> None:
+ try:
+ available_devices = ov.Core().available_devices
+ if "NPU" not in available_devices:
+ raise RuntimeError(
+ f"Intel NPU is not available to OpenVINO. Devices: {', '.join(available_devices)}"
+ )
+ vae_device = "GPU" if "GPU" in available_devices else "CPU"
+ self.devices = {
+ "text_encoder": "CPU",
+ "unet": "NPU",
+ "vae_decoder": vae_device,
+ }
+ print(f"[openvino-npu] Loading model: {self.model_dir}", flush=True)
+ print(
+ "[openvino-npu] Devices: "
+ f"text_encoder=CPU, unet=NPU, vae_decoder={vae_device}",
+ flush=True,
+ )
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
+ pipe = genai.Text2ImagePipeline(str(self.model_dir))
+ pipe.reshape(1, self.height, self.width, 1.0)
+ pipe.compile(
+ "CPU",
+ "NPU",
+ vae_device,
+ config={"CACHE_DIR": str(self.cache_dir)},
+ )
+ self.pipeline = pipe
+ self.ready = True
+ print("[openvino-npu] READY", flush=True)
+ except Exception as exc:
+ self.error = str(exc)
+ print(f"[openvino-npu] ERROR: {self.error}", file=sys.stderr, flush=True)
+ raise
+
+
+def make_handler(state: WorkerState):
+ class Handler(BaseHTTPRequestHandler):
+ def _json(self, code: int, payload: dict) -> None:
+ body = json.dumps(payload).encode("utf-8")
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_OPTIONS(self) -> None:
+ 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) -> None:
+ if self.path == "/health":
+ self._json(200, {
+ "ok": state.ready and not state.error,
+ "ready": state.ready,
+ "error": state.error,
+ "model": str(state.model_dir),
+ "devices": state.devices,
+ "uptime_sec": round(time.time() - state.started_at, 1),
+ })
+ return
+ self._json(404, {"ok": False, "error": "Unknown endpoint"})
+
+ def do_POST(self) -> None:
+ if self.path != "/generate":
+ self._json(404, {"ok": False, "error": "Unknown endpoint"})
+ return
+ if not state.ready or state.pipeline is None:
+ self._json(503, {"ok": False, "error": state.error or "OpenVINO NPU worker is not ready"})
+ return
+
+ length = int(self.headers.get("Content-Length", "0") or "0")
+ try:
+ request = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
+ prompt = str(request.get("prompt") or "").strip()
+ if not prompt:
+ raise ValueError("Prompt is required")
+ steps = max(1, min(8, int(request.get("steps") or 4)))
+ guidance_scale = float(request.get("guidance_scale") or request.get("cfg_scale") or 1.0)
+ width = int(request.get("width") or state.width)
+ height = int(request.get("height") or state.height)
+ if (width, height) not in ((512, 512), (1024, 1024)):
+ raise ValueError("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale.")
+
+ started = time.time()
+ last_step_at = started
+
+ def progress_callback(step: int, num_steps: int, _latent) -> bool:
+ nonlocal last_step_at
+ now = time.time()
+ step_duration = max(0.001, now - last_step_at)
+ last_step_at = now
+ print(
+ f"[openvino-npu] PROGRESS {step + 1}/{num_steps} {1.0 / step_duration:.2f} it/s",
+ flush=True,
+ )
+ return False
+
+ generate_args = {
+ "width": state.width,
+ "height": state.height,
+ "num_inference_steps": steps,
+ "guidance_scale": guidance_scale,
+ "rng_seed": int(request.get("seed")) if request.get("seed") not in (None, -1) else int(time.time_ns() % (2**32)),
+ "callback": progress_callback,
+ }
+ negative_prompt = str(request.get("negative_prompt") or "").strip()
+ if negative_prompt and guidance_scale > 1.0:
+ generate_args["negative_prompt"] = negative_prompt
+ image_tensor = state.pipeline.generate(prompt, **generate_args)
+ print("[openvino-npu] DECODING", flush=True)
+ image = Image.fromarray(image_tensor.data[0])
+ upscaled = width != state.width or height != state.height
+ if upscaled:
+ image = image.resize((width, height), Image.Resampling.LANCZOS)
+ buf = BytesIO()
+ image.save(buf, format="PNG")
+ encoded = base64.b64encode(buf.getvalue()).decode("ascii")
+ self._json(200, {
+ "ok": True,
+ "data": [{
+ "b64_json": encoded,
+ "seed": request.get("seed"),
+ }],
+ "generated_size": f"{state.width}x{state.height}",
+ "output_size": f"{width}x{height}",
+ "upscaled": upscaled,
+ "duration_sec": round(time.time() - started, 2),
+ })
+ except Exception as exc:
+ self._json(500, {"ok": False, "error": str(exc)})
+
+ def log_message(self, fmt: str, *args) -> None:
+ print(f"[openvino-npu-http] {fmt % args}", flush=True)
+
+ return Handler
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--model-dir", required=True, type=Path)
+ parser.add_argument("--port", required=True, type=int)
+ parser.add_argument("--width", type=int, default=512)
+ parser.add_argument("--height", type=int, default=512)
+ parser.add_argument("--cache-dir", type=Path, default=Path("app/tools/openvino-cache"))
+ args = parser.parse_args()
+
+ state = WorkerState(args.model_dir, args.width, args.height, args.cache_dir)
+ state.load()
+ server = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(state))
+ print(f"[openvino-npu] Listening on http://127.0.0.1:{args.port}", flush=True)
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/serve.cjs b/scripts/serve.cjs
index c8b901a..6b74f06 100644
--- a/scripts/serve.cjs
+++ b/scripts/serve.cjs
@@ -68,15 +68,38 @@ const MODELS = path.join(ROOT, "app", "models");
if (!fs.existsSync(MODELS)) {
fs.mkdirSync(MODELS, { recursive: true });
}
+const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models");
+if (!fs.existsSync(OPENVINO_MODELS)) {
+ fs.mkdirSync(OPENVINO_MODELS, { recursive: true });
+}
const OUTPUTS = path.join(ROOT, "app", "outputs");
if (!fs.existsSync(OUTPUTS)) {
fs.mkdirSync(OUTPUTS, { recursive: true });
}
+const OPENVINO_NPU_MODELS = [
+ {
+ id: "lcm-dreamshaper-v7-fp16",
+ name: "LCM DreamShaper v7 FP16",
+ repo: "OpenVINO/LCM_Dreamshaper_v7-fp16-ov",
+ folder: "LCM_Dreamshaper_v7-fp16-ov",
+ approxSize: "2.0 GB",
+ resolution: "512x512",
+ notes: "OpenVINO LCM image model. Uses CPU text encoder, NPU UNet, and GPU or CPU VAE decoder.",
+ },
+];
+
// ── Backend process state ─────────────────────────────────────────────────────
let backendProc = null;
let backendReady = false;
let backendError = null;
+let openvinoProc = null;
+let openvinoReady = false;
+let openvinoError = null;
+let openvinoPort = null;
+let openvinoModel = null;
+let openvinoWidth = null;
+let openvinoHeight = null;
let backendLoadState = {
active: false,
phase: "",
@@ -104,6 +127,8 @@ let currentSettings = {
backendType: "auto",
vaeTiling: true,
vaeOnCpu: false,
+ width: 512,
+ height: 512,
};
let lastCpuSample = null;
@@ -360,6 +385,98 @@ function getPathSize(targetPath) {
}
}
+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", "Scripts", "python.exe"));
+ candidates.push("C:\\tmp\\npu-test-venv\\Scripts\\python.exe");
+ candidates.push("python");
+ } else {
+ candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python"));
+ candidates.push("python3");
+ candidates.push("python");
+ }
+ return [...new Set(candidates)];
+}
+
+function getOpenVinoPython() {
+ for (const candidate of getOpenVinoPythonCandidates()) {
+ try {
+ const result = spawnSync(candidate, [
+ "-c",
+ "import openvino, openvino_genai, PIL; print(openvino.__version__)",
+ ], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
+ if (result.status === 0) return candidate;
+ } catch (_) {}
+ }
+ return null;
+}
+
+function getOpenVinoNpuInfo() {
+ if (osPlatform !== "win32" && osPlatform !== "linux") {
+ return { supported: false, reason: "OpenVINO NPU backend is supported on Windows and Linux Intel Core Ultra systems." };
+ }
+ const python = getOpenVinoPython();
+ if (!python) {
+ const setupScript = osPlatform === "win32"
+ ? "scripts/setup-openvino-npu.ps1"
+ : "bash scripts/setup-openvino-npu.sh";
+ return {
+ supported: false,
+ reason: `OpenVINO GenAI runtime is not installed. Run ${setupScript} first.`,
+ };
+ }
+ try {
+ const script = [
+ "import json, openvino as ov",
+ "core=ov.Core()",
+ "devices=core.available_devices",
+ "info={'devices':devices,'npu':None}",
+ "if 'NPU' in devices:",
+ " info['npu']={'name':core.get_property('NPU','FULL_DEVICE_NAME'),'capabilities':core.get_property('NPU','OPTIMIZATION_CAPABILITIES')}",
+ "print(json.dumps(info))",
+ ].join("\n");
+ const result = spawnSync(python, ["-c", script], { encoding: "utf8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
+ if (result.status !== 0) {
+ return { supported: false, python, reason: result.stderr.trim() || "OpenVINO NPU probe failed." };
+ }
+ const info = JSON.parse(result.stdout.trim());
+ if (!info.devices.includes("NPU")) {
+ return { supported: false, python, reason: `OpenVINO is installed, but NPU is not available. Devices: ${info.devices.join(", ")}` };
+ }
+ return { supported: true, platform: osPlatform, python, devices: info.devices, npu: info.npu };
+ } catch (err) {
+ return { supported: false, python, reason: err.message || String(err) };
+ }
+}
+
+function getOpenVinoModelInfo() {
+ return OPENVINO_NPU_MODELS.map((model) => {
+ const modelPath = path.join(OPENVINO_MODELS, model.folder);
+ const requiredFiles = [
+ path.join(modelPath, "model_index.json"),
+ path.join(modelPath, "text_encoder", "openvino_model.xml"),
+ path.join(modelPath, "unet", "openvino_model.xml"),
+ path.join(modelPath, "vae_decoder", "openvino_model.xml"),
+ ];
+ const installed = requiredFiles.every((file) => fs.existsSync(file));
+ return {
+ ...model,
+ filename: model.id,
+ path: modelPath,
+ installed,
+ sizeBytes: getPathSize(modelPath),
+ size: formatBytes(getPathSize(modelPath)),
+ format: "OpenVINO",
+ };
+ });
+}
+
+function findOpenVinoModel(modelId) {
+ return getOpenVinoModelInfo().find((model) => model.id === modelId || model.folder === modelId || model.name === modelId);
+}
+
function pathInside(childPath, parentPath) {
const relative = path.relative(path.resolve(parentPath), path.resolve(childPath));
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
@@ -468,7 +585,7 @@ async function getHealth() {
const ports = {
frontend: { ...(await checkPort(PORT_FRONTEND)), expectedInUse: true },
- backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null },
+ backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null || openvinoProc !== null },
};
ports.frontend.ok = !ports.frontend.available;
ports.backend.preferred = PREFERRED_BACKEND_PORT;
@@ -489,8 +606,8 @@ async function getHealth() {
checks,
ports,
backend: {
- ready: backendReady,
- running: backendProc !== null,
+ ready: backendReady || openvinoReady,
+ running: backendProc !== null || openvinoProc !== null,
error: backendError,
settings: currentSettings,
options: getBackendOptions(),
@@ -609,12 +726,16 @@ function getBackendOptions() {
// as Metal-capable so the UI does not default to CPU if a probe flag changes
// upstream.
const metalAvailable = metalInstalled;
+ 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 (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 });
+ if (openvinoNpuAvailable) options.push({ id: "openvino-npu", label: "NPU (OpenVINO)", available: true });
const unavailable = [];
if (vulkanInstalled && !vulkanAvailable) {
@@ -629,6 +750,11 @@ function getBackendOptions() {
if (metalInstalled && !metalAvailable) {
unavailable.push({ id: "metal", label: "Metal GPU", reason: "Installed, but Metal backend validation failed." });
}
+ 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")) {
+ unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: openvinoNpu.reason });
+ }
let defaultBackend = "cpu";
if (metalAvailable) {
@@ -654,6 +780,9 @@ function getBackendOptions() {
vulkanAvailable,
rocmAvailable,
metalAvailable,
+ openvinoNpuAvailable,
+ openvinoNpu,
+ openvinoModels,
defaultBackendType: defaultBackend,
};
return cachedBackendOptions;
@@ -795,6 +924,208 @@ function pingBackendReady() {
});
}
+function requestJson(url, payload = null, timeoutMs = 120000) {
+ return new Promise((resolve, reject) => {
+ const parsed = new URL(url);
+ const data = payload ? Buffer.from(JSON.stringify(payload), "utf8") : null;
+ const req = http.request({
+ hostname: parsed.hostname,
+ port: parsed.port,
+ path: parsed.pathname + parsed.search,
+ method: data ? "POST" : "GET",
+ headers: data ? {
+ "Content-Type": "application/json",
+ "Content-Length": data.length,
+ } : {},
+ timeout: timeoutMs,
+ }, (res) => {
+ let body = "";
+ res.setEncoding("utf8");
+ res.on("data", (chunk) => { body += chunk; });
+ res.on("end", () => {
+ try {
+ const jsonBody = JSON.parse(body || "{}");
+ if (res.statusCode < 200 || res.statusCode >= 300 || jsonBody.ok === false) {
+ reject(new Error(jsonBody.error || `HTTP ${res.statusCode}`));
+ return;
+ }
+ resolve(jsonBody);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ req.on("error", reject);
+ req.on("timeout", () => req.destroy(new Error("Request timed out")));
+ if (data) req.write(data);
+ req.end();
+ });
+}
+
+async function killOpenVinoWorker() {
+ if (!openvinoProc) {
+ openvinoReady = false;
+ return;
+ }
+ console.log(" [openvino-npu] Stopping worker...");
+ try { openvinoProc.kill("SIGTERM"); } catch (_) {}
+ await new Promise((resolve) => setTimeout(resolve, 800));
+ try { openvinoProc.kill("SIGKILL"); } catch (_) {}
+ openvinoProc = null;
+ openvinoReady = false;
+}
+
+function getOpenVinoResolution(settings = {}) {
+ const width = Number(settings.width) || 512;
+ const height = Number(settings.height) || 512;
+ const supported = (width === 512 && height === 512) || (width === 1024 && height === 1024);
+ if (!supported) {
+ throw new Error("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale.");
+ }
+ return { width, height };
+}
+
+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);
+ if (!model || !model.installed) {
+ throw new Error("OpenVINO NPU model is not downloaded. Download an OpenVINO NPU model from Model Manager first.");
+ }
+ const { width, height } = getOpenVinoResolution(settings);
+ if (openvinoProc &&
+ openvinoReady &&
+ openvinoModel === model.id) {
+ currentSettings = { ...currentSettings, ...settings, model: model.id, width, height };
+ return;
+ }
+
+ await killBackend();
+ await killOpenVinoWorker();
+ openvinoError = null;
+ openvinoReady = false;
+ openvinoModel = model.id;
+ openvinoWidth = 512;
+ openvinoHeight = 512;
+ openvinoPort = await findAvailableBackendPort();
+ PORT_BACKEND = openvinoPort;
+
+ currentSettings = { ...currentSettings, ...settings };
+ currentSettings.backendType = "openvino-npu";
+ currentSettings.useGpu = true;
+ currentSettings.backendMode = "NPU (OpenVINO)";
+ currentSettings.backendBinary = "openvino_npu_worker.py";
+ currentSettings.backendDevice = npuInfo.npu?.name || "Intel AI Boost";
+ currentSettings.model = model.id;
+ currentSettings.width = width;
+ currentSettings.height = height;
+
+ backendError = null;
+ backendLoadState = {
+ active: true,
+ phase: "Compiling OpenVINO NPU pipeline (512x512)...",
+ progress: 5,
+ current: 0,
+ total: 0,
+ speed: "",
+ model: model.name,
+ backendMode: "NPU (OpenVINO)",
+ backendBinary: "openvino_npu_worker.py",
+ device: currentSettings.backendDevice,
+ };
+
+ const workerPath = path.join(ROOT, "scripts", "openvino_npu_worker.py");
+ const cacheDir = path.join(ROOT, "app", "tools", "openvino-cache", "512x512");
+ console.log(` [openvino-npu] Starting 512x512 worker on port ${openvinoPort}`);
+ openvinoProc = spawn(npuInfo.python, [
+ workerPath,
+ "--model-dir", model.path,
+ "--port", String(openvinoPort),
+ "--width", "512",
+ "--height", "512",
+ "--cache-dir", cacheDir,
+ ], { stdio: "pipe" });
+
+ let openvinoStdoutBuffer = "";
+ openvinoProc.stdout.on("data", (data) => {
+ const text = data.toString();
+ process.stdout.write(" " + text);
+ openvinoStdoutBuffer += text;
+ const lines = openvinoStdoutBuffer.split(/\r?\n/);
+ openvinoStdoutBuffer = lines.pop() || "";
+ for (const line of lines) {
+ if (line.includes("READY")) {
+ openvinoReady = true;
+ backendReady = true;
+ backendLoadState = { ...backendLoadState, active: false, phase: "OpenVINO NPU model ready", progress: 100 };
+ }
+ const progressMatch = line.match(/PROGRESS\s+(\d+)\/(\d+)\s+(.+)$/);
+ if (progressMatch) {
+ generationState.active = true;
+ generationState.step = Number(progressMatch[1]);
+ generationState.steps = Number(progressMatch[2]);
+ generationState.speed = progressMatch[3].trim();
+ generationState.decoding = false;
+ } else if (line.includes("DECODING") && generationState.active) {
+ generationState.step = generationState.steps;
+ generationState.decoding = true;
+ generationState.speed = "";
+ }
+ }
+ });
+ openvinoProc.stderr.on("data", (data) => {
+ const text = data.toString();
+ process.stderr.write(" " + text);
+ const cleanText = stripAnsi(text).trim();
+ if (cleanText && (cleanText.includes("ERROR") || cleanText.includes("Traceback") || cleanText.includes("Exception"))) {
+ openvinoError = cleanText;
+ backendError = openvinoError;
+ }
+ });
+ openvinoProc.on("exit", (code) => {
+ console.log(" [openvino-npu] worker exited with code", code);
+ openvinoProc = null;
+ openvinoReady = false;
+ backendReady = false;
+ if (code !== 0 && code !== null && !backendError) backendError = `OpenVINO NPU worker exited with code ${code}`;
+ backendLoadState.active = false;
+ });
+
+ const startedAt = Date.now();
+ while (Date.now() - startedAt < 360000) {
+ if (openvinoReady) return;
+ if (backendError) throw new Error(backendError);
+ if (!openvinoProc) throw new Error("OpenVINO NPU worker exited during startup.");
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
+ backendLoadState.progress = Math.min(95, 5 + Math.round(elapsed / 2));
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ await killOpenVinoWorker();
+ throw new Error("OpenVINO NPU 512x512 compile timed out after 6 minutes.");
+}
+
+async function generateWithOpenVino(body) {
+ if (!openvinoReady || !openvinoPort) throw new Error("OpenVINO NPU worker is not ready. Load an OpenVINO NPU model first.");
+ const steps = Math.max(1, Math.min(8, Number(body.steps) || 4));
+ generationState = { active: true, step: 0, steps, speed: "", decoding: false };
+ try {
+ const result = await requestJson(`http://127.0.0.1:${openvinoPort}/generate`, {
+ prompt: body.prompt,
+ negative_prompt: body.negative_prompt || "",
+ width: Number(body.width) || currentSettings.width || 512,
+ height: Number(body.height) || currentSettings.height || 512,
+ steps,
+ cfg_scale: Number(body.cfg_scale) || 1.0,
+ seed: body.seed,
+ }, 300000);
+ resetGenerationState();
+ return result;
+ } catch (err) {
+ resetGenerationState();
+ throw err;
+ }
+}
+
function startBackendReadyPoll() {
let attempts = 0;
const interval = setInterval(async () => {
@@ -839,6 +1170,11 @@ function killBackend() {
}
async function startBackend(settings = {}) {
+ if (settings.backendType === "openvino-npu") {
+ await startOpenVinoWorker(settings);
+ return;
+ }
+ await killOpenVinoWorker();
backendError = null;
currentSettings = { ...currentSettings, ...settings };
if (!currentSettings.model) currentSettings.model = getDefaultModel();
@@ -1118,6 +1454,54 @@ let downloadState = {
};
let activeDownload = null;
+function startOpenVinoModelDownload(modelId) {
+ if (downloadState.active) return;
+ const npuInfo = getOpenVinoNpuInfo();
+ if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU runtime is not available.");
+ const model = OPENVINO_NPU_MODELS.find((item) => item.id === modelId);
+ if (!model) throw new Error("Unknown OpenVINO model.");
+
+ const destDir = path.join(OPENVINO_MODELS, model.folder);
+ fs.mkdirSync(destDir, { recursive: true });
+ downloadState = {
+ active: true,
+ filename: model.name,
+ progress: -1,
+ speed: "Downloading snapshot",
+ eta: 0,
+ totalBytes: 0,
+ downloadedBytes: 0,
+ error: null,
+ };
+
+ const script = [
+ "from huggingface_hub import snapshot_download",
+ `snapshot_download(repo_id=${JSON.stringify(model.repo)}, local_dir=${JSON.stringify(destDir)}, ignore_patterns=['safety_checker/*','feature_extractor/*','*.safetensors'])`,
+ "print('DONE')",
+ ].join("\n");
+ console.log(` [openvino-download] Downloading ${model.repo} -> ${destDir}`);
+ const proc = spawn(npuInfo.python, ["-c", script], { stdio: "pipe" });
+ activeDownload = { process: proc, destPath: destDir, openvino: true };
+ proc.stdout.on("data", (data) => process.stdout.write(" [openvino-download] " + data.toString()));
+ proc.stderr.on("data", (data) => process.stderr.write(" [openvino-download] " + data.toString()));
+ proc.on("exit", (code) => {
+ activeDownload = null;
+ if (String(downloadState.error || "").toLowerCase().includes("cancelled")) {
+ return;
+ }
+ downloadState.active = false;
+ if (code === 0) {
+ downloadState.progress = 100;
+ downloadState.downloadedBytes = getPathSize(destDir);
+ downloadState.totalBytes = downloadState.downloadedBytes;
+ downloadState.speed = "Complete";
+ cachedBackendOptions = null;
+ } else {
+ downloadState.error = `OpenVINO model download failed with code ${code}`;
+ }
+ });
+}
+
// ── Generation State (Real-time progress parser) ─────────────────────────────
let generationState = {
active: false,
@@ -1342,9 +1726,14 @@ function cancelModelDownload() {
}
const filename = downloadState.filename;
if (activeDownload) {
- try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {}
- try { activeDownload.fileStream.destroy(); } catch (_) {}
- try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
+ if (activeDownload.process) {
+ try { activeDownload.process.kill("SIGTERM"); } catch (_) {}
+ setTimeout(() => { try { activeDownload?.process?.kill("SIGKILL"); } catch (_) {} }, 1000);
+ } else {
+ try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {}
+ try { activeDownload.fileStream.destroy(); } catch (_) {}
+ try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
+ }
} else if (filename) {
try { fs.unlinkSync(`${path.join(MODELS, filename)}.part`); } catch (_) {}
}
@@ -1514,9 +1903,9 @@ function safeOutputName(value) {
}
function saveGeneratedOutput(imageDataUrl, metadata = {}) {
- const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp|svg\+xml));base64,(.+)$/);
+ const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/);
if (!match) {
- throw new Error("Expected a base64 image data URL");
+ throw new Error("Expected a real base64 PNG, JPEG, or WebP image data URL");
}
const mime = match[1];
@@ -1525,9 +1914,26 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) {
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/webp": ".webp",
- "image/svg+xml": ".svg",
};
const ext = extByMime[mime] || ".png";
+ const imageBuffer = Buffer.from(match[2], "base64");
+ const isPng = imageBuffer.length > 8 &&
+ imageBuffer[0] === 0x89 &&
+ imageBuffer[1] === 0x50 &&
+ imageBuffer[2] === 0x4e &&
+ imageBuffer[3] === 0x47;
+ const isJpeg = imageBuffer.length > 3 &&
+ imageBuffer[0] === 0xff &&
+ imageBuffer[1] === 0xd8 &&
+ imageBuffer[2] === 0xff;
+ const isWebp = imageBuffer.length > 12 &&
+ imageBuffer.toString("ascii", 0, 4) === "RIFF" &&
+ imageBuffer.toString("ascii", 8, 12) === "WEBP";
+ if ((mime === "image/png" && !isPng) ||
+ ((mime === "image/jpeg" || mime === "image/jpg") && !isJpeg) ||
+ (mime === "image/webp" && !isWebp)) {
+ throw new Error("Generated image payload did not match its declared image format.");
+ }
const createdAt = new Date().toISOString();
const stamp = createdAt.replace(/[:.]/g, "-");
const seed = metadata.seed !== undefined && metadata.seed !== null ? `-${safeOutputName(metadata.seed)}` : "";
@@ -1537,7 +1943,7 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) {
const imagePath = path.join(OUTPUTS, imageFilename);
const metadataPath = path.join(OUTPUTS, metadataFilename);
- fs.writeFileSync(imagePath, Buffer.from(match[2], "base64"));
+ fs.writeFileSync(imagePath, imageBuffer);
const savedMetadata = {
...metadata,
createdAt,
@@ -1703,9 +2109,9 @@ const server = http.createServer(async (req, res) => {
// GET /api/backend-status
if (req.url === "/api/backend-status" && req.method === "GET") {
- return json(res, 200, {
- ready: backendReady,
- running: backendProc !== null,
+ return json(res, 200, {
+ ready: backendReady || openvinoReady,
+ running: backendProc !== null || openvinoProc !== null,
port: PORT_BACKEND,
preferredPort: PREFERRED_BACKEND_PORT,
error: backendError,
@@ -1742,6 +2148,17 @@ const server = http.createServer(async (req, res) => {
} catch (_) { return json(res, 200, { models: [] }); }
}
+ if (req.url === "/api/openvino-models" && req.method === "GET") {
+ const npuInfo = getOpenVinoNpuInfo();
+ return json(res, 200, {
+ supported: npuInfo.supported,
+ reason: npuInfo.reason || "",
+ npu: npuInfo.npu || null,
+ python: npuInfo.python || "",
+ models: npuInfo.supported ? getOpenVinoModelInfo() : [],
+ });
+ }
+
// POST /api/restart-backend — restart with new settings
if (req.url === "/api/restart-backend" && req.method === "POST") {
const body = await readJsonBody(req, res);
@@ -1750,11 +2167,13 @@ const server = http.createServer(async (req, res) => {
await killBackend();
await new Promise(r => setTimeout(r, 500));
const newSettings = {};
- if (body.model) newSettings.model = path.join(MODELS, body.model);
+ if (body.model) newSettings.model = body.backend_type === "openvino-npu" ? String(body.model) : path.join(MODELS, body.model);
if (body.steps) newSettings.steps = parseInt(body.steps);
if (body.cfgScale) newSettings.cfgScale = parseFloat(body.cfgScale);
if (body.sampler) newSettings.sampler = body.sampler;
if (body.threads) newSettings.threads = parseInt(body.threads);
+ if (body.width) newSettings.width = parseInt(body.width);
+ if (body.height) newSettings.height = parseInt(body.height);
if (typeof body.use_gpu === "boolean") newSettings.useGpu = body.use_gpu;
if (body.backend_type) {
newSettings.backendType = String(body.backend_type);
@@ -1763,6 +2182,18 @@ const server = http.createServer(async (req, res) => {
if (typeof body.vae_tiling === "boolean") newSettings.vaeTiling = body.vae_tiling;
if (typeof body.vae_on_cpu === "boolean") newSettings.vaeOnCpu = body.vae_on_cpu;
try {
+ if (newSettings.backendType === "openvino-npu") {
+ startBackend(newSettings).catch((err) => {
+ backendError = err.message || String(err);
+ backendLoadState = {
+ ...backendLoadState,
+ active: false,
+ phase: "OpenVINO NPU model load failed",
+ };
+ console.error(" [openvino-npu] Startup failed:", backendError);
+ });
+ return json(res, 200, { ok: true, message: "OpenVINO NPU backend starting...", settings: currentSettings, port: PORT_BACKEND });
+ }
await startBackend(newSettings);
return json(res, 200, { ok: true, message: "Backend restarting...", settings: currentSettings, port: PORT_BACKEND });
} catch (err) {
@@ -1774,6 +2205,7 @@ const server = http.createServer(async (req, res) => {
// POST /api/stop-backend
if (req.url === "/api/stop-backend" && req.method === "POST") {
await killBackend();
+ await killOpenVinoWorker();
return json(res, 200, { ok: true });
}
@@ -1788,6 +2220,19 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { ok: true, message: "Download started" });
}
+ if (req.url === "/api/download-openvino-model" && req.method === "POST") {
+ const body = await readJsonBody(req, res);
+ if (!body) return;
+ const modelId = String(body.model_id || body.modelId || "");
+ if (!modelId) return json(res, 400, { ok: false, error: "model_id is required" });
+ try {
+ startOpenVinoModelDownload(modelId);
+ return json(res, 200, { ok: true, message: "OpenVINO model download started" });
+ } catch (err) {
+ return json(res, 500, { ok: false, error: err.message || String(err) });
+ }
+ }
+
// GET /api/download-progress
if (req.url === "/api/download-progress" && req.method === "GET") {
return json(res, 200, downloadState);
@@ -1804,6 +2249,16 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, generationState);
}
+ if (req.url === "/api/openvino-generate" && req.method === "POST") {
+ const body = await readJsonBody(req, res);
+ if (!body) return;
+ try {
+ return json(res, 200, await generateWithOpenVino(body));
+ } catch (err) {
+ return json(res, 500, { ok: false, error: err.message || String(err) });
+ }
+ }
+
// GET /api/outputs
if (req.url === "/api/outputs" && req.method === "GET") {
return json(res, 200, { outputs: listGeneratedOutputs() });
@@ -1871,11 +2326,17 @@ const server = http.createServer(async (req, res) => {
const { filename } = body;
if (!filename) return json(res, 400, { error: "Filename is required" });
+ const openvinoModel = findOpenVinoModel(filename);
const safeFilename = path.basename(filename);
const modelPath = path.join(MODELS, safeFilename);
try {
- if (fs.existsSync(modelPath)) {
+ if (openvinoModel?.installed && openvinoModel.path && pathInside(openvinoModel.path, OPENVINO_MODELS)) {
+ fs.rmSync(openvinoModel.path, { recursive: true, force: true });
+ cachedBackendOptions = null;
+ console.log(` [api] Deleted OpenVINO model: ${openvinoModel.id}`);
+ return json(res, 200, { ok: true, message: `Deleted ${openvinoModel.name}` });
+ } else if (fs.existsSync(modelPath)) {
fs.unlinkSync(modelPath);
console.log(` [api] Deleted model file: ${safeFilename}`);
return json(res, 200, { ok: true, message: `Deleted ${safeFilename}` });
diff --git a/scripts/setup-openvino-npu.ps1 b/scripts/setup-openvino-npu.ps1
new file mode 100644
index 0000000..da5f70e
--- /dev/null
+++ b/scripts/setup-openvino-npu.ps1
@@ -0,0 +1,46 @@
+# Optional OpenVINO NPU setup for Intel AI Boost systems.
+# This does not modify the existing stable-diffusion.cpp backends.
+
+$ErrorActionPreference = "Stop"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$rootDir = Split-Path -Parent $scriptDir
+$venvDir = Join-Path $rootDir "app\tools\openvino-venv"
+$pythonExe = Join-Path $venvDir "Scripts\python.exe"
+
+Write-Host ""
+Write-Host " ============================================================"
+Write-Host " Local AI Image Generator - OpenVINO NPU Setup"
+Write-Host " ============================================================"
+Write-Host ""
+
+$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
+
+if (-not $npu) {
+ Write-Host " [ERROR] Intel AI Boost NPU was not detected on this system." -ForegroundColor Red
+ exit 1
+}
+
+Write-Host " Detected: $($npu.Name)"
+
+if (-not (Test-Path $pythonExe)) {
+ Write-Host " Creating Python environment: $venvDir"
+ python -m venv $venvDir
+}
+
+Write-Host " Installing OpenVINO GenAI runtime..."
+& $pythonExe -m pip install --upgrade pip
+& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub
+
+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)"
+
+Write-Host ""
+Write-Host " ============================================================"
+Write-Host " OpenVINO NPU setup complete."
+Write-Host " Restart windows.bat, then download an OpenVINO NPU model."
+Write-Host " ============================================================"
+Write-Host ""
diff --git a/scripts/setup-openvino-npu.sh b/scripts/setup-openvino-npu.sh
new file mode 100644
index 0000000..fc74d9d
--- /dev/null
+++ b/scripts/setup-openvino-npu.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+#
+# Optional OpenVINO NPU setup for Linux Intel Core Ultra systems.
+# The Intel NPU kernel/user-mode driver must already be installed.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+VENV_DIR="$ROOT_DIR/app/tools/openvino-venv"
+PYTHON_BIN="$VENV_DIR/bin/python"
+
+echo ""
+echo " ============================================================"
+echo " Local AI Image Generator - Linux OpenVINO NPU Setup"
+echo " ============================================================"
+echo ""
+
+if [[ "$(uname -s)" != "Linux" ]]; then
+ echo " [ERROR] This setup script is for Linux only." >&2
+ exit 1
+fi
+
+if [[ "$(uname -m)" != "x86_64" ]]; then
+ echo " [ERROR] OpenVINO NPU support requires x86_64 Linux." >&2
+ exit 1
+fi
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo " [ERROR] python3 is required. Install Python 3 and the venv module." >&2
+ exit 1
+fi
+
+KERNEL_VERSION="$(uname -r)"
+KERNEL_MAJOR="${KERNEL_VERSION%%.*}"
+KERNEL_REST="${KERNEL_VERSION#*.}"
+KERNEL_MINOR="${KERNEL_REST%%.*}"
+if (( KERNEL_MAJOR < 6 || (KERNEL_MAJOR == 6 && KERNEL_MINOR < 6) )); then
+ echo " [ERROR] Intel NPU support requires Linux kernel 6.6 or newer."
+ echo " Current kernel: $KERNEL_VERSION"
+ exit 1
+fi
+
+if [[ ! -e /dev/accel/accel0 ]]; then
+ echo " [ERROR] Intel NPU device /dev/accel/accel0 was not found."
+ echo " Install the Intel Linux NPU driver and reboot first."
+ echo " Driver: https://github.com/intel/linux-npu-driver"
+ 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."
+ echo " On Ubuntu, install python3-venv and retry."
+ exit 1
+ fi
+fi
+
+echo " Installing OpenVINO GenAI runtime..."
+"$PYTHON_BIN" -m pip install --upgrade pip
+"$PYTHON_BIN" -m pip install openvino openvino-genai pillow huggingface-hub
+
+echo ""
+echo " Verifying OpenVINO NPU device..."
+"$PYTHON_BIN" - <<'PY'
+import openvino as ov
+
+core = ov.Core()
+devices = core.available_devices
+print(" Available devices:", ", ".join(devices))
+if "NPU" not in devices:
+ raise SystemExit(
+ " [ERROR] OpenVINO is installed, but NPU is unavailable. "
+ "Verify the Intel NPU driver and device permissions."
+ )
+print(" NPU:", core.get_property("NPU", "FULL_DEVICE_NAME"))
+PY
+
+echo ""
+echo " ============================================================"
+echo " OpenVINO NPU setup complete."
+echo " Run ./linux.sh, then download an OpenVINO NPU model."
+echo " ============================================================"
+echo ""
diff --git a/scripts/setup.ps1 b/scripts/setup.ps1
index cfa6836..feabd06 100644
--- a/scripts/setup.ps1
+++ b/scripts/setup.ps1
@@ -47,6 +47,18 @@ function Format-Speed {
return "{0:N0} KB/s" -f ($bps / 1KB)
}
+function Enable-Tls12 {
+ try {
+ # Use the protocol's numeric value because older .NET enum definitions
+ # do not expose the Tls12 member even when Windows supports TLS 1.2.
+ $tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
+ [Net.ServicePointManager]::SecurityProtocol =
+ [Net.ServicePointManager]::SecurityProtocol -bor $tls12
+ } catch {
+ throw "TLS 1.2 could not be enabled. This setup requires 64-bit Windows 10 or Windows 11 with current system updates. $($_.Exception.Message)"
+ }
+}
+
function Invoke-RichDownload {
param([string]$Url, [string]$Dest, [string]$Label)
Print-Info "Downloading: $Label"
@@ -57,7 +69,7 @@ function Invoke-RichDownload {
$lastTime = [DateTime]::Now
try {
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Enable-Tls12
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.UserAgent = "Mozilla/5.0"
$resp = $req.GetResponse()