Merge remote-tracking branch 'origin/linux' into linux
# Conflicts: # scripts/serve.cjs
This commit is contained in:
commit
b55e62be7d
24 files changed
+1474
-179
No files matched your search
@@ -97,6 +97,8 @@ function App() {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
steps: 4, // Recommended default steps for Flux.1 Schnell/Lightning
|
||||
npuSteps: 4,
|
||||
standardSteps: 20,
|
||||
cfgScale: 1.0, // Recommended default CFG for Flux.1 Schnell
|
||||
sampler: "euler_a",
|
||||
seed: -1,
|
||||
@@ -277,10 +279,22 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (activeModel) {
|
||||
const name = activeModel.toLowerCase();
|
||||
if (name.includes("flux") || name.includes("schnell")) {
|
||||
if (name.includes("lcm-dreamshaper-v7-fp16")) {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 4,
|
||||
npuSteps: 4,
|
||||
cfgScale: 1.0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
backendType: "openvino-npu",
|
||||
useGpu: true,
|
||||
}));
|
||||
} else if (name.includes("flux") || name.includes("schnell")) {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 4,
|
||||
standardSteps: 4,
|
||||
cfgScale: 1.0,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
@@ -289,6 +303,7 @@ function App() {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 4,
|
||||
standardSteps: 4,
|
||||
cfgScale: 1.5,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
@@ -297,6 +312,7 @@ function App() {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 25,
|
||||
standardSteps: 25,
|
||||
cfgScale: 7.0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
@@ -305,6 +321,7 @@ function App() {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 20,
|
||||
standardSteps: 20,
|
||||
cfgScale: 4.5,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
@@ -314,6 +331,7 @@ function App() {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
steps: 20,
|
||||
standardSteps: 20,
|
||||
cfgScale: 7.0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
@@ -353,6 +371,7 @@ function App() {
|
||||
serverRunning={serverRunning}
|
||||
setServerRunning={setServerRunning}
|
||||
constraints={constraints}
|
||||
backendOptions={backendOptions}
|
||||
showAlert={showAlert}
|
||||
showConfirm={showConfirm}
|
||||
/>
|
||||
@@ -365,6 +384,11 @@ function App() {
|
||||
activeModel={activeModel}
|
||||
specs={specs}
|
||||
backendOptions={backendOptions}
|
||||
serverRunning={serverRunning}
|
||||
setServerRunning={setServerRunning}
|
||||
setActiveModel={setActiveModel}
|
||||
showAlert={showAlert}
|
||||
showConfirm={showConfirm}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -98,6 +98,8 @@ function Generator({
|
||||
settings.sampler !== constraints.sampler ||
|
||||
parseInt(settings.threads) !== parseInt(constraints.threads) ||
|
||||
Boolean(settings.useGpu) !== (constraints.useGpu !== false) ||
|
||||
parseInt(settings.width || 512) !== parseInt(constraints.width || 512) ||
|
||||
parseInt(settings.height || 512) !== parseInt(constraints.height || 512) ||
|
||||
(settings.backendType || (settings.useGpu === false ? "cpu" : "auto")) !== (constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"))) {
|
||||
needsRestart = true;
|
||||
}
|
||||
@@ -115,7 +117,8 @@ function Generator({
|
||||
|
||||
let isReady = false;
|
||||
let crashError = null;
|
||||
for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms)
|
||||
const maxStartupPolls = constraints.backendType === "openvino-npu" ? 1200 : 240;
|
||||
for (let i = 0; i < maxStartupPolls; i++) {
|
||||
const status = await getBackendStatus();
|
||||
if (status.loading) {
|
||||
setRestartLoadProgress({
|
||||
@@ -136,7 +139,7 @@ function Generator({
|
||||
crashError = status.error;
|
||||
break;
|
||||
}
|
||||
if (!status.running && i > 3) {
|
||||
if (!status.running && !status.loading?.active && i > 3) {
|
||||
crashError = "The backend process terminated immediately on startup.";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { memo } from "react";
|
||||
import React, { memo, useEffect } from "react";
|
||||
import { Crop, Sliders, Cpu, Info } from "lucide-react";
|
||||
import { stopServer } from "../services/api";
|
||||
|
||||
const ASPECT_RATIOS = [
|
||||
{ id: "1:1", label: "1:1 Square", width: 512, height: 512, sdxl_width: 1024, sdxl_height: 1024, desc: "Social posts & avatars" },
|
||||
@@ -17,26 +18,55 @@ const isSD15OrCustomModel = (modelName) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
function ImageConstraints({ constraints, setConstraints, activeModel, specs, backendOptions }) {
|
||||
function ImageConstraints({
|
||||
constraints,
|
||||
setConstraints,
|
||||
activeModel,
|
||||
specs,
|
||||
backendOptions,
|
||||
serverRunning,
|
||||
setServerRunning,
|
||||
setActiveModel,
|
||||
showAlert = async ({ message }) => window.alert(message),
|
||||
showConfirm = async ({ message }) => window.confirm(message),
|
||||
}) {
|
||||
const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false;
|
||||
const isOpenVinoNpu = constraints.backendType === "openvino-npu";
|
||||
const forceStandardMode = isSD15OrCustom && !isOpenVinoNpu;
|
||||
const availableBackends = backendOptions?.options?.length
|
||||
? backendOptions.options
|
||||
: [{ id: "cpu", label: "CPU", available: true }];
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenVinoNpu && constraints.steps > 8) {
|
||||
setConstraints((prev) => ({ ...prev, steps: 8, npuSteps: 8 }));
|
||||
}
|
||||
}, [constraints.steps, isOpenVinoNpu, setConstraints]);
|
||||
|
||||
const updateConstraint = (key, value) => {
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
[key]: value,
|
||||
...(key === "steps"
|
||||
? isOpenVinoNpu
|
||||
? { npuSteps: value }
|
||||
: { standardSteps: value }
|
||||
: {}),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAspectRatioChange = (ratio, sizeType) => {
|
||||
if (isOpenVinoNpu && ratio !== "1:1") return;
|
||||
const isSDXL = sizeType === "sdxl" && !isSD15OrCustom;
|
||||
const selected = ASPECT_RATIOS.find((r) => r.id === ratio);
|
||||
if (selected) {
|
||||
let w = isSDXL ? selected.sdxl_width : selected.width;
|
||||
let h = isSDXL ? selected.sdxl_height : selected.height;
|
||||
if (isSD15OrCustom) {
|
||||
if (isOpenVinoNpu) {
|
||||
const size = constraints.width >= 1024 ? 1024 : 512;
|
||||
w = size;
|
||||
h = size;
|
||||
} else if (isSD15OrCustom) {
|
||||
if (w > h) {
|
||||
h = Math.round((h * 512) / w);
|
||||
w = 512;
|
||||
@@ -52,11 +82,54 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackendChange = (backendType) => {
|
||||
const handleBackendChange = async (backendType) => {
|
||||
const currentBackend = constraints.backendType || "cpu";
|
||||
if (backendType === currentBackend) return;
|
||||
|
||||
const switchesAccelerator =
|
||||
(currentBackend === "openvino-npu" && backendType !== "openvino-npu") ||
|
||||
(currentBackend !== "openvino-npu" && backendType === "openvino-npu");
|
||||
|
||||
if (serverRunning && switchesAccelerator) {
|
||||
const leavingNpu = currentBackend === "openvino-npu";
|
||||
const confirmed = await showConfirm({
|
||||
title: leavingNpu ? "Unload NPU Model?" : "Unload Model?",
|
||||
message: leavingNpu
|
||||
? "The OpenVINO NPU model must be unloaded before switching to the standard backend."
|
||||
: "The active model must be unloaded before switching to the OpenVINO NPU backend.",
|
||||
confirmLabel: "Unload",
|
||||
cancelLabel: "Cancel",
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await stopServer();
|
||||
setServerRunning(false);
|
||||
setActiveModel(null);
|
||||
} catch (err) {
|
||||
await showAlert({
|
||||
title: "Unload Failed",
|
||||
message: err.message || String(err),
|
||||
danger: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setConstraints((prev) => ({
|
||||
...prev,
|
||||
backendType,
|
||||
useGpu: backendType !== "cpu",
|
||||
steps: backendType === "openvino-npu"
|
||||
? Math.max(1, Math.min(8, prev.npuSteps || 4))
|
||||
: Math.max(1, Math.min(60, prev.standardSteps || 20)),
|
||||
...(backendType === "openvino-npu"
|
||||
? {
|
||||
width: prev.width >= 1024 ? 1024 : 512,
|
||||
height: prev.width >= 1024 ? 1024 : 512,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -79,7 +152,25 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
1. Picture Size & Shape
|
||||
</h3>
|
||||
|
||||
{isSD15OrCustom && (
|
||||
{isOpenVinoNpu ? (
|
||||
<div style={{
|
||||
background: "rgba(99, 102, 241, 0.1)",
|
||||
border: "1px solid var(--md-sys-color-primary)",
|
||||
color: "var(--md-sys-color-on-surface)",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
marginBottom: "16px",
|
||||
fontSize: "0.85rem",
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "flex-start"
|
||||
}}>
|
||||
<Info size={16} style={{ color: "var(--md-sys-color-primary)", flexShrink: 0, marginTop: "2px" }} />
|
||||
<div>
|
||||
<strong>OpenVINO NPU Resolution:</strong> The NPU generates at its stable 512x512 resolution. HD mode produces a 1024x1024 output using high-quality Lanczos upscaling without recompiling the model.
|
||||
</div>
|
||||
</div>
|
||||
) : isSD15OrCustom && (
|
||||
<div style={{
|
||||
background: "rgba(251, 191, 36, 0.1)",
|
||||
border: "1px solid rgb(251, 191, 36)",
|
||||
@@ -104,7 +195,7 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
<div className="m3-text-field">
|
||||
<label className="m3-text-field-label">AI Generation Engine Optimization</label>
|
||||
<div className="m3-segmented-button">
|
||||
{isSD15OrCustom ? (
|
||||
{forceStandardMode ? (
|
||||
<div
|
||||
className="m3-segment-item disabled"
|
||||
style={{
|
||||
@@ -115,7 +206,7 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
}}
|
||||
title="High Quality Mode is disabled for SD 1.x models."
|
||||
>
|
||||
High Quality Mode (1024px)
|
||||
{isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
@@ -125,11 +216,11 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
updateConstraint("height", 1024);
|
||||
}}
|
||||
>
|
||||
High Quality Mode (1024px)
|
||||
{isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`m3-segment-item ${constraints.width < 1024 || isSD15OrCustom ? "active" : ""}`}
|
||||
className={`m3-segment-item ${constraints.width < 1024 ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
updateConstraint("width", 512);
|
||||
updateConstraint("height", 512);
|
||||
@@ -139,7 +230,9 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px" }}>
|
||||
{isSD15OrCustom
|
||||
{isOpenVinoNpu
|
||||
? "*HD mode generates at 512x512 on the NPU, then upscales to 1024x1024. It improves output size, but does not add native diffusion detail."
|
||||
: isSD15OrCustom
|
||||
? "*Standard Speed Mode (512px) is forced for SD 1.5 / custom models to ensure stable local generations."
|
||||
: "*Use High Quality (1024px) for Flux or SDXL models. Use Standard (512px) for SD 1.5."}
|
||||
</span>
|
||||
@@ -151,10 +244,12 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }}>
|
||||
{ASPECT_RATIOS.map((ratio) => {
|
||||
const isActive = Math.abs((constraints.width / constraints.height) - (ratio.width / ratio.height)) < 0.1;
|
||||
const isUnsupportedOpenVinoShape = isOpenVinoNpu && ratio.id !== "1:1";
|
||||
return (
|
||||
<button
|
||||
key={ratio.id}
|
||||
className={`m3-btn m3-btn-outlined aspect-ratio-btn ${isActive ? "active" : ""}`}
|
||||
disabled={isUnsupportedOpenVinoShape}
|
||||
style={{
|
||||
height: "86px",
|
||||
display: "flex",
|
||||
@@ -165,12 +260,14 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
borderRadius: "var(--md-shape-corner-medium)",
|
||||
borderColor: "var(--md-sys-color-outline-variant)"
|
||||
}}
|
||||
onClick={() => handleAspectRatioChange(ratio.id, isSD15OrCustom ? "sd15" : (constraints.width >= 1024 ? "sdxl" : "sd15"))}
|
||||
onClick={() => handleAspectRatioChange(ratio.id, forceStandardMode ? "sd15" : (constraints.width >= 1024 ? "sdxl" : "sd15"))}
|
||||
>
|
||||
<div className={`aspect-ratio-preview ratio-${ratio.id.replace(":", "-")}`}></div>
|
||||
<span style={{ fontWeight: 600, fontSize: "0.85rem", marginTop: "2px" }}>{ratio.label}</span>
|
||||
<span style={{ fontSize: "0.68rem", opacity: 0.7 }}>
|
||||
{isSD15OrCustom
|
||||
{isUnsupportedOpenVinoShape
|
||||
? "Not yet supported on NPU"
|
||||
: forceStandardMode
|
||||
? `Max: ${ratio.id === "1:1" ? "512x512" : ratio.id === "4:3" ? "512x384" : ratio.id === "16:9" ? "512x288" : "288x512"}`
|
||||
: ratio.desc}
|
||||
</span>
|
||||
@@ -202,10 +299,12 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
value={constraints.steps}
|
||||
onChange={(e) => updateConstraint("steps", parseInt(e.target.value))}
|
||||
min="1"
|
||||
max="60"
|
||||
max={isOpenVinoNpu ? "8" : "60"}
|
||||
/>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", lineHeight: "1.3" }}>
|
||||
The number of times the AI cleans up the image. More steps = sharper details, but takes longer.
|
||||
{isOpenVinoNpu
|
||||
? "LCM OpenVINO uses 1-8 fast inference steps. Progress is reported after each completed NPU step."
|
||||
: "The number of times the AI cleans up the image. More steps = sharper details, but takes longer."}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { memo, useState, useEffect } from "react";
|
||||
import { FolderOpen, DownloadCloud, RefreshCw, Database, Trash2, Square, HardDrive, Library, AlertTriangle } from "lucide-react";
|
||||
import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api";
|
||||
import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, downloadOpenVinoModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api";
|
||||
|
||||
const MODEL_LIBRARY = [
|
||||
{
|
||||
@@ -60,8 +60,28 @@ const MODEL_LIBRARY = [
|
||||
},
|
||||
];
|
||||
|
||||
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) {
|
||||
const OPENVINO_MODEL_LIBRARY = [
|
||||
{
|
||||
group: "Intel NPU - OpenVINO Test",
|
||||
items: [
|
||||
{
|
||||
id: "lcm-dreamshaper-v7-fp16",
|
||||
name: "LCM DreamShaper v7 FP16",
|
||||
filename: "lcm-dreamshaper-v7-fp16",
|
||||
format: "OpenVINO",
|
||||
backendType: "openvino-npu",
|
||||
approxSize: "2.0 GB",
|
||||
resolution: "512x512",
|
||||
notes: "NPU test model for Windows and Linux. Runs the text encoder on CPU, UNet on Intel NPU, and VAE decoder on Intel GPU or CPU fallback.",
|
||||
url: "https://huggingface.co/OpenVINO/LCM_Dreamshaper_v7-fp16-ov",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) {
|
||||
const [localModels, setLocalModels] = useState([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(true);
|
||||
const [downloadingModelId, setDownloadingModelId] = useState(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const [downloadSpeed, setDownloadSpeed] = useState("");
|
||||
@@ -78,6 +98,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
|
||||
const modelNames = localModels.map((model) => normalizeModel(model).filename);
|
||||
const isBusy = loadingModelId !== null || isUnloading;
|
||||
const openvinoSupported = Boolean(backendOptions?.openvinoNpu?.supported);
|
||||
const visibleModelLibrary = openvinoSupported ? [...MODEL_LIBRARY, ...OPENVINO_MODEL_LIBRARY] : MODEL_LIBRARY;
|
||||
const getLocalModelInfo = (modelId) => localModels.map(normalizeModel).find((model) => model.filename === modelId);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels();
|
||||
@@ -149,11 +172,14 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
setIsLoadingModels(true);
|
||||
try {
|
||||
const list = await listLocalModels();
|
||||
setLocalModels(list.map(normalizeModel).filter((model) => model.filename));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,7 +243,21 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
setDownloadUrl("");
|
||||
};
|
||||
|
||||
const handleLibraryDownload = (model) => {
|
||||
const handleLibraryDownload = async (model) => {
|
||||
if (model.backendType === "openvino-npu") {
|
||||
if (downloadingModelId) return;
|
||||
try {
|
||||
const res = await downloadOpenVinoModel(model.id);
|
||||
if (res.ok) {
|
||||
startProgressPolling(model.filename);
|
||||
} else {
|
||||
showAlert({ title: "Download Failed", message: res.error || "Unknown error", danger: true });
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert({ title: "Download Error", message: err.message, danger: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
downloadByUrl(model.url, model.filename);
|
||||
};
|
||||
|
||||
@@ -246,7 +286,11 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
|
||||
if (!isTauriDesktop && !isLocalServerMode) {
|
||||
const isAlreadyRunning = await pingServer();
|
||||
const cmd = `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`;
|
||||
const modelInfo = getLocalModelInfo(modelId);
|
||||
const isOpenVinoModel = modelInfo?.backendType === "openvino-npu";
|
||||
const cmd = isOpenVinoModel
|
||||
? `Run the platform launcher and load "${modelId}" from Model Manager on a configured Intel NPU machine.`
|
||||
: `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`;
|
||||
|
||||
if (isAlreadyRunning) {
|
||||
if (await showConfirm({ title: "Web Browser Mode", message: `To load "${modelId}" on the active GPU server, restart your terminal backend with:\n\n${cmd}\n\nUpdate the UI status anyway?`, confirmLabel: "Update UI" })) {
|
||||
@@ -275,12 +319,26 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
device: "",
|
||||
});
|
||||
try {
|
||||
const response = await startServer(modelId, constraints);
|
||||
const modelInfo = getLocalModelInfo(modelId);
|
||||
const loadConstraints = modelInfo?.backendType === "openvino-npu"
|
||||
? {
|
||||
...constraints,
|
||||
backendType: "openvino-npu",
|
||||
useGpu: true,
|
||||
width: constraints.width >= 1024 ? 1024 : 512,
|
||||
height: constraints.height >= 1024 ? 1024 : 512,
|
||||
steps: Math.max(1, Math.min(8, constraints.steps || 4)),
|
||||
cfgScale: constraints.cfgScale || 1,
|
||||
}
|
||||
: constraints;
|
||||
const response = await startServer(modelId, loadConstraints);
|
||||
console.log(response);
|
||||
|
||||
let isReady = false;
|
||||
let crashError = null;
|
||||
for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms)
|
||||
const isOpenVinoModel = modelInfo?.backendType === "openvino-npu";
|
||||
const maxStartupPolls = isOpenVinoModel ? 1200 : 240;
|
||||
for (let i = 0; i < maxStartupPolls; i++) {
|
||||
const status = await getBackendStatus();
|
||||
if (status.loading) {
|
||||
setModelLoadProgress({
|
||||
@@ -303,7 +361,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
crashError = status.error;
|
||||
break;
|
||||
}
|
||||
if (!status.running && i > 3) {
|
||||
if (!status.running && !status.loading?.active && i > 3) {
|
||||
crashError = "The backend process terminated immediately on startup.";
|
||||
break;
|
||||
}
|
||||
@@ -574,7 +632,12 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
Local Models ({localModels.length})
|
||||
</h3>
|
||||
|
||||
{localModels.length === 0 ? (
|
||||
{isLoadingModels ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", gap: "10px", padding: "24px 0", color: "var(--md-sys-color-outline)" }}>
|
||||
<RefreshCw className="progress-spinner" size={16} />
|
||||
<span style={{ fontSize: "0.9rem" }}>Scanning models folder...</span>
|
||||
</div>
|
||||
) : localModels.length === 0 ? (
|
||||
<p style={{ fontSize: "0.9rem", color: "var(--md-sys-color-outline)", textAlign: "center", padding: "16px 0" }}>
|
||||
No models detected in app/models/. Download from the library below or import a file.
|
||||
</p>
|
||||
@@ -601,7 +664,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
|
||||
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>{filename}</span>
|
||||
<span style={{ fontSize: "0.75rem", opacity: 0.8 }}>
|
||||
Local Weights File • {model.size || formatBytes(model.sizeBytes)}
|
||||
{model.backendType === "openvino-npu" ? "OpenVINO NPU Model" : model.format || "Local Weights File"} • {model.size || formatBytes(model.sizeBytes)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -654,7 +717,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
{MODEL_LIBRARY.map((section) => (
|
||||
{visibleModelLibrary.map((section) => (
|
||||
<div key={section.group} className="m3-card" style={{ margin: 0 }}>
|
||||
<h4 style={{ fontWeight: 700, marginBottom: "12px" }}>{section.group}</h4>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "12px" }}>
|
||||
|
||||
@@ -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,
|
||||
flash_attn: constraints.useFlashAttn !== false,
|
||||
@@ -331,14 +337,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.
|
||||
@@ -346,8 +372,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,
|
||||
@@ -362,6 +386,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;
|
||||
@@ -417,9 +480,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,
|
||||
};
|
||||
@@ -655,6 +726,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" });
|
||||
|
||||
Reference in new issue
Block a user