add ModelManager component and infrastructure to support local model management, downloading, and server integration
This commit is contained in:
1 parent
6595213696
commit
992a642b40
10 files changed
+207
-56
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import React, { memo, useState, useRef, useCallback } from "react";
|
||||
import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Layers, Trash2 } from "lucide-react";
|
||||
import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2 } from "lucide-react";
|
||||
import { generateImage, startServer, stopServer, waitForServerReady, getBackendStatus, getGenerationProgress, saveGeneratedOutput, deleteGeneratedOutputs } from "../services/api";
|
||||
|
||||
const GalleryItem = memo(({ img, idx, isSelected, onClick }) => {
|
||||
@@ -600,10 +600,6 @@ function Generator({
|
||||
<RefreshCw size={14} />
|
||||
<span>Steps: {constraints.steps}</span>
|
||||
</div>
|
||||
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
|
||||
<Layers size={14} />
|
||||
<span>CFG: {constraints.cfgScale.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="status-chip" onClick={() => setActiveTab("constraints")}>
|
||||
<Sparkles size={14} />
|
||||
<span>Sampler: {constraints.sampler}</span>
|
||||
|
||||
@@ -209,26 +209,6 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* CFG Scale Slider */}
|
||||
<div className="m3-slider-group" style={{ marginTop: "8px" }}>
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Prompt Strictness (Instruction obedience)</span>
|
||||
<span className="m3-slider-value">{constraints.cfgScale.toFixed(1)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
className="m3-slider"
|
||||
value={constraints.cfgScale}
|
||||
onChange={(e) => updateConstraint("cfgScale", parseFloat(e.target.value))}
|
||||
min="1"
|
||||
max="15"
|
||||
step="0.5"
|
||||
/>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", lineHeight: "1.3" }}>
|
||||
How closely the AI follows your text prompt. Lower = more creative. Higher = more strict.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Random Seed DNA */}
|
||||
<div className="m3-text-field" style={{ marginTop: "8px" }}>
|
||||
<label className="m3-text-field-label">Image Blueprint (DNA Seed)</label>
|
||||
|
||||
@@ -250,10 +250,12 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
const performLoadModel = async (modelId) => {
|
||||
const isTauriDesktop = typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined;
|
||||
const isLocalServerMode = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
||||
const backendStatus = await getBackendStatus();
|
||||
const backendPort = backendStatus?.port || 8080;
|
||||
|
||||
if (!isTauriDesktop && !isLocalServerMode) {
|
||||
const isAlreadyRunning = await pingServer();
|
||||
const cmd = `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port 8080 --model .\\models\\${modelId} --threads 8`;
|
||||
const cmd = `.\\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" })) {
|
||||
@@ -261,7 +263,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
setServerRunning(true);
|
||||
}
|
||||
} else {
|
||||
if (await showConfirm({ title: "Backend Not Running", message: `C++ backend server is not running on port 8080.\n\nTo run locally, start:\n\n${cmd}\n\nProceed in Simulation Mode instead?`, confirmLabel: "Proceed" })) {
|
||||
if (await showConfirm({ title: "Backend Not Running", message: `C++ backend server is not running on port ${backendPort}.\n\nTo run locally, start:\n\n${cmd}\n\nProceed in Simulation Mode instead?`, confirmLabel: "Proceed" })) {
|
||||
setActiveModel(modelId);
|
||||
setServerRunning(false);
|
||||
}
|
||||
@@ -330,7 +332,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
|
||||
setActiveModel(modelId);
|
||||
setServerRunning(true);
|
||||
} else {
|
||||
throw new Error(crashError || "Model server failed to respond on port 8080.");
|
||||
throw new Error(crashError || `Model server failed to respond on port ${backendPort}.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load model:", e);
|
||||
|
||||
@@ -8,12 +8,32 @@ export const isTauri = () => {
|
||||
|
||||
// Cached hardware specs
|
||||
let cachedSpecs = null;
|
||||
let cachedBackendPort = null;
|
||||
export const EXPECTED_SERVER_BUILD = "polish-setup-v1";
|
||||
|
||||
const isLocalServerMode = () => {
|
||||
return typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
||||
};
|
||||
|
||||
async function getBackendPort() {
|
||||
if (isLocalServerMode()) {
|
||||
try {
|
||||
const status = await getBackendStatus();
|
||||
const port = Number(status?.port);
|
||||
if (Number.isInteger(port) && port > 0) {
|
||||
cachedBackendPort = port;
|
||||
return port;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (cachedBackendPort) return cachedBackendPort;
|
||||
return 8080;
|
||||
}
|
||||
|
||||
async function getBackendBaseUrl() {
|
||||
return `http://127.0.0.1:${await getBackendPort()}`;
|
||||
}
|
||||
|
||||
export function formatBytes(bytes) {
|
||||
const value = Number(bytes) || 0;
|
||||
if (value <= 0) return "0 B";
|
||||
@@ -208,10 +228,11 @@ export async function listLocalModels() {
|
||||
// In web/portable mode this calls serve.cjs management API which restarts the
|
||||
// sd-vulkan.exe process with --steps, --cfg-scale, --sampling-method flags.
|
||||
export async function startServer(modelPath, constraints) {
|
||||
const backendPort = await getBackendPort();
|
||||
if (isTauri()) {
|
||||
const launchParams = {
|
||||
model_path: modelPath,
|
||||
port: 8080,
|
||||
port: backendPort,
|
||||
use_gpu: constraints.useGpu !== false,
|
||||
backend_type: constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"),
|
||||
threads: constraints.threads || 8,
|
||||
@@ -242,6 +263,9 @@ export async function startServer(modelPath, constraints) {
|
||||
if (!res.ok || data.ok === false) {
|
||||
throw new Error(data.error || `Backend restart failed (HTTP ${res.status})`);
|
||||
}
|
||||
if (Number.isInteger(Number(data.port)) && Number(data.port) > 0) {
|
||||
cachedBackendPort = Number(data.port);
|
||||
}
|
||||
console.log("Backend restart:", data.message);
|
||||
return data.message;
|
||||
} catch (e) {
|
||||
@@ -320,8 +344,7 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
|
||||
console.log("Initiating image generation:", { prompt, negativePrompt, constraints, activeModelName });
|
||||
const startTime = Date.now();
|
||||
|
||||
const port = 8080;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const baseUrl = await getBackendBaseUrl();
|
||||
|
||||
// Prepare payload based on standard stable-diffusion.cpp REST endpoint schemas
|
||||
const payload = {
|
||||
@@ -738,8 +761,7 @@ export async function deleteModel(filename) {
|
||||
|
||||
// Ping the server to check if it is active and responding
|
||||
export async function pingServer() {
|
||||
const port = 8080;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const baseUrl = await getBackendBaseUrl();
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/models`, { method: "GET" });
|
||||
return response.ok;
|
||||
|
||||
Reference in new issue
Block a user