notoken-core 1.5.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +5023 -65
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +315 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Image Generation.
|
|
3
|
+
*
|
|
4
|
+
* Detects, installs, and interfaces with local image generation engines.
|
|
5
|
+
* Falls back to informing users about online services.
|
|
6
|
+
*
|
|
7
|
+
* Supported local engines:
|
|
8
|
+
* 1. AUTOMATIC1111 (Stable Diffusion Web UI) — API at :7860
|
|
9
|
+
* 2. ComfyUI — API at :8188
|
|
10
|
+
* 3. Fooocus — simplest UI
|
|
11
|
+
* 4. Docker (stable-diffusion-webui container)
|
|
12
|
+
*
|
|
13
|
+
* Online services (info only):
|
|
14
|
+
* - OpenAI DALL-E API
|
|
15
|
+
* - Midjourney (Discord-based)
|
|
16
|
+
* - Leonardo.ai
|
|
17
|
+
* - Stability AI API
|
|
18
|
+
*/
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
import { homedir, platform } from "node:os";
|
|
23
|
+
import { USER_HOME } from "./paths.js";
|
|
24
|
+
import { trackInstall } from "./installTracker.js";
|
|
25
|
+
const c = {
|
|
26
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
27
|
+
green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m",
|
|
28
|
+
cyan: "\x1b[36m", magenta: "\x1b[35m", blue: "\x1b[34m",
|
|
29
|
+
};
|
|
30
|
+
export function getDriveInfo(path) {
|
|
31
|
+
const isWSL = (() => { try {
|
|
32
|
+
return !!execSync("grep -qi microsoft /proc/version && echo wsl", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 2000 }).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
} })();
|
|
37
|
+
// Walk up to find an existing parent directory for df
|
|
38
|
+
let checkPath = path;
|
|
39
|
+
for (let i = 0; i < 5; i++) {
|
|
40
|
+
try {
|
|
41
|
+
const output = execSync(`df -BG "${checkPath}" 2>/dev/null | tail -1`, { encoding: "utf-8", timeout: 3000 });
|
|
42
|
+
const parts = output.trim().split(/\s+/);
|
|
43
|
+
if (parts.length < 6) {
|
|
44
|
+
checkPath = resolve(checkPath, "..");
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const total = parseInt(parts[1]) || 0;
|
|
48
|
+
let free = parseInt(parts[3]) || 0;
|
|
49
|
+
const pct = parseInt(parts[4]) || 0;
|
|
50
|
+
const mount = parts[parts.length - 1];
|
|
51
|
+
// WSL fix: paths on the WSL virtual disk (/dev/sdd, /dev/sdc) report
|
|
52
|
+
// the VHD max size, not actual free space on the host drive.
|
|
53
|
+
// Real free space = C: drive free space (where the VHD lives)
|
|
54
|
+
if (isWSL && !mount.startsWith("/mnt/") && mount !== "/mnt/wsl") {
|
|
55
|
+
try {
|
|
56
|
+
const cDrive = execSync('df -BG /mnt/c 2>/dev/null | tail -1', { encoding: "utf-8", timeout: 3000 });
|
|
57
|
+
const cParts = cDrive.trim().split(/\s+/);
|
|
58
|
+
if (cParts.length >= 6) {
|
|
59
|
+
const cFree = parseInt(cParts[3]) || 0;
|
|
60
|
+
free = cFree; // Use C: drive free space as the real limit
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
return { path, freeGB: free, totalGB: total, usedPct: pct, mount };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
checkPath = resolve(checkPath, "..");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function getDriveFreeGB(path) {
|
|
74
|
+
return getDriveInfo(path)?.freeGB ?? 0;
|
|
75
|
+
}
|
|
76
|
+
function chooseBestInstallDir() {
|
|
77
|
+
const os = platform();
|
|
78
|
+
const isWSL = (() => { try {
|
|
79
|
+
return !!execSync("grep -qi microsoft /proc/version && echo wsl", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 2000 }).trim();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
} })();
|
|
84
|
+
const candidates = [];
|
|
85
|
+
const MIN_GB = 15;
|
|
86
|
+
if (isWSL) {
|
|
87
|
+
// Check mounted Windows drives (skip C:)
|
|
88
|
+
for (const drive of ["/mnt/d", "/mnt/e", "/mnt/f", "/mnt/g", "/mnt/h", "/mnt/i"]) {
|
|
89
|
+
const free = getDriveFreeGB(drive);
|
|
90
|
+
if (free > 0) {
|
|
91
|
+
candidates.push({ path: resolve(drive, "notoken", "ai"), freeGB: free, rejected: free < MIN_GB ? `only ${free}GB free` : undefined });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Also check C: but mark it
|
|
95
|
+
const cFree = getDriveFreeGB("/mnt/c");
|
|
96
|
+
if (cFree > 0)
|
|
97
|
+
candidates.push({ path: "/mnt/c/notoken/ai", freeGB: cFree, rejected: cFree < MIN_GB ? `only ${cFree}GB free (system drive)` : "system drive — avoid" });
|
|
98
|
+
// Linux root
|
|
99
|
+
const rootFree = getDriveFreeGB("/");
|
|
100
|
+
candidates.push({ path: resolve(homedir(), "notoken", "ai"), freeGB: rootFree, rejected: rootFree < MIN_GB ? `only ${rootFree}GB free` : undefined });
|
|
101
|
+
}
|
|
102
|
+
else if (os === "win32") {
|
|
103
|
+
for (const letter of ["D", "E", "F", "G", "H", "I"]) {
|
|
104
|
+
const drive = `${letter}:\\`;
|
|
105
|
+
const free = getDriveFreeGB(drive);
|
|
106
|
+
if (free > 0)
|
|
107
|
+
candidates.push({ path: resolve(drive, "notoken", "ai"), freeGB: free, rejected: free < MIN_GB ? `only ${free}GB free` : undefined });
|
|
108
|
+
}
|
|
109
|
+
const cFree = getDriveFreeGB("C:\\");
|
|
110
|
+
if (cFree > 0)
|
|
111
|
+
candidates.push({ path: resolve("C:\\notoken\\ai"), freeGB: cFree, rejected: "system drive — avoid" });
|
|
112
|
+
candidates.push({ path: resolve(homedir(), "notoken", "ai"), freeGB: cFree, rejected: cFree < MIN_GB ? `only ${cFree}GB free` : undefined });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Linux/macOS
|
|
116
|
+
const homeFree = getDriveFreeGB(homedir());
|
|
117
|
+
candidates.push({ path: resolve(homedir(), "notoken", "ai"), freeGB: homeFree, rejected: homeFree < MIN_GB ? `only ${homeFree}GB free` : undefined });
|
|
118
|
+
// Check /opt if available
|
|
119
|
+
const optFree = getDriveFreeGB("/opt");
|
|
120
|
+
if (optFree > 0)
|
|
121
|
+
candidates.push({ path: "/opt/notoken/ai", freeGB: optFree, rejected: optFree < MIN_GB ? `only ${optFree}GB free` : undefined });
|
|
122
|
+
}
|
|
123
|
+
// Pick best: most free space that's not rejected
|
|
124
|
+
const viable = candidates.filter(c => !c.rejected).sort((a, b) => b.freeGB - a.freeGB);
|
|
125
|
+
if (viable.length > 0) {
|
|
126
|
+
const best = viable[0];
|
|
127
|
+
const rejected = candidates.filter(c => c.rejected);
|
|
128
|
+
let reasoning = `Chose ${best.path} (${best.freeGB}GB free)`;
|
|
129
|
+
if (rejected.length > 0) {
|
|
130
|
+
reasoning += `. Skipped: ${rejected.map(r => `${r.path} (${r.rejected})`).join(", ")}`;
|
|
131
|
+
}
|
|
132
|
+
return { dir: best.path, freeGB: best.freeGB, reasoning, candidates };
|
|
133
|
+
}
|
|
134
|
+
// No viable option — pick least bad
|
|
135
|
+
const sorted = candidates.sort((a, b) => b.freeGB - a.freeGB);
|
|
136
|
+
const best = sorted[0] ?? { path: homedir(), freeGB: 0 };
|
|
137
|
+
return {
|
|
138
|
+
dir: best.path,
|
|
139
|
+
freeGB: best.freeGB,
|
|
140
|
+
reasoning: `No drive with ${MIN_GB}GB+ free. Best available: ${best.path} (${best.freeGB}GB free)`,
|
|
141
|
+
candidates,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Resolve a user-specified path like "D drive", "F:", "/mnt/f", "/opt/mydir" */
|
|
145
|
+
export function resolveUserPath(input) {
|
|
146
|
+
const normalized = input.trim().toLowerCase();
|
|
147
|
+
// "D drive", "d:", "D:\\"
|
|
148
|
+
const driveMatch = normalized.match(/^([a-z])\s*(?:drive|:|\s|$)/i);
|
|
149
|
+
if (driveMatch) {
|
|
150
|
+
const letter = driveMatch[1].toUpperCase();
|
|
151
|
+
const os = platform();
|
|
152
|
+
const isWSL = (() => { try {
|
|
153
|
+
return !!execSync("grep -qi microsoft /proc/version && echo wsl", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 2000 }).trim();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
} })();
|
|
158
|
+
if (isWSL)
|
|
159
|
+
return `/mnt/${letter.toLowerCase()}/notoken/ai`;
|
|
160
|
+
if (os === "win32")
|
|
161
|
+
return `${letter}:\\notoken\\ai`;
|
|
162
|
+
}
|
|
163
|
+
// Absolute path
|
|
164
|
+
if (input.startsWith("/") || input.match(/^[A-Z]:\\/))
|
|
165
|
+
return input;
|
|
166
|
+
// "/mnt/d", "/mnt/f/mydir"
|
|
167
|
+
if (normalized.startsWith("/mnt/"))
|
|
168
|
+
return input;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function getInstallBase() {
|
|
172
|
+
return process.env.NOTOKEN_INSTALL_DIR ?? chooseBestInstallDir().dir;
|
|
173
|
+
}
|
|
174
|
+
function getSDDir() { return resolve(getInstallBase(), "stable-diffusion-webui"); }
|
|
175
|
+
function getComfyDir() { return resolve(getInstallBase(), "ComfyUI"); }
|
|
176
|
+
function getFooocusDir() { return resolve(getInstallBase(), "Fooocus"); }
|
|
177
|
+
// Scan all possible install locations — includes all mounted drives
|
|
178
|
+
function getAllKnownDirs(engineName) {
|
|
179
|
+
const dirs = [];
|
|
180
|
+
// Current install base
|
|
181
|
+
dirs.push(resolve(getInstallBase(), engineName));
|
|
182
|
+
// Home directory
|
|
183
|
+
dirs.push(resolve(homedir(), engineName));
|
|
184
|
+
dirs.push(resolve(homedir(), "notoken", "ai", engineName));
|
|
185
|
+
// All mounted Windows drives (WSL)
|
|
186
|
+
for (const letter of ["c", "d", "e", "f", "g", "h", "i"]) {
|
|
187
|
+
dirs.push(resolve(`/mnt/${letter}`, "notoken", "ai", engineName));
|
|
188
|
+
dirs.push(resolve(`/mnt/${letter}`, "apps", engineName));
|
|
189
|
+
}
|
|
190
|
+
// Windows paths
|
|
191
|
+
for (const letter of ["C", "D", "E", "F", "G"]) {
|
|
192
|
+
dirs.push(resolve(`${letter}:\\notoken\\ai`, engineName));
|
|
193
|
+
}
|
|
194
|
+
// Linux common
|
|
195
|
+
dirs.push(resolve("/opt/notoken/ai", engineName));
|
|
196
|
+
return dirs;
|
|
197
|
+
}
|
|
198
|
+
const STABILITY_MATRIX_DIR = resolve(homedir(), "StabilityMatrix");
|
|
199
|
+
const EASY_DIFFUSION_DIR = resolve(homedir(), "easy-diffusion");
|
|
200
|
+
const OUTPUT_DIR = resolve(USER_HOME, "generated-images");
|
|
201
|
+
const USAGE_FILE = resolve(USER_HOME, "image-gen-usage.json");
|
|
202
|
+
function loadUsage() {
|
|
203
|
+
try {
|
|
204
|
+
if (existsSync(USAGE_FILE))
|
|
205
|
+
return JSON.parse(readFileSync(USAGE_FILE, "utf-8"));
|
|
206
|
+
}
|
|
207
|
+
catch { }
|
|
208
|
+
return { cloudGenerations: 0, localGenerations: 0, totalGenerations: 0, lastGenerated: "" };
|
|
209
|
+
}
|
|
210
|
+
function saveUsage(stats) {
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(USER_HOME, { recursive: true });
|
|
213
|
+
writeFileSync(USAGE_FILE, JSON.stringify(stats, null, 2));
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}
|
|
217
|
+
function recordGeneration(isLocal) {
|
|
218
|
+
const stats = loadUsage();
|
|
219
|
+
if (isLocal)
|
|
220
|
+
stats.localGenerations++;
|
|
221
|
+
else
|
|
222
|
+
stats.cloudGenerations++;
|
|
223
|
+
stats.totalGenerations++;
|
|
224
|
+
stats.lastGenerated = new Date().toISOString();
|
|
225
|
+
// Check for API keys
|
|
226
|
+
if (process.env.HF_TOKEN || process.env.HUGGINGFACE_TOKEN) {
|
|
227
|
+
stats.apiKey = { provider: "huggingface", configured: true };
|
|
228
|
+
}
|
|
229
|
+
else if (process.env.STABILITY_API_KEY) {
|
|
230
|
+
stats.apiKey = { provider: "stability-ai", configured: true };
|
|
231
|
+
}
|
|
232
|
+
saveUsage(stats);
|
|
233
|
+
return stats;
|
|
234
|
+
}
|
|
235
|
+
// ─── Detection ─────────────────────────────────────────────────────────────
|
|
236
|
+
function tryExec(cmd, timeout = 5000) {
|
|
237
|
+
try {
|
|
238
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout }).trim() || null;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function detectGpu() {
|
|
245
|
+
// Find nvidia-smi — check PATH first, then known locations
|
|
246
|
+
const nvidiaSmiPaths = [
|
|
247
|
+
"nvidia-smi", // in PATH
|
|
248
|
+
"/usr/lib/wsl/lib/nvidia-smi", // WSL2
|
|
249
|
+
"/usr/lib/wsl/drivers/*/nvidia-smi", // WSL2 driver dir
|
|
250
|
+
"/mnt/c/Windows/System32/nvidia-smi.exe", // Windows side
|
|
251
|
+
"nvidia-smi.exe", // Windows in PATH
|
|
252
|
+
"/usr/bin/nvidia-smi", // Linux standard
|
|
253
|
+
"/usr/local/bin/nvidia-smi", // Linux local
|
|
254
|
+
];
|
|
255
|
+
let nvidiaSmi = null;
|
|
256
|
+
for (const smiPath of nvidiaSmiPaths) {
|
|
257
|
+
if (smiPath.includes("*")) {
|
|
258
|
+
// Glob — try to find it
|
|
259
|
+
const found = tryExec(`ls ${smiPath} 2>/dev/null | head -1`);
|
|
260
|
+
if (found) {
|
|
261
|
+
nvidiaSmi = found;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const test = tryExec(`${smiPath} --query-gpu=name --format=csv,noheader 2>/dev/null`);
|
|
267
|
+
if (test) {
|
|
268
|
+
nvidiaSmi = smiPath;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// If found but not in PATH, add it
|
|
274
|
+
if (nvidiaSmi && nvidiaSmi !== "nvidia-smi" && !tryExec("nvidia-smi --version 2>/dev/null")) {
|
|
275
|
+
const smiDir = nvidiaSmi.replace(/\/nvidia-smi.*$/, "");
|
|
276
|
+
if (smiDir && !process.env.PATH?.includes(smiDir)) {
|
|
277
|
+
process.env.PATH = `${smiDir}:${process.env.PATH}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Query GPU info
|
|
281
|
+
let gpuName;
|
|
282
|
+
let vram;
|
|
283
|
+
let vramFree;
|
|
284
|
+
let gpuTemp;
|
|
285
|
+
let gpuUtil;
|
|
286
|
+
let driverVersion;
|
|
287
|
+
let gpuError;
|
|
288
|
+
if (nvidiaSmi) {
|
|
289
|
+
try {
|
|
290
|
+
const info = tryExec(`${nvidiaSmi} --query-gpu=name,memory.total,memory.free,temperature.gpu,utilization.gpu,driver_version --format=csv,noheader,nounits 2>/dev/null`);
|
|
291
|
+
if (info) {
|
|
292
|
+
const parts = info.split(",").map(s => s.trim());
|
|
293
|
+
gpuName = parts[0];
|
|
294
|
+
vram = parts[1] ? `${parts[1]} MB` : undefined;
|
|
295
|
+
vramFree = parts[2] ? `${parts[2]} MB` : undefined;
|
|
296
|
+
gpuTemp = parts[3] ? `${parts[3]}°C` : undefined;
|
|
297
|
+
gpuUtil = parts[4] ? `${parts[4]}%` : undefined;
|
|
298
|
+
driverVersion = parts[5];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
gpuError = `nvidia-smi found but failed: ${err instanceof Error ? err.message : err}`;
|
|
303
|
+
}
|
|
304
|
+
// Check for GPU errors from nvidia-smi
|
|
305
|
+
const errCheck = tryExec(`${nvidiaSmi} --query-gpu=gpu_bus_id,ecc.errors.corrected.aggregate.total --format=csv,noheader 2>/dev/null`);
|
|
306
|
+
if (errCheck?.includes("ERR") || errCheck?.includes("Unknown Error")) {
|
|
307
|
+
gpuError = "GPU reporting ECC errors — may be unstable";
|
|
308
|
+
}
|
|
309
|
+
// Check kernel log for GPU crashes (WSL dxg errors, Xid errors)
|
|
310
|
+
const dmesgErrors = tryExec("dmesg 2>/dev/null | grep -ci 'dxgkio_reserve_gpu_va\\|xid.*error\\|nvrm.*error\\|gpu.*fault' 2>/dev/null");
|
|
311
|
+
const crashCount = parseInt(dmesgErrors ?? "0") || 0;
|
|
312
|
+
if (crashCount > 0) {
|
|
313
|
+
// Get when the last error happened
|
|
314
|
+
const lastError = tryExec("dmesg 2>/dev/null | grep -i 'dxgkio_reserve_gpu_va\\|xid.*error\\|nvrm.*error\\|gpu.*fault' | tail -1 | awk '{print $1}' | tr -d '[]'");
|
|
315
|
+
const uptime = tryExec("cat /proc/uptime 2>/dev/null | awk '{print $1}'");
|
|
316
|
+
let agoStr = "";
|
|
317
|
+
if (lastError && uptime) {
|
|
318
|
+
const errorSec = parseFloat(lastError);
|
|
319
|
+
const uptimeSec = parseFloat(uptime);
|
|
320
|
+
const agoSec = uptimeSec - errorSec;
|
|
321
|
+
if (agoSec < 60)
|
|
322
|
+
agoStr = `${Math.round(agoSec)}s ago`;
|
|
323
|
+
else if (agoSec < 3600)
|
|
324
|
+
agoStr = `${Math.round(agoSec / 60)} min ago`;
|
|
325
|
+
else
|
|
326
|
+
agoStr = `${(agoSec / 3600).toFixed(1)} hours ago`;
|
|
327
|
+
}
|
|
328
|
+
gpuError = (gpuError ? gpuError + ". " : "") +
|
|
329
|
+
`${crashCount} GPU passthrough error(s) in WSL kernel log${agoStr ? ` (last: ${agoStr})` : ""}. GPU compute may crash — CPU mode recommended.`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Check for WSL CUDA libs
|
|
333
|
+
const wslCuda = existsSync("/usr/lib/wsl/lib/libcuda.so");
|
|
334
|
+
// CUDA toolkit version
|
|
335
|
+
const cuda = tryExec("nvcc --version 2>/dev/null");
|
|
336
|
+
const cudaMatch = cuda?.match(/release ([\d.]+)/);
|
|
337
|
+
// Also check nvidia-smi CUDA version
|
|
338
|
+
const smiCuda = nvidiaSmi ? tryExec(`${nvidiaSmi} --query-gpu=driver_version --format=csv,noheader 2>/dev/null`) : null;
|
|
339
|
+
const amd = tryExec("rocm-smi --showproductname 2>/dev/null");
|
|
340
|
+
return {
|
|
341
|
+
hasNvidia: !!nvidiaSmi,
|
|
342
|
+
hasAmd: !!amd,
|
|
343
|
+
gpuName,
|
|
344
|
+
vram,
|
|
345
|
+
vramFree,
|
|
346
|
+
gpuTemp,
|
|
347
|
+
gpuUtil,
|
|
348
|
+
driverVersion,
|
|
349
|
+
gpuError,
|
|
350
|
+
wslCuda,
|
|
351
|
+
cudaVersion: cudaMatch?.[1],
|
|
352
|
+
maxCudaVersion: driverVersion ? getMaxCudaForDriver(parseFloat(driverVersion)) : undefined,
|
|
353
|
+
recommendedTorch: driverVersion ? getRecommendedTorch(parseFloat(driverVersion)) : undefined,
|
|
354
|
+
cpuOnly: !nvidiaSmi && !amd,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function getMaxCudaForDriver(driverVer) {
|
|
358
|
+
// NVIDIA driver → max CUDA version mapping
|
|
359
|
+
if (driverVer >= 570)
|
|
360
|
+
return "13.0";
|
|
361
|
+
if (driverVer >= 560)
|
|
362
|
+
return "12.6";
|
|
363
|
+
if (driverVer >= 550)
|
|
364
|
+
return "12.4";
|
|
365
|
+
if (driverVer >= 545)
|
|
366
|
+
return "12.3";
|
|
367
|
+
if (driverVer >= 535)
|
|
368
|
+
return "12.2";
|
|
369
|
+
if (driverVer >= 525)
|
|
370
|
+
return "12.0";
|
|
371
|
+
if (driverVer >= 520)
|
|
372
|
+
return "11.8";
|
|
373
|
+
if (driverVer >= 510)
|
|
374
|
+
return "11.6";
|
|
375
|
+
return "11.4";
|
|
376
|
+
}
|
|
377
|
+
function getRecommendedTorch(driverVer) {
|
|
378
|
+
// Recommend the right PyTorch CUDA version for this driver
|
|
379
|
+
if (driverVer >= 570)
|
|
380
|
+
return "cu130";
|
|
381
|
+
if (driverVer >= 550)
|
|
382
|
+
return "cu124";
|
|
383
|
+
if (driverVer >= 535)
|
|
384
|
+
return "cu121";
|
|
385
|
+
if (driverVer >= 520)
|
|
386
|
+
return "cu118";
|
|
387
|
+
return "cpu";
|
|
388
|
+
}
|
|
389
|
+
export function detectImageEngines() {
|
|
390
|
+
const engines = [];
|
|
391
|
+
const isWSLEnv = (() => { try {
|
|
392
|
+
return !!execSync("grep -qi microsoft /proc/version && echo wsl", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 2000 }).trim();
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return false;
|
|
396
|
+
} })();
|
|
397
|
+
const os = platform();
|
|
398
|
+
// Check which ports are active
|
|
399
|
+
const port7860Up = !!tryExec("curl -sf --max-time 2 http://localhost:7860/sdapi/v1/sd-models 2>/dev/null");
|
|
400
|
+
const port8188Up = !!tryExec("curl -sf --max-time 2 http://localhost:8188/system_stats 2>/dev/null");
|
|
401
|
+
const port9000Up = !!tryExec("curl -sf --max-time 2 http://localhost:9000/ping 2>/dev/null");
|
|
402
|
+
// Detect what process owns port 7860
|
|
403
|
+
const port7860Pid = tryExec("ss -tlnp 2>/dev/null | grep ':7860' | grep -oP 'pid=\\K[0-9]+'") ?? tryExec("lsof -ti:7860 2>/dev/null");
|
|
404
|
+
const port7860Process = port7860Pid ? tryExec(`ps -p ${port7860Pid} -o comm= 2>/dev/null`) : null;
|
|
405
|
+
const port7860IsWSL = port7860Pid ? !tryExec(`/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -Command "Get-Process -Id ${port7860Pid}" 2>/dev/null`) : true;
|
|
406
|
+
// AUTOMATIC1111 / Forge (WSL installs)
|
|
407
|
+
const SD_DIR = getAllKnownDirs("stable-diffusion-webui").find(d => existsSync(d) && existsSync(resolve(d, "webui.py"))) ?? getSDDir();
|
|
408
|
+
const a1Installed = existsSync(SD_DIR) && existsSync(resolve(SD_DIR, "webui.py"));
|
|
409
|
+
// Also check Forge
|
|
410
|
+
const FORGE_DIR = getAllKnownDirs("sd-forge").find(d => existsSync(d) && existsSync(resolve(d, "launch.py"))) ?? resolve(getInstallBase(), "sd-forge");
|
|
411
|
+
const forgeInstalled = existsSync(FORGE_DIR) && existsSync(resolve(FORGE_DIR, "launch.py"));
|
|
412
|
+
const sdDir = forgeInstalled ? FORGE_DIR : (a1Installed ? SD_DIR : undefined);
|
|
413
|
+
const sdPlatform = sdDir?.startsWith("/mnt/") ? "wsl" : (os === "win32" ? "windows" : "linux");
|
|
414
|
+
engines.push({
|
|
415
|
+
engine: "auto1111",
|
|
416
|
+
installed: !!(sdDir),
|
|
417
|
+
running: port7860Up,
|
|
418
|
+
path: sdDir,
|
|
419
|
+
url: port7860Up ? "http://localhost:7860" : undefined,
|
|
420
|
+
platform: sdPlatform,
|
|
421
|
+
pid: port7860Pid ? parseInt(port7860Pid) : undefined,
|
|
422
|
+
port: 7860,
|
|
423
|
+
});
|
|
424
|
+
// ComfyUI
|
|
425
|
+
const COMFY_DIR = getAllKnownDirs("ComfyUI").find(d => existsSync(d) && existsSync(resolve(d, "main.py"))) ?? getComfyDir();
|
|
426
|
+
const comfyInstalled = existsSync(COMFY_DIR) && existsSync(resolve(COMFY_DIR, "main.py"));
|
|
427
|
+
engines.push({
|
|
428
|
+
engine: "comfyui",
|
|
429
|
+
installed: comfyInstalled,
|
|
430
|
+
running: port8188Up,
|
|
431
|
+
path: comfyInstalled ? COMFY_DIR : undefined,
|
|
432
|
+
url: port8188Up ? "http://localhost:8188" : undefined,
|
|
433
|
+
port: 8188,
|
|
434
|
+
});
|
|
435
|
+
// Fooocus
|
|
436
|
+
const FOOOCUS_DIR = getAllKnownDirs("Fooocus").find(d => existsSync(d) && existsSync(resolve(d, "entry_with_update.py"))) ?? getFooocusDir();
|
|
437
|
+
const fooocusInstalled = existsSync(FOOOCUS_DIR) && existsSync(resolve(FOOOCUS_DIR, "entry_with_update.py"));
|
|
438
|
+
engines.push({
|
|
439
|
+
engine: "fooocus",
|
|
440
|
+
installed: fooocusInstalled,
|
|
441
|
+
running: false,
|
|
442
|
+
path: fooocusInstalled ? FOOOCUS_DIR : undefined,
|
|
443
|
+
});
|
|
444
|
+
// Stability Matrix — check both WSL-accessible and Windows paths
|
|
445
|
+
const smDirs = [
|
|
446
|
+
STABILITY_MATRIX_DIR,
|
|
447
|
+
resolve(homedir(), "AppData", "Local", "StabilityMatrix"),
|
|
448
|
+
resolve(homedir(), ".local", "share", "StabilityMatrix"),
|
|
449
|
+
...getAllKnownDirs("StabilityMatrix"),
|
|
450
|
+
];
|
|
451
|
+
const smDir = smDirs.find(d => existsSync(d));
|
|
452
|
+
const smPlatform = smDir?.startsWith("/mnt/") ? "windows" : (os === "win32" ? "windows" : "linux");
|
|
453
|
+
engines.push({
|
|
454
|
+
engine: "stability-matrix",
|
|
455
|
+
installed: !!smDir,
|
|
456
|
+
running: port7860Up || port8188Up, // SM launches standard engines
|
|
457
|
+
path: smDir,
|
|
458
|
+
url: port7860Up ? "http://localhost:7860" : port8188Up ? "http://localhost:8188" : undefined,
|
|
459
|
+
platform: smPlatform,
|
|
460
|
+
});
|
|
461
|
+
// Easy Diffusion
|
|
462
|
+
const edDir = [
|
|
463
|
+
EASY_DIFFUSION_DIR,
|
|
464
|
+
resolve(homedir(), "EasyDiffusion"),
|
|
465
|
+
resolve(homedir(), "easy_diffusion"),
|
|
466
|
+
].find(d => existsSync(d));
|
|
467
|
+
engines.push({
|
|
468
|
+
engine: "easy-diffusion",
|
|
469
|
+
installed: !!edDir,
|
|
470
|
+
running: port9000Up,
|
|
471
|
+
path: edDir,
|
|
472
|
+
url: port9000Up ? "http://localhost:9000" : undefined,
|
|
473
|
+
port: 9000,
|
|
474
|
+
});
|
|
475
|
+
// Detect port conflicts — multiple engines trying to use same port
|
|
476
|
+
const runningOn7860 = engines.filter(e => e.running && e.port === 7860);
|
|
477
|
+
if (runningOn7860.length > 1) {
|
|
478
|
+
for (const e of runningOn7860)
|
|
479
|
+
e.portConflict = true;
|
|
480
|
+
}
|
|
481
|
+
// Docker
|
|
482
|
+
const dockerSd = tryExec("docker ps --format '{{.Image}}' 2>/dev/null | grep -i 'stable-diffusion\\|automatic1111\\|comfyui'");
|
|
483
|
+
engines.push({
|
|
484
|
+
engine: "docker",
|
|
485
|
+
installed: !!tryExec("docker --version"),
|
|
486
|
+
running: !!dockerSd,
|
|
487
|
+
url: dockerSd ? "http://localhost:7860" : undefined,
|
|
488
|
+
});
|
|
489
|
+
return engines;
|
|
490
|
+
}
|
|
491
|
+
export function getBestImageEngine() {
|
|
492
|
+
const engines = detectImageEngines();
|
|
493
|
+
// Prefer running engine first
|
|
494
|
+
const running = engines.find(e => e.running);
|
|
495
|
+
if (running)
|
|
496
|
+
return running;
|
|
497
|
+
// Then an SD engine that's actually installed (not just Docker being available)
|
|
498
|
+
const installed = engines.find(e => e.installed && e.engine !== "docker");
|
|
499
|
+
if (installed)
|
|
500
|
+
return installed;
|
|
501
|
+
// Docker only if it has the SD image already pulled
|
|
502
|
+
const docker = engines.find(e => e.engine === "docker" && e.running);
|
|
503
|
+
if (docker)
|
|
504
|
+
return docker;
|
|
505
|
+
// Don't return Docker just because Docker daemon exists — that's not an SD install
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
// ─── Generation ────────────────────────────────────────────────────────────
|
|
509
|
+
export async function generateImage(prompt) {
|
|
510
|
+
// First: check if any API is already running (could be Windows SM, WSL Forge, anything)
|
|
511
|
+
const apiOnPort7860 = !!tryExec("curl -sf --max-time 2 http://localhost:7860/sdapi/v1/sd-models 2>/dev/null");
|
|
512
|
+
const apiOnPort8188 = !!tryExec("curl -sf --max-time 2 http://localhost:8188/system_stats 2>/dev/null");
|
|
513
|
+
if (apiOnPort7860) {
|
|
514
|
+
console.error(`${c.cyan}Step 1/${c.reset} Using running engine at http://localhost:7860`);
|
|
515
|
+
console.error(`${c.cyan}Step 2/${c.reset} Sending prompt to local engine...`);
|
|
516
|
+
return generateViaAuto1111(prompt, "http://localhost:7860");
|
|
517
|
+
}
|
|
518
|
+
if (apiOnPort8188) {
|
|
519
|
+
console.error(`${c.cyan}Step 1/${c.reset} Using running engine at http://localhost:8188`);
|
|
520
|
+
console.error(`${c.cyan}Step 2/${c.reset} Sending prompt to local engine...`);
|
|
521
|
+
return generateViaAuto1111(prompt, "http://localhost:8188");
|
|
522
|
+
}
|
|
523
|
+
const engine = getBestImageEngine();
|
|
524
|
+
if (!engine || (!engine.running && !engine.installed)) {
|
|
525
|
+
// No local engine — try cloud API (zero-install, free)
|
|
526
|
+
console.error(`${c.cyan}Step 1/${c.reset} Checking for local image generators...`);
|
|
527
|
+
console.error(`${c.dim} No local engine found (AUTOMATIC1111, ComfyUI, Easy Diffusion, etc.)${c.reset}`);
|
|
528
|
+
console.error(`${c.cyan}Step 2/${c.reset} Using cloud API — free, no setup required`);
|
|
529
|
+
console.error(`${c.dim} Sending prompt to Pollinations.ai (Stable Diffusion)...${c.reset}`);
|
|
530
|
+
const cloudResult = await generateViaCloud(prompt);
|
|
531
|
+
if (cloudResult.success) {
|
|
532
|
+
console.error(`${c.cyan}Step 3/${c.reset} ${c.green}Image received — saving to disk${c.reset}`);
|
|
533
|
+
return cloudResult;
|
|
534
|
+
}
|
|
535
|
+
console.error(`${c.yellow}Step 3/${c.reset} Cloud API unavailable — showing alternatives`);
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
prompt,
|
|
539
|
+
message: (cloudResult.error ? `${c.yellow}Cloud API:${c.reset} ${cloudResult.error}\n\n` : "") + formatNoEngineMessage(prompt),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// If installed but not running — auto-start it, wait, then generate
|
|
543
|
+
if (engine.installed && !engine.running) {
|
|
544
|
+
console.error(`${c.cyan}Step 1/${c.reset} Found ${c.bold}${engine.engine}${c.reset} installed at ${engine.path}`);
|
|
545
|
+
console.error(`${c.cyan}Step 2/${c.reset} Engine is not running — starting it automatically...`);
|
|
546
|
+
const started = await autoStartEngine(engine);
|
|
547
|
+
if (!started) {
|
|
548
|
+
console.error(`${c.yellow}Step 3/${c.reset} Could not auto-start — trying cloud API as fallback`);
|
|
549
|
+
const cloudFallback = await generateViaCloud(prompt);
|
|
550
|
+
if (cloudFallback.success)
|
|
551
|
+
return cloudFallback;
|
|
552
|
+
return {
|
|
553
|
+
success: false,
|
|
554
|
+
engine: engine.engine,
|
|
555
|
+
prompt,
|
|
556
|
+
message: formatStartMessage(engine),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
console.error(`${c.cyan}Step 3/${c.reset} ${c.green}Engine started — generating image...${c.reset}`);
|
|
560
|
+
// Re-detect to get the URL
|
|
561
|
+
const refreshed = detectImageEngines().find(e => e.engine === engine.engine);
|
|
562
|
+
if (refreshed?.running) {
|
|
563
|
+
engine.running = true;
|
|
564
|
+
engine.url = refreshed.url;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.error(`${c.cyan}Step 1/${c.reset} Using ${c.bold}${engine.engine}${c.reset} at ${engine.url}`);
|
|
569
|
+
console.error(`${c.cyan}Step 2/${c.reset} Sending prompt to local engine...`);
|
|
570
|
+
}
|
|
571
|
+
// Engine is running — generate via API
|
|
572
|
+
let genResult = null;
|
|
573
|
+
if (engine.engine === "auto1111" || (engine.engine === "docker" && engine.url?.includes("7860"))) {
|
|
574
|
+
genResult = await generateViaAuto1111(prompt, engine.url ?? "http://localhost:7860");
|
|
575
|
+
}
|
|
576
|
+
else if (engine.engine === "comfyui") {
|
|
577
|
+
genResult = await generateViaAuto1111(prompt, engine.url ?? "http://localhost:8188");
|
|
578
|
+
}
|
|
579
|
+
else if (engine.engine === "easy-diffusion") {
|
|
580
|
+
genResult = await generateViaEasyDiffusion(prompt, engine.url ?? "http://localhost:9000");
|
|
581
|
+
}
|
|
582
|
+
else if (engine.engine === "stability-matrix") {
|
|
583
|
+
if (engine.url?.includes("7860"))
|
|
584
|
+
genResult = await generateViaAuto1111(prompt, engine.url);
|
|
585
|
+
else if (engine.url?.includes("8188"))
|
|
586
|
+
genResult = await generateViaAuto1111(prompt, engine.url);
|
|
587
|
+
}
|
|
588
|
+
if (genResult) {
|
|
589
|
+
if (genResult.success) {
|
|
590
|
+
console.error(`${c.cyan}Step 3/${c.reset} ${c.green}Image saved successfully${c.reset}`);
|
|
591
|
+
}
|
|
592
|
+
return genResult;
|
|
593
|
+
}
|
|
594
|
+
return { success: false, prompt, message: formatNoEngineMessage(prompt) };
|
|
595
|
+
}
|
|
596
|
+
// ─── Auto-Start ────────────────────────────────────────────────────────────
|
|
597
|
+
async function autoStartEngine(engine) {
|
|
598
|
+
try {
|
|
599
|
+
if (engine.engine === "auto1111" && engine.path) {
|
|
600
|
+
// Check if model exists — if not, startup will take much longer
|
|
601
|
+
const modelsDir = resolve(engine.path, "models", "Stable-diffusion");
|
|
602
|
+
let hasModel = false;
|
|
603
|
+
try {
|
|
604
|
+
const files = readdirSync(modelsDir);
|
|
605
|
+
hasModel = files.some(f => f.endsWith(".safetensors") || f.endsWith(".ckpt"));
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
// Health check: fix corrupted packages from interrupted installs
|
|
609
|
+
try {
|
|
610
|
+
const spDir = resolve(engine.path, "venv", "lib");
|
|
611
|
+
const pyDirs = readdirSync(spDir).filter(d => d.startsWith("python"));
|
|
612
|
+
for (const pyDir of pyDirs) {
|
|
613
|
+
const pkgDir = resolve(spDir, pyDir, "site-packages");
|
|
614
|
+
try {
|
|
615
|
+
const corrupted = readdirSync(pkgDir).filter(e => e.startsWith("~"));
|
|
616
|
+
if (corrupted.length > 0) {
|
|
617
|
+
console.error(`${c.yellow}Fixing ${corrupted.length} corrupted package(s)...${c.reset}`);
|
|
618
|
+
const { rmSync } = await import("node:fs");
|
|
619
|
+
for (const dir of corrupted) {
|
|
620
|
+
try {
|
|
621
|
+
rmSync(resolve(pkgDir, dir), { recursive: true });
|
|
622
|
+
}
|
|
623
|
+
catch { }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch { }
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch { }
|
|
631
|
+
const timeout = hasModel ? 180 : 600; // 3 min with model, 10 min without
|
|
632
|
+
console.error(`${c.dim}Starting Stable Diffusion...${hasModel ? "" : " (first launch — downloading model, this takes several minutes)"}${c.reset}`);
|
|
633
|
+
const { spawn } = await import("node:child_process");
|
|
634
|
+
const venvPython = resolve(engine.path, "venv", "bin", "python");
|
|
635
|
+
const winVenvPython = resolve(engine.path, "venv", "Scripts", "python.exe");
|
|
636
|
+
const pythonCmd = existsSync(venvPython) ? venvPython : existsSync(winVenvPython) ? winVenvPython : "python3";
|
|
637
|
+
// Force CPU mode — disable CUDA completely to avoid DXG GPU errors on WSL
|
|
638
|
+
const cpuEnv = {
|
|
639
|
+
...process.env,
|
|
640
|
+
CUDA_VISIBLE_DEVICES: "", // hide all GPUs
|
|
641
|
+
TORCH_CUDA_ARCH_LIST: "", // no CUDA architectures
|
|
642
|
+
COMMANDLINE_ARGS: "--api --listen --skip-torch-cuda-test --use-cpu all --no-half",
|
|
643
|
+
};
|
|
644
|
+
const child = spawn(pythonCmd, ["launch.py", "--api", "--listen", "--skip-torch-cuda-test", "--use-cpu", "all", "--no-half"], {
|
|
645
|
+
cwd: engine.path,
|
|
646
|
+
detached: true,
|
|
647
|
+
stdio: "ignore",
|
|
648
|
+
env: cpuEnv,
|
|
649
|
+
});
|
|
650
|
+
child.unref();
|
|
651
|
+
return waitForReady("http://localhost:7860/sdapi/v1/sd-models", timeout);
|
|
652
|
+
}
|
|
653
|
+
if (engine.engine === "comfyui" && engine.path) {
|
|
654
|
+
console.error(`${c.dim}Starting ComfyUI...${c.reset}`);
|
|
655
|
+
const { spawn } = await import("node:child_process");
|
|
656
|
+
const child = spawn("python3", ["main.py", "--listen"], {
|
|
657
|
+
cwd: engine.path,
|
|
658
|
+
detached: true,
|
|
659
|
+
stdio: "ignore",
|
|
660
|
+
});
|
|
661
|
+
child.unref();
|
|
662
|
+
return waitForReady("http://localhost:8188/system_stats", 90);
|
|
663
|
+
}
|
|
664
|
+
if (engine.engine === "fooocus" && engine.path) {
|
|
665
|
+
console.error(`${c.dim}Starting Fooocus...${c.reset}`);
|
|
666
|
+
const { spawn } = await import("node:child_process");
|
|
667
|
+
const child = spawn("python3", ["entry_with_update.py"], {
|
|
668
|
+
cwd: engine.path,
|
|
669
|
+
detached: true,
|
|
670
|
+
stdio: "ignore",
|
|
671
|
+
});
|
|
672
|
+
child.unref();
|
|
673
|
+
// Fooocus doesn't have a reliable API — just wait a bit
|
|
674
|
+
await sleep(15000);
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
if (engine.engine === "docker") {
|
|
678
|
+
console.error(`${c.dim}Starting Docker SD container...${c.reset}`);
|
|
679
|
+
const gpu = detectGpu();
|
|
680
|
+
const gpuFlag = gpu.hasNvidia ? "--gpus all" : "";
|
|
681
|
+
const envFlag = gpu.cpuOnly ? "-e COMMANDLINE_ARGS='--use-cpu all --skip-torch-cuda-test --no-half'" : "";
|
|
682
|
+
tryExec(`docker start sd-webui 2>/dev/null`) ??
|
|
683
|
+
tryExec(`docker run -d ${gpuFlag} -p 7860:7860 --name sd-webui ${envFlag} ghcr.io/ai-dock/stable-diffusion-webui:latest`);
|
|
684
|
+
return waitForReady("http://localhost:7860/sdapi/v1/sd-models", 120);
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function waitForReady(url, timeoutSeconds) {
|
|
693
|
+
const start = Date.now();
|
|
694
|
+
const deadline = start + timeoutSeconds * 1000;
|
|
695
|
+
let dots = 0;
|
|
696
|
+
while (Date.now() < deadline) {
|
|
697
|
+
const check = tryExec(`curl -sf --max-time 2 "${url}" 2>/dev/null`, 3000);
|
|
698
|
+
if (check) {
|
|
699
|
+
console.error(`${c.green}✓${c.reset} Ready! (${((Date.now() - start) / 1000).toFixed(0)}s)`);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
dots++;
|
|
703
|
+
if (dots % 5 === 0) {
|
|
704
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
705
|
+
console.error(`${c.dim} Waiting for engine to start... (${elapsed}s)${c.reset}`);
|
|
706
|
+
}
|
|
707
|
+
await sleep(3000);
|
|
708
|
+
}
|
|
709
|
+
console.error(`${c.yellow}⚠${c.reset} Engine not ready after ${timeoutSeconds}s — still loading in the background.`);
|
|
710
|
+
console.error(`${c.dim} First launch downloads the AI model (~4GB) — this can take 5-10 minutes.${c.reset}`);
|
|
711
|
+
console.error(`${c.dim} The engine is still starting in the background.${c.reset}`);
|
|
712
|
+
console.error(`${c.dim} Say "check image status" to see if it's ready, or we'll use cloud for now.${c.reset}`);
|
|
713
|
+
// Store pending action for "is it ready yet"
|
|
714
|
+
const { suggestAction } = await import("../conversation/pendingActions.js");
|
|
715
|
+
suggestAction({
|
|
716
|
+
action: "generate a picture of a cat",
|
|
717
|
+
description: "Try generating locally — engine may be ready now",
|
|
718
|
+
type: "intent",
|
|
719
|
+
});
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
function sleep(ms) {
|
|
723
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
724
|
+
}
|
|
725
|
+
/** Check if a path is a Windows drive accessed through WSL */
|
|
726
|
+
function isWslWindowsPath(path) {
|
|
727
|
+
return path.startsWith("/mnt/") && /^\/mnt\/[a-z]\//.test(path);
|
|
728
|
+
}
|
|
729
|
+
/** Convert WSL path to Windows path */
|
|
730
|
+
function toWindowsPath(wslPath) {
|
|
731
|
+
try {
|
|
732
|
+
return execSync(`wslpath -w "${wslPath}" 2>/dev/null`, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Manual conversion: /mnt/d/foo → D:\foo
|
|
736
|
+
const match = wslPath.match(/^\/mnt\/([a-z])\/(.*)$/);
|
|
737
|
+
if (match)
|
|
738
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
739
|
+
return wslPath;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Run a command on the Windows side via PowerShell (faster for Windows drives).
|
|
744
|
+
* Falls back to WSL-native execution if PowerShell not available.
|
|
745
|
+
*/
|
|
746
|
+
async function runOnWindowsSide(cmd, cwd) {
|
|
747
|
+
const winCwd = toWindowsPath(cwd);
|
|
748
|
+
const psCmd = `/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
749
|
+
try {
|
|
750
|
+
const { spawn: spawnAsync } = await import("node:child_process");
|
|
751
|
+
return new Promise((resolve, reject) => {
|
|
752
|
+
const child = spawnAsync(psCmd, ["-Command", `cd '${winCwd}'; ${cmd}`], {
|
|
753
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
754
|
+
});
|
|
755
|
+
child.stdout?.on("data", (d) => {
|
|
756
|
+
const lines = d.toString().split("\n").filter((l) => l.trim());
|
|
757
|
+
for (const line of lines) {
|
|
758
|
+
process.stderr.write(` ${c.dim}${line.trim()}${c.reset}\n`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
child.stderr?.on("data", (d) => {
|
|
762
|
+
const lines = d.toString().split("\n").filter((l) => l.trim());
|
|
763
|
+
for (const line of lines) {
|
|
764
|
+
process.stderr.write(` ${c.dim}${line.trim()}${c.reset}\n`);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`PowerShell exit ${code}`)));
|
|
768
|
+
child.on("error", reject);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
throw new Error(`Windows-side execution failed: ${err instanceof Error ? err.message : err}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Run a command with streaming output. For short commands, runs inline.
|
|
777
|
+
* For long commands (pip install torch), runs as detached background process
|
|
778
|
+
* with log file monitoring — survives parent process timeouts.
|
|
779
|
+
*/
|
|
780
|
+
async function runWithProgress(cmd, args, cwd) {
|
|
781
|
+
const { spawn: spawnAsync } = await import("node:child_process");
|
|
782
|
+
const isLongRunning = args.some(a => a.includes("torch") || a.includes("requirements"));
|
|
783
|
+
const logFile = resolve(USER_HOME, ".install-progress.log");
|
|
784
|
+
if (isLongRunning) {
|
|
785
|
+
// Long-running: spawn detached with log file, then poll for completion
|
|
786
|
+
console.error(`${c.dim} Running in background (logging to ${logFile})...${c.reset}`);
|
|
787
|
+
// Write a shell script that runs the command and writes a status file
|
|
788
|
+
const statusFile = resolve(USER_HOME, ".install-status");
|
|
789
|
+
const script = `#!/bin/bash
|
|
790
|
+
${cmd} ${args.map(a => `'${a}'`).join(" ")} > "${logFile}" 2>&1
|
|
791
|
+
echo $? > "${statusFile}"
|
|
792
|
+
`;
|
|
793
|
+
const scriptFile = resolve(USER_HOME, ".install-run.sh");
|
|
794
|
+
writeFileSync(scriptFile, script, { mode: 0o755 });
|
|
795
|
+
// Remove old status file
|
|
796
|
+
try {
|
|
797
|
+
(await import("node:fs")).unlinkSync(statusFile);
|
|
798
|
+
}
|
|
799
|
+
catch { }
|
|
800
|
+
// Spawn detached — survives parent timeout
|
|
801
|
+
const child = spawnAsync("bash", [scriptFile], {
|
|
802
|
+
cwd,
|
|
803
|
+
detached: true,
|
|
804
|
+
stdio: "ignore",
|
|
805
|
+
});
|
|
806
|
+
child.unref();
|
|
807
|
+
// Poll for completion by watching the status file
|
|
808
|
+
const startTime = Date.now();
|
|
809
|
+
const maxWait = 30 * 60 * 1000; // 30 minutes max
|
|
810
|
+
while (Date.now() - startTime < maxWait) {
|
|
811
|
+
await sleep(5000);
|
|
812
|
+
// Show latest log lines
|
|
813
|
+
try {
|
|
814
|
+
const log = readFileSync(logFile, "utf-8");
|
|
815
|
+
const lines = log.split("\n").filter(l => l.trim());
|
|
816
|
+
const recent = lines.slice(-3);
|
|
817
|
+
for (const line of recent) {
|
|
818
|
+
if (line.includes("Downloading") || line.includes("Installing") ||
|
|
819
|
+
line.includes("Collecting") || line.includes("Successfully") ||
|
|
820
|
+
line.includes("━") || line.includes("%") || line.includes("error")) {
|
|
821
|
+
process.stderr.write(` ${c.dim}${line.trim()}${c.reset}\n`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch { }
|
|
826
|
+
// Check if done
|
|
827
|
+
try {
|
|
828
|
+
if (existsSync(statusFile)) {
|
|
829
|
+
const exitCode = parseInt(readFileSync(statusFile, "utf-8").trim());
|
|
830
|
+
if (exitCode === 0) {
|
|
831
|
+
console.error(`${c.green} ✓ Complete${c.reset}`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
const log = readFileSync(logFile, "utf-8");
|
|
836
|
+
const lastLines = log.split("\n").filter(l => l.trim()).slice(-5).join("\n");
|
|
837
|
+
throw new Error(`Command failed (exit ${exitCode}): ${lastLines}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
if (err instanceof Error && err.message.startsWith("Command failed"))
|
|
843
|
+
throw err;
|
|
844
|
+
}
|
|
845
|
+
// Show elapsed time every 30s
|
|
846
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
847
|
+
if (elapsed % 30 === 0) {
|
|
848
|
+
console.error(`${c.dim} Still working... (${elapsed}s elapsed)${c.reset}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
throw new Error("Install timed out after 30 minutes");
|
|
852
|
+
}
|
|
853
|
+
// Short-running: inline with streaming output
|
|
854
|
+
return new Promise((resolve, reject) => {
|
|
855
|
+
const child = spawnAsync(cmd, args, {
|
|
856
|
+
cwd,
|
|
857
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
858
|
+
shell: false,
|
|
859
|
+
});
|
|
860
|
+
let lastLine = "";
|
|
861
|
+
const handleData = (data) => {
|
|
862
|
+
const lines = data.toString().split("\n").filter(l => l.trim());
|
|
863
|
+
for (const line of lines) {
|
|
864
|
+
lastLine = line;
|
|
865
|
+
if (line.includes("Downloading") || line.includes("Installing") ||
|
|
866
|
+
line.includes("Collecting") || line.includes("Successfully") ||
|
|
867
|
+
line.includes("━") || line.includes("error") || line.includes("ERROR") ||
|
|
868
|
+
line.includes("%") || line.includes("curl")) {
|
|
869
|
+
process.stderr.write(` ${c.dim}${line.trim()}${c.reset}\n`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
child.stdout?.on("data", handleData);
|
|
874
|
+
child.stderr?.on("data", handleData);
|
|
875
|
+
child.on("close", (code) => {
|
|
876
|
+
if (code === 0)
|
|
877
|
+
resolve();
|
|
878
|
+
else
|
|
879
|
+
reject(new Error(`Command failed (exit ${code}): ${lastLine}`));
|
|
880
|
+
});
|
|
881
|
+
child.on("error", (err) => reject(err));
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
// ─── Cloud API (free, no install, no auth) ─────────────────────────────────
|
|
885
|
+
async function generateViaCloud(prompt) {
|
|
886
|
+
try {
|
|
887
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
888
|
+
const timestamp = Date.now();
|
|
889
|
+
const safeName = prompt.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
890
|
+
const imagePath = resolve(OUTPUT_DIR, `${safeName}_${timestamp}.png`);
|
|
891
|
+
// Pollinations.ai — free, no auth, Stable Diffusion
|
|
892
|
+
const encodedPrompt = encodeURIComponent(prompt);
|
|
893
|
+
const url = `https://image.pollinations.ai/prompt/${encodedPrompt}`;
|
|
894
|
+
console.error(`${c.dim} Prompt: "${prompt}"${c.reset}`);
|
|
895
|
+
console.error(`${c.dim} Waiting for image (10-30 seconds)...${c.reset}`);
|
|
896
|
+
const { execSync: exec } = await import("node:child_process");
|
|
897
|
+
// Retry up to 3 times — Pollinations sometimes returns 502 on first try
|
|
898
|
+
let success = false;
|
|
899
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
900
|
+
try {
|
|
901
|
+
exec(`curl -sL --max-time 120 "${url}" -o "${imagePath}"`, { timeout: 130000 });
|
|
902
|
+
const s = (await import("node:fs")).statSync(imagePath);
|
|
903
|
+
if (s.size > 1000) {
|
|
904
|
+
success = true;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
catch { }
|
|
909
|
+
if (attempt < 2)
|
|
910
|
+
console.error(`${c.dim} Server busy — retrying (attempt ${attempt + 2}/3)...${c.reset}`);
|
|
911
|
+
}
|
|
912
|
+
if (!success)
|
|
913
|
+
return { success: false, prompt, error: "Image generation timed out after 3 attempts. The cloud service may be busy — try again in a moment." };
|
|
914
|
+
if (!existsSync(imagePath)) {
|
|
915
|
+
return { success: false, prompt, error: "Image generation failed — no file returned" };
|
|
916
|
+
}
|
|
917
|
+
const { statSync: stat } = await import("node:fs");
|
|
918
|
+
const size = stat(imagePath).size;
|
|
919
|
+
if (size < 1000) {
|
|
920
|
+
// Too small — probably an error response
|
|
921
|
+
return { success: false, prompt, error: "Image generation returned an empty or error response" };
|
|
922
|
+
}
|
|
923
|
+
const stats = recordGeneration(false);
|
|
924
|
+
const usageLine = ` ${c.dim}Cloud images generated: ${stats.cloudGenerations} | Total: ${stats.totalGenerations}${c.reset}`;
|
|
925
|
+
const apiLine = stats.apiKey?.configured
|
|
926
|
+
? ` ${c.green}✓ API key configured (${stats.apiKey.provider})${c.reset}`
|
|
927
|
+
: ` ${c.dim}Tip: For faster generation, set an API key:${c.reset}\n ${c.dim} HuggingFace (free): https://huggingface.co/settings/tokens → export HF_TOKEN=hf_...${c.reset}\n ${c.dim} Stability AI: https://platform.stability.ai/account/keys → export STABILITY_API_KEY=sk-...${c.reset}`;
|
|
928
|
+
const localLine = ` ${c.dim}To create images offline for free: notoken install stability-matrix${c.reset}`;
|
|
929
|
+
return {
|
|
930
|
+
success: true,
|
|
931
|
+
engine: "auto1111",
|
|
932
|
+
prompt,
|
|
933
|
+
imagePath,
|
|
934
|
+
message: `${c.green}✓${c.reset} Image generated!\n ${c.bold}Prompt:${c.reset} ${prompt}\n ${c.bold}Saved:${c.reset} ${imagePath}\n ${c.bold}Size:${c.reset} ${(size / 1024).toFixed(0)} KB\n\n${usageLine}\n${apiLine}\n${localLine}`,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
return { success: false, prompt, error: `Cloud generation failed: ${err instanceof Error ? err.message : err}` };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// ─── Easy Diffusion API ────────────────────────────────────────────────────
|
|
942
|
+
async function generateViaEasyDiffusion(prompt, baseUrl) {
|
|
943
|
+
try {
|
|
944
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
945
|
+
const timestamp = Date.now();
|
|
946
|
+
const safeName = prompt.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
947
|
+
const imagePath = resolve(OUTPUT_DIR, `${safeName}_${timestamp}.png`);
|
|
948
|
+
const payload = JSON.stringify({
|
|
949
|
+
prompt,
|
|
950
|
+
negative_prompt: "blurry, bad quality, distorted",
|
|
951
|
+
width: 512, height: 512,
|
|
952
|
+
num_inference_steps: 20,
|
|
953
|
+
guidance_scale: 7,
|
|
954
|
+
});
|
|
955
|
+
const result = tryExec(`curl -sf --max-time 120 -X POST "${baseUrl}/image" -H "Content-Type: application/json" -d '${payload.replace(/'/g, "'\\''")}'`, 130000);
|
|
956
|
+
if (!result)
|
|
957
|
+
return { success: false, prompt, error: "Easy Diffusion generation timed out" };
|
|
958
|
+
const data = JSON.parse(result);
|
|
959
|
+
if (!data.output?.[0]?.data)
|
|
960
|
+
return { success: false, prompt, error: "No image returned" };
|
|
961
|
+
const imgData = data.output[0].data.replace(/^data:image\/\w+;base64,/, "");
|
|
962
|
+
const buffer = Buffer.from(imgData, "base64");
|
|
963
|
+
writeFileSync(imagePath, buffer);
|
|
964
|
+
return {
|
|
965
|
+
success: true, engine: "easy-diffusion", prompt, imagePath,
|
|
966
|
+
message: `${c.green}✓${c.reset} Image generated (Easy Diffusion)!\n ${c.bold}Prompt:${c.reset} ${prompt}\n ${c.bold}Saved:${c.reset} ${imagePath}\n ${c.bold}Size:${c.reset} ${(buffer.length / 1024).toFixed(0)} KB`,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
return { success: false, prompt, error: `Easy Diffusion failed: ${err instanceof Error ? err.message : err}` };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// ─── API Generation ────────────────────────────────────────────────────────
|
|
974
|
+
async function generateViaAuto1111(prompt, baseUrl) {
|
|
975
|
+
try {
|
|
976
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
977
|
+
// Detect GPU/CPU mode
|
|
978
|
+
const cmdFlags = tryExec(`curl -sf --max-time 3 "${baseUrl}/sdapi/v1/cmd-flags" 2>/dev/null`);
|
|
979
|
+
const isGpuMode = cmdFlags ? !cmdFlags.includes('"skip_torch_cuda_test":true') : false;
|
|
980
|
+
const gpu = detectGpu();
|
|
981
|
+
const modeStr = isGpuMode && gpu.hasNvidia ? `GPU (${gpu.gpuName})` : "CPU";
|
|
982
|
+
console.error(`${c.dim} Mode: ${modeStr}${c.reset}`);
|
|
983
|
+
console.error(`${c.dim} Prompt: "${prompt}"${c.reset}`);
|
|
984
|
+
console.error(`${c.dim} Generating (512x512, 20 steps)...${c.reset}`);
|
|
985
|
+
const payload = JSON.stringify({
|
|
986
|
+
prompt,
|
|
987
|
+
negative_prompt: "blurry, bad quality, distorted, watermark, text",
|
|
988
|
+
steps: 20,
|
|
989
|
+
cfg_scale: 7,
|
|
990
|
+
width: 512,
|
|
991
|
+
height: 512,
|
|
992
|
+
sampler_name: "Euler a",
|
|
993
|
+
});
|
|
994
|
+
// Start generation in background via async fetch
|
|
995
|
+
const { spawn: spawnProc } = await import("node:child_process");
|
|
996
|
+
const tmpResult = resolve(OUTPUT_DIR, `.gen-result-${Date.now()}.json`);
|
|
997
|
+
const curlProc = spawnProc("curl", [
|
|
998
|
+
"-sf", "--max-time", "300",
|
|
999
|
+
"-X", "POST", `${baseUrl}/sdapi/v1/txt2img`,
|
|
1000
|
+
"-H", "Content-Type: application/json",
|
|
1001
|
+
"-d", payload,
|
|
1002
|
+
"-o", tmpResult,
|
|
1003
|
+
], { stdio: "ignore", detached: false });
|
|
1004
|
+
// Poll progress while generating
|
|
1005
|
+
const startTime = Date.now();
|
|
1006
|
+
let lastStep = -1;
|
|
1007
|
+
while (!existsSync(tmpResult) || getFileSize(tmpResult) === 0) {
|
|
1008
|
+
await sleep(2000);
|
|
1009
|
+
// Check progress API
|
|
1010
|
+
const prog = tryExec(`curl -sf --max-time 2 "${baseUrl}/sdapi/v1/progress" 2>/dev/null`);
|
|
1011
|
+
if (prog) {
|
|
1012
|
+
try {
|
|
1013
|
+
const p = JSON.parse(prog);
|
|
1014
|
+
const pct = Math.round((p.progress ?? 0) * 100);
|
|
1015
|
+
const step = p.state?.sampling_step ?? 0;
|
|
1016
|
+
const steps = p.state?.sampling_steps ?? 20;
|
|
1017
|
+
if (step !== lastStep && step > 0) {
|
|
1018
|
+
const bar = "█".repeat(Math.round(pct / 5)) + "░".repeat(20 - Math.round(pct / 5));
|
|
1019
|
+
console.error(` ${c.cyan}${bar}${c.reset} ${pct}% (step ${step}/${steps})`);
|
|
1020
|
+
lastStep = step;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch { }
|
|
1024
|
+
}
|
|
1025
|
+
// Timeout check
|
|
1026
|
+
if (Date.now() - startTime > 300000) {
|
|
1027
|
+
try {
|
|
1028
|
+
curlProc.kill();
|
|
1029
|
+
}
|
|
1030
|
+
catch { }
|
|
1031
|
+
return { success: false, prompt, error: "Generation timed out after 5 minutes" };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Wait a bit for file to finish writing
|
|
1035
|
+
await sleep(500);
|
|
1036
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1037
|
+
const resultData = readFileSync(tmpResult, "utf-8");
|
|
1038
|
+
try {
|
|
1039
|
+
removeFile(tmpResult);
|
|
1040
|
+
}
|
|
1041
|
+
catch { }
|
|
1042
|
+
if (!resultData || resultData.length < 100) {
|
|
1043
|
+
return { success: false, prompt, error: "Generation returned empty response" };
|
|
1044
|
+
}
|
|
1045
|
+
const data = JSON.parse(resultData);
|
|
1046
|
+
if (!data.images?.[0]) {
|
|
1047
|
+
return { success: false, prompt, error: "No image returned from API" };
|
|
1048
|
+
}
|
|
1049
|
+
// Save base64 image
|
|
1050
|
+
const timestamp = Date.now();
|
|
1051
|
+
const safeName = prompt.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
1052
|
+
const imagePath = resolve(OUTPUT_DIR, `${safeName}_${timestamp}.png`);
|
|
1053
|
+
const buffer = Buffer.from(data.images[0], "base64");
|
|
1054
|
+
writeFileSync(imagePath, buffer);
|
|
1055
|
+
const stats = recordGeneration(true);
|
|
1056
|
+
return {
|
|
1057
|
+
success: true,
|
|
1058
|
+
engine: "auto1111",
|
|
1059
|
+
prompt,
|
|
1060
|
+
imagePath,
|
|
1061
|
+
message: [
|
|
1062
|
+
`${c.green}✓${c.reset} Image generated locally in ${elapsed}s (${modeStr})`,
|
|
1063
|
+
` ${c.bold}Prompt:${c.reset} ${prompt}`,
|
|
1064
|
+
` ${c.bold}Saved:${c.reset} ${imagePath}`,
|
|
1065
|
+
` ${c.bold}Size:${c.reset} ${(buffer.length / 1024).toFixed(0)} KB`,
|
|
1066
|
+
` ${c.dim}Local: ${stats.localGenerations} | Cloud: ${stats.cloudGenerations} | Total: ${stats.totalGenerations}${c.reset}`,
|
|
1067
|
+
].join("\n"),
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
catch (err) {
|
|
1071
|
+
return { success: false, prompt, error: `Generation failed: ${err instanceof Error ? err.message : err}` };
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function getFileSize(filePath) {
|
|
1075
|
+
try {
|
|
1076
|
+
return statSync(filePath).size;
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
return 0;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function removeFile(filePath) {
|
|
1083
|
+
try {
|
|
1084
|
+
writeFileSync(filePath, "");
|
|
1085
|
+
}
|
|
1086
|
+
catch { }
|
|
1087
|
+
}
|
|
1088
|
+
export function getInstallPlan(engine) {
|
|
1089
|
+
const gpu = detectGpu();
|
|
1090
|
+
const torchExtra = gpu.hasNvidia ? "+cu121" : gpu.hasAmd ? "+rocm5.7" : "+cpu";
|
|
1091
|
+
const plans = {
|
|
1092
|
+
auto1111: {
|
|
1093
|
+
engine: "AUTOMATIC1111 (Stable Diffusion Web UI)",
|
|
1094
|
+
requirements: ["Python 3.10+", "git", gpu.hasNvidia ? `NVIDIA GPU (${gpu.gpuName})` : "CPU (slow)"],
|
|
1095
|
+
estimatedTime: gpu.hasNvidia ? "10-20 minutes" : "15-30 minutes",
|
|
1096
|
+
diskSpace: "~10 GB (with model)",
|
|
1097
|
+
steps: [
|
|
1098
|
+
`git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git ${getSDDir()}`,
|
|
1099
|
+
`cd ${getSDDir()} && python3 -m venv venv && source venv/bin/activate`,
|
|
1100
|
+
`pip install torch torchvision --index-url https://download.pytorch.org/whl/${torchExtra.replace("+", "")}`,
|
|
1101
|
+
`cd ${getSDDir()} && bash webui.sh --api --listen`,
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1104
|
+
comfyui: {
|
|
1105
|
+
engine: "ComfyUI (Node-based workflow UI)",
|
|
1106
|
+
requirements: ["Python 3.10+", "git", gpu.hasNvidia ? `NVIDIA GPU (${gpu.gpuName})` : "CPU"],
|
|
1107
|
+
estimatedTime: "10-15 minutes",
|
|
1108
|
+
diskSpace: "~8 GB (with model)",
|
|
1109
|
+
steps: [
|
|
1110
|
+
`git clone https://github.com/comfyanonymous/ComfyUI.git ${getComfyDir()}`,
|
|
1111
|
+
`cd ${getComfyDir()} && python3 -m venv venv && source venv/bin/activate`,
|
|
1112
|
+
`pip install torch torchvision --index-url https://download.pytorch.org/whl/${torchExtra.replace("+", "")}`,
|
|
1113
|
+
`pip install -r requirements.txt`,
|
|
1114
|
+
`python3 main.py --listen`,
|
|
1115
|
+
],
|
|
1116
|
+
},
|
|
1117
|
+
fooocus: {
|
|
1118
|
+
engine: "Fooocus (Simplest — one-click style)",
|
|
1119
|
+
requirements: ["Python 3.10+", "git", gpu.hasNvidia ? `NVIDIA GPU (${gpu.gpuName})` : "CPU"],
|
|
1120
|
+
estimatedTime: "10-15 minutes",
|
|
1121
|
+
diskSpace: "~10 GB (with model)",
|
|
1122
|
+
steps: [
|
|
1123
|
+
`git clone https://github.com/lllyasviel/Fooocus.git ${getFooocusDir()}`,
|
|
1124
|
+
`cd ${getFooocusDir()} && python3 -m venv venv && source venv/bin/activate`,
|
|
1125
|
+
`pip install -r requirements_versions.txt`,
|
|
1126
|
+
`python3 entry_with_update.py`,
|
|
1127
|
+
],
|
|
1128
|
+
},
|
|
1129
|
+
docker: {
|
|
1130
|
+
engine: "Docker (Containerized — no dependency headaches)",
|
|
1131
|
+
requirements: ["Docker", gpu.hasNvidia ? "NVIDIA Container Toolkit" : "CPU mode"],
|
|
1132
|
+
estimatedTime: "5-10 minutes (pull only)",
|
|
1133
|
+
diskSpace: "~15 GB",
|
|
1134
|
+
steps: gpu.hasNvidia
|
|
1135
|
+
? [
|
|
1136
|
+
"docker pull ghcr.io/ai-dock/stable-diffusion-webui:latest",
|
|
1137
|
+
"docker run -d --gpus all -p 7860:7860 --name sd-webui ghcr.io/ai-dock/stable-diffusion-webui:latest",
|
|
1138
|
+
]
|
|
1139
|
+
: [
|
|
1140
|
+
"docker pull ghcr.io/ai-dock/stable-diffusion-webui:latest",
|
|
1141
|
+
"docker run -d -p 7860:7860 --name sd-webui -e COMMANDLINE_ARGS='--use-cpu all --skip-torch-cuda-test --no-half' ghcr.io/ai-dock/stable-diffusion-webui:latest",
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
return plans[engine];
|
|
1146
|
+
}
|
|
1147
|
+
export async function installImageEngine(engine) {
|
|
1148
|
+
const gpu = detectGpu();
|
|
1149
|
+
const os = (await import("node:os")).platform();
|
|
1150
|
+
const isWSL = !!tryExec("grep -qi microsoft /proc/version && echo wsl");
|
|
1151
|
+
// ── Docker path ──
|
|
1152
|
+
if (engine === "docker") {
|
|
1153
|
+
if (!tryExec("docker --version")) {
|
|
1154
|
+
console.log(`${c.cyan}Step 1/${c.reset} Docker not found — installing...`);
|
|
1155
|
+
try {
|
|
1156
|
+
execSync("curl -fsSL https://get.docker.com | sh", { stdio: "inherit", timeout: 300000 });
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
return { success: false, message: `Could not install Docker. Run: notoken install docker` };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Check disk space where Docker stores data
|
|
1163
|
+
try {
|
|
1164
|
+
const dockerRoot = tryExec("docker info 2>/dev/null | grep 'Docker Root Dir' | awk '{print $NF}'") ?? "/var/lib/docker";
|
|
1165
|
+
const dockerDrive = getDriveInfo(dockerRoot);
|
|
1166
|
+
if (dockerDrive) {
|
|
1167
|
+
console.log(`${c.cyan}Step 1b/${c.reset} Docker data: ${dockerRoot} (${dockerDrive.freeGB}GB free)`);
|
|
1168
|
+
if (dockerDrive.freeGB < 16) {
|
|
1169
|
+
console.log(`${c.yellow}⚠ Only ${dockerDrive.freeGB}GB free where Docker stores data (${dockerRoot}).${c.reset}`);
|
|
1170
|
+
console.log(`${c.yellow} The SD Docker image needs ~15GB. Not enough space.${c.reset}`);
|
|
1171
|
+
return { success: false, message: [
|
|
1172
|
+
`Not enough space for Docker image (${dockerDrive.freeGB}GB free, need ~15GB).`,
|
|
1173
|
+
`Docker stores data at: ${dockerRoot}`,
|
|
1174
|
+
``,
|
|
1175
|
+
`${c.bold}Options:${c.reset}`,
|
|
1176
|
+
` 1. Free up space on C: drive`,
|
|
1177
|
+
` 2. Use Python install instead (installs on any drive):`,
|
|
1178
|
+
` ${c.cyan}notoken install stable-diffusion on D drive${c.reset}`,
|
|
1179
|
+
` 3. Move Docker data root manually:`,
|
|
1180
|
+
` ${c.dim}echo '{"data-root":"/mnt/d/docker-data"}' | sudo tee /etc/docker/daemon.json${c.reset}`,
|
|
1181
|
+
` ${c.dim}sudo service docker restart${c.reset}`,
|
|
1182
|
+
].join("\n") };
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
catch { }
|
|
1187
|
+
try {
|
|
1188
|
+
console.log(`${c.cyan}Step 2/${c.reset} Pulling Stable Diffusion Docker image (~15GB)...`);
|
|
1189
|
+
console.log(`${c.dim} This may take 10-30 minutes depending on connection speed.${c.reset}`);
|
|
1190
|
+
execSync("docker pull ghcr.io/ai-dock/stable-diffusion-webui:latest", { stdio: "inherit", timeout: 600000 });
|
|
1191
|
+
const gpuFlag = gpu.hasNvidia ? "--gpus all" : "";
|
|
1192
|
+
const envFlag = gpu.cpuOnly ? "-e COMMANDLINE_ARGS='--use-cpu all --skip-torch-cuda-test --no-half'" : "";
|
|
1193
|
+
console.log(`${c.cyan}Step 3/${c.reset} Starting container...`);
|
|
1194
|
+
execSync(`docker run -d ${gpuFlag} -p 7860:7860 --name sd-webui ${envFlag} ghcr.io/ai-dock/stable-diffusion-webui:latest`, { stdio: "inherit", timeout: 30000 });
|
|
1195
|
+
trackInstall({
|
|
1196
|
+
name: "stable-diffusion-docker",
|
|
1197
|
+
type: "docker-image",
|
|
1198
|
+
method: "docker-pull",
|
|
1199
|
+
path: tryExec("docker info 2>/dev/null | grep 'Docker Root Dir' | awk '{print $NF}'") ?? "/var/lib/docker",
|
|
1200
|
+
uninstallCmd: "docker stop sd-webui && docker rm sd-webui && docker rmi ghcr.io/ai-dock/stable-diffusion-webui:latest",
|
|
1201
|
+
notes: "Container: sd-webui at localhost:7860",
|
|
1202
|
+
});
|
|
1203
|
+
return { success: true, message: `${c.green}✓${c.reset} Stable Diffusion running in Docker at ${c.bold}http://localhost:7860${c.reset}` };
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
return { success: false, message: `Docker install failed: ${err instanceof Error ? err.message : err}` };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
// ── Windows (no WSL) — Stability Matrix first, then Python ──
|
|
1210
|
+
if (os === "win32") {
|
|
1211
|
+
console.log(`${c.cyan}Step 0/${c.reset} Windows detected — using Stability Matrix (zero dependencies)`);
|
|
1212
|
+
const smResult = await installStabilityMatrix("win32");
|
|
1213
|
+
if (smResult.success)
|
|
1214
|
+
return smResult;
|
|
1215
|
+
// Fall back to Python install if SM failed
|
|
1216
|
+
console.log(`${c.dim}Stability Matrix failed — falling back to Python install...${c.reset}`);
|
|
1217
|
+
return installOnWindows(engine, gpu);
|
|
1218
|
+
}
|
|
1219
|
+
// ── WSL — Stability Matrix first, then Python ──
|
|
1220
|
+
if (isWSL) {
|
|
1221
|
+
console.log(`${c.cyan}Step 0/${c.reset} WSL detected`);
|
|
1222
|
+
// Check if Stability Matrix already installed on Windows side
|
|
1223
|
+
const smDir = ["/mnt/d/notoken/ai/StabilityMatrix", "/mnt/c/notoken/ai/StabilityMatrix"].find(d => existsSync(d));
|
|
1224
|
+
if (!smDir) {
|
|
1225
|
+
console.log(`${c.dim} Installing Stability Matrix on Windows side (no pip/Python headaches)...${c.reset}`);
|
|
1226
|
+
const smResult = await installStabilityMatrix("wsl");
|
|
1227
|
+
if (smResult.success)
|
|
1228
|
+
return smResult;
|
|
1229
|
+
console.log(`${c.dim} SM failed — falling back to WSL Python install...${c.reset}`);
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
console.log(`${c.green} ✓ Stability Matrix found at ${smDir}${c.reset}`);
|
|
1233
|
+
// Launch it
|
|
1234
|
+
try {
|
|
1235
|
+
const winPath = tryExec(`wslpath -w "${smDir}" 2>/dev/null`);
|
|
1236
|
+
if (winPath) {
|
|
1237
|
+
execSync(`/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -Command "Start-Process '${winPath}\\StabilityMatrix.exe'" 2>/dev/null`, { stdio: "ignore", timeout: 10000 });
|
|
1238
|
+
const gpuRec = detectGpu();
|
|
1239
|
+
const dv = parseFloat(gpuRec.driverVersion ?? "0");
|
|
1240
|
+
const pkgRec = dv > 0 && dv < 570
|
|
1241
|
+
? `\n ${c.yellow}⚠ Your GPU driver (${gpuRec.driverVersion}) does NOT support Forge Neo (needs driver 570+).${c.reset}\n ${c.bold}Choose "Reforge" — it works with your driver (CUDA ${gpuRec.maxCudaVersion}).${c.reset}\n ${c.dim}Do NOT choose "Forge Neo" — it will fail.${c.reset}`
|
|
1242
|
+
: `\n ${c.bold}Choose "Forge Neo" for best performance.${c.reset}`;
|
|
1243
|
+
return { success: true, message: `${c.green}✓${c.reset} Stability Matrix launched!${pkgRec}\n SM downloads everything automatically. Click "Launch" when ready.\n ${c.dim}Then say "generate a picture of a cat"${c.reset}` };
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
catch { }
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// ── WSL — check if better to install on Windows side ──
|
|
1250
|
+
if (isWSL) {
|
|
1251
|
+
console.log(`${c.cyan}Step 0/${c.reset} WSL detected — installing inside WSL (GPU passthrough supported)`);
|
|
1252
|
+
}
|
|
1253
|
+
// Show disk space reasoning
|
|
1254
|
+
{
|
|
1255
|
+
const choice = chooseBestInstallDir();
|
|
1256
|
+
const actualBase = process.env.NOTOKEN_INSTALL_DIR ?? choice.dir;
|
|
1257
|
+
const actualFree = getDriveFreeGB(actualBase);
|
|
1258
|
+
console.log(`${c.cyan}Disk/${c.reset} ${c.bold}Evaluating disk space...${c.reset}`);
|
|
1259
|
+
for (const cand of choice.candidates) {
|
|
1260
|
+
const icon = cand.rejected ? `${c.dim}✗${c.reset}` : `${c.green}✓${c.reset}`;
|
|
1261
|
+
const note = cand.rejected ? ` ${c.dim}— ${cand.rejected}${c.reset}` : "";
|
|
1262
|
+
console.log(` ${icon} ${cand.path}: ${cand.freeGB}GB free${note}`);
|
|
1263
|
+
}
|
|
1264
|
+
console.log(`${c.cyan} ${c.reset} ${c.dim}${choice.reasoning}${c.reset}`);
|
|
1265
|
+
console.log(`${c.cyan}Disk/${c.reset} Installing to: ${c.bold}${actualBase}${c.reset} (${actualFree}GB free)`);
|
|
1266
|
+
if (actualFree < 10) {
|
|
1267
|
+
console.log(`${c.red}⚠ WARNING: Only ${actualFree}GB free — need ~10GB for Stable Diffusion${c.reset}`);
|
|
1268
|
+
if (actualFree < 5) {
|
|
1269
|
+
return {
|
|
1270
|
+
success: false,
|
|
1271
|
+
message: [
|
|
1272
|
+
`${c.red}✗ Not enough disk space (${actualFree}GB free, need 10GB).${c.reset}`,
|
|
1273
|
+
``,
|
|
1274
|
+
`${c.bold}To install on a different drive, say:${c.reset}`,
|
|
1275
|
+
` ${c.cyan}"install stable diffusion on F drive"${c.reset}`,
|
|
1276
|
+
` ${c.cyan}"install stable diffusion on /mnt/f"${c.reset}`,
|
|
1277
|
+
` Or set: ${c.dim}NOTOKEN_INSTALL_DIR=/mnt/f/apps notoken install stable-diffusion${c.reset}`,
|
|
1278
|
+
].join("\n"),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
console.log(`${c.dim}To change location: "put it on F drive" or "install stable diffusion on F drive"${c.reset}\n`);
|
|
1283
|
+
// Store as pending action so user can say "put it on F drive"
|
|
1284
|
+
const { suggestAction: suggest } = await import("../conversation/pendingActions.js");
|
|
1285
|
+
suggest({
|
|
1286
|
+
action: `install stable diffusion`,
|
|
1287
|
+
description: `Install Stable Diffusion at ${actualBase}`,
|
|
1288
|
+
type: "intent",
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
// ── Linux / WSL / macOS — install prerequisites then engine ──
|
|
1292
|
+
// Step 1: git
|
|
1293
|
+
if (!tryExec("git --version")) {
|
|
1294
|
+
console.log(`${c.cyan}Step 1/${c.reset} Installing git...`);
|
|
1295
|
+
try {
|
|
1296
|
+
if (tryExec("apt-get --version")) {
|
|
1297
|
+
execSync("apt-get update -qq && apt-get install -y -qq git", { stdio: "inherit", timeout: 120000 });
|
|
1298
|
+
}
|
|
1299
|
+
else if (tryExec("dnf --version")) {
|
|
1300
|
+
execSync("dnf install -y git", { stdio: "inherit", timeout: 120000 });
|
|
1301
|
+
}
|
|
1302
|
+
else if (tryExec("brew --version")) {
|
|
1303
|
+
execSync("brew install git", { stdio: "inherit", timeout: 120000 });
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
return { success: false, message: "Cannot auto-install git. Install it manually first." };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
catch (err) {
|
|
1310
|
+
return { success: false, message: `Failed to install git: ${err instanceof Error ? err.message : err}` };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
console.log(`${c.cyan}Step 1/${c.reset} ${c.green}git found${c.reset} — ${tryExec("git --version")}`);
|
|
1315
|
+
}
|
|
1316
|
+
// Step 2: Python 3.10+
|
|
1317
|
+
let pythonCmd = tryExec("python3 --version") ? "python3" : tryExec("python --version") ? "python" : null;
|
|
1318
|
+
const pyVersion = pythonCmd ? tryExec(`${pythonCmd} --version`) : null;
|
|
1319
|
+
const pyMatch = pyVersion?.match(/(\d+)\.(\d+)/);
|
|
1320
|
+
const pyOk = pyMatch && parseInt(pyMatch[1]) >= 3 && parseInt(pyMatch[2]) >= 10;
|
|
1321
|
+
if (!pyOk) {
|
|
1322
|
+
console.log(`${c.cyan}Step 2/${c.reset} Installing Python 3.11...`);
|
|
1323
|
+
try {
|
|
1324
|
+
if (tryExec("apt-get --version")) {
|
|
1325
|
+
execSync("apt-get update -qq && apt-get install -y -qq python3.11 python3.11-venv python3-pip", { stdio: "inherit", timeout: 120000 });
|
|
1326
|
+
pythonCmd = "python3.11";
|
|
1327
|
+
}
|
|
1328
|
+
else if (tryExec("dnf --version")) {
|
|
1329
|
+
execSync("dnf install -y python3.11 python3.11-pip", { stdio: "inherit", timeout: 120000 });
|
|
1330
|
+
pythonCmd = "python3.11";
|
|
1331
|
+
}
|
|
1332
|
+
else if (tryExec("brew --version")) {
|
|
1333
|
+
execSync("brew install python@3.11", { stdio: "inherit", timeout: 120000 });
|
|
1334
|
+
pythonCmd = "python3.11";
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
return { success: false, message: "Cannot auto-install Python. Install Python 3.10+ manually." };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
return { success: false, message: `Failed to install Python: ${err instanceof Error ? err.message : err}` };
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
console.log(`${c.cyan}Step 2/${c.reset} ${c.green}Python found${c.reset} — ${pyVersion}`);
|
|
1346
|
+
}
|
|
1347
|
+
// Step 3: pip/venv
|
|
1348
|
+
if (!tryExec(`${pythonCmd} -m venv --help 2>/dev/null`)) {
|
|
1349
|
+
console.log(`${c.cyan}Step 3/${c.reset} Installing python3-venv...`);
|
|
1350
|
+
try {
|
|
1351
|
+
if (tryExec("apt-get --version")) {
|
|
1352
|
+
execSync(`apt-get install -y -qq python3-venv python3-full 2>/dev/null || apt-get install -y -qq python3.11-venv 2>/dev/null || true`, { stdio: "inherit", timeout: 60000 });
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
catch { }
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
console.log(`${c.cyan}Step 3/${c.reset} ${c.green}venv available${c.reset}`);
|
|
1359
|
+
}
|
|
1360
|
+
// Step 3b: Build tools (needed for scikit-image, tokenizers, scipy, etc.)
|
|
1361
|
+
{
|
|
1362
|
+
const hasGcc = !!tryExec("gcc --version 2>/dev/null");
|
|
1363
|
+
const hasCmake = !!tryExec("cmake --version 2>/dev/null");
|
|
1364
|
+
const hasRust = !!tryExec("cargo --version 2>/dev/null") || !!tryExec(". $HOME/.cargo/env 2>/dev/null && cargo --version");
|
|
1365
|
+
if (!hasGcc || !hasCmake || !hasRust) {
|
|
1366
|
+
console.log(`${c.cyan}Step 3b/${c.reset} Installing build tools...`);
|
|
1367
|
+
const missing = [];
|
|
1368
|
+
if (!hasGcc)
|
|
1369
|
+
missing.push("gcc/build-essential");
|
|
1370
|
+
if (!hasCmake)
|
|
1371
|
+
missing.push("cmake");
|
|
1372
|
+
if (!hasRust)
|
|
1373
|
+
missing.push("rust/cargo");
|
|
1374
|
+
console.log(`${c.dim} Missing: ${missing.join(", ")}${c.reset}`);
|
|
1375
|
+
try {
|
|
1376
|
+
// C/C++ build tools
|
|
1377
|
+
if (!hasGcc || !hasCmake) {
|
|
1378
|
+
if (tryExec("apt-get --version")) {
|
|
1379
|
+
execSync("apt-get install -y -qq build-essential cmake pkg-config libffi-dev libjpeg-dev libpng-dev 2>/dev/null", { stdio: "inherit", timeout: 120000 });
|
|
1380
|
+
}
|
|
1381
|
+
else if (tryExec("dnf --version")) {
|
|
1382
|
+
execSync("dnf install -y gcc gcc-c++ cmake pkg-config libffi-devel libjpeg-devel libpng-devel 2>/dev/null", { stdio: "inherit", timeout: 120000 });
|
|
1383
|
+
}
|
|
1384
|
+
else if (tryExec("brew --version")) {
|
|
1385
|
+
execSync("brew install cmake pkg-config 2>/dev/null", { stdio: "inherit", timeout: 120000 });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Rust (needed for tokenizers, safetensors)
|
|
1389
|
+
if (!hasRust) {
|
|
1390
|
+
console.log(`${c.dim} Installing Rust (needed for tokenizers)...${c.reset}`);
|
|
1391
|
+
execSync("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 2>/dev/null", { stdio: "inherit", timeout: 120000 });
|
|
1392
|
+
}
|
|
1393
|
+
// Python build tools
|
|
1394
|
+
tryExec(`${pythonCmd} -m pip install --user meson-python meson ninja Cython 2>/dev/null`);
|
|
1395
|
+
}
|
|
1396
|
+
catch {
|
|
1397
|
+
console.log(`${c.yellow}⚠${c.reset} Some build tools could not be installed — pip may fall back to pre-built wheels`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
console.log(`${c.cyan}Step 3b/${c.reset} ${c.green}Build tools available${c.reset} (gcc, cmake, rust)`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// Step 4: Clone and setup
|
|
1405
|
+
const dirs = { auto1111: getSDDir(), comfyui: getComfyDir(), fooocus: getFooocusDir() };
|
|
1406
|
+
const dir = dirs[engine];
|
|
1407
|
+
const repos = {
|
|
1408
|
+
auto1111: "https://github.com/AUTOMATIC1111/stable-diffusion-webui.git",
|
|
1409
|
+
comfyui: "https://github.com/comfyanonymous/ComfyUI.git",
|
|
1410
|
+
fooocus: "https://github.com/lllyasviel/Fooocus.git",
|
|
1411
|
+
};
|
|
1412
|
+
// Always use WSL Python for pip installs — Windows Python may lack build tools
|
|
1413
|
+
// and PowerShell execution is unreliable for complex pip builds.
|
|
1414
|
+
// The WSL→NTFS bridge is slower but more reliable.
|
|
1415
|
+
const useWindowsSide = false;
|
|
1416
|
+
if (isWSL && isWslWindowsPath(dir)) {
|
|
1417
|
+
console.log(`${c.dim} Installing to Windows drive via WSL (slower I/O but reliable)${c.reset}`);
|
|
1418
|
+
}
|
|
1419
|
+
if (existsSync(dir)) {
|
|
1420
|
+
console.log(`${c.cyan}Step 4/${c.reset} ${c.green}Already cloned${c.reset} at ${dir}`);
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
console.log(`${c.cyan}Step 4/${c.reset} Cloning ${engine}...`);
|
|
1424
|
+
try {
|
|
1425
|
+
if (useWindowsSide) {
|
|
1426
|
+
const winDir = toWindowsPath(dir);
|
|
1427
|
+
await runOnWindowsSide(`git clone '${repos[engine]}' '${winDir}'`, resolve(dir, ".."));
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
execSync(`git clone "${repos[engine]}" "${dir}"`, { stdio: "inherit", timeout: 300000 });
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
catch (err) {
|
|
1434
|
+
return { success: false, message: `Clone failed: ${err instanceof Error ? err.message : err}` };
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Step 5: Create venv and install deps
|
|
1438
|
+
console.log(`${c.cyan}Step 5/${c.reset} Setting up virtual environment and dependencies...`);
|
|
1439
|
+
const sizeNote = gpu.hasNvidia ? "~2GB for GPU" : "~190MB for CPU";
|
|
1440
|
+
console.log(`${c.dim} This downloads PyTorch (${sizeNote}). May take 5-15 minutes.${c.reset}`);
|
|
1441
|
+
if (useWindowsSide) {
|
|
1442
|
+
console.log(`${c.dim} Running via Windows Python for faster disk I/O on Windows drives.${c.reset}`);
|
|
1443
|
+
}
|
|
1444
|
+
const torchExtra = gpu.hasNvidia ? "cu121" : gpu.hasAmd ? "rocm5.7" : "cpu";
|
|
1445
|
+
try {
|
|
1446
|
+
if (useWindowsSide) {
|
|
1447
|
+
// Run via Windows Python — much faster for I/O on NTFS drives
|
|
1448
|
+
const winDir = toWindowsPath(dir);
|
|
1449
|
+
console.log(`${c.dim} Creating virtual environment (Windows Python)...${c.reset}`);
|
|
1450
|
+
await runOnWindowsSide(`python -m venv venv`, dir);
|
|
1451
|
+
console.log(`${c.dim} Upgrading pip...${c.reset}`);
|
|
1452
|
+
await runOnWindowsSide(`venv\\Scripts\\pip install --upgrade pip`, dir);
|
|
1453
|
+
console.log(`${c.dim} Installing PyTorch (${gpu.hasNvidia ? "GPU/CUDA" : "CPU"} version)...${c.reset}`);
|
|
1454
|
+
await runOnWindowsSide(`venv\\Scripts\\pip install torch torchvision --index-url https://download.pytorch.org/whl/${torchExtra}`, dir);
|
|
1455
|
+
if (engine === "auto1111" && existsSync(resolve(dir, "requirements_versions.txt"))) {
|
|
1456
|
+
console.log(`${c.dim} Installing Stable Diffusion requirements...${c.reset}`);
|
|
1457
|
+
await runOnWindowsSide(`venv\\Scripts\\pip install -r requirements_versions.txt`, dir);
|
|
1458
|
+
}
|
|
1459
|
+
else if ((engine === "comfyui" || engine === "fooocus")) {
|
|
1460
|
+
const reqFile = engine === "fooocus" ? "requirements_versions.txt" : "requirements.txt";
|
|
1461
|
+
if (existsSync(resolve(dir, reqFile))) {
|
|
1462
|
+
console.log(`${c.dim} Installing ${reqFile}...${c.reset}`);
|
|
1463
|
+
await runOnWindowsSide(`venv\\Scripts\\pip install -r ${reqFile}`, dir);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
// Run via WSL Python
|
|
1469
|
+
const venvPip = `${dir}/venv/bin/pip`;
|
|
1470
|
+
if (!existsSync(`${dir}/venv`)) {
|
|
1471
|
+
console.log(`${c.dim} Creating virtual environment...${c.reset}`);
|
|
1472
|
+
execSync(`${pythonCmd} -m venv "${dir}/venv"`, { stdio: "inherit", timeout: 120000 });
|
|
1473
|
+
}
|
|
1474
|
+
console.log(`${c.dim} Upgrading pip...${c.reset}`);
|
|
1475
|
+
await runWithProgress(venvPip, ["install", "--upgrade", "pip"], dir);
|
|
1476
|
+
console.log(`${c.dim} Installing PyTorch (${gpu.hasNvidia ? "GPU/CUDA" : "CPU"} version)...${c.reset}`);
|
|
1477
|
+
await runWithProgress(venvPip, [
|
|
1478
|
+
"install", "torch", "torchvision",
|
|
1479
|
+
"--index-url", `https://download.pytorch.org/whl/${torchExtra}`,
|
|
1480
|
+
], dir);
|
|
1481
|
+
if (engine === "auto1111" && existsSync(resolve(dir, "requirements_versions.txt"))) {
|
|
1482
|
+
console.log(`${c.dim} Installing Stable Diffusion requirements...${c.reset}`);
|
|
1483
|
+
// Fix pinned versions that don't have wheels for this Python version
|
|
1484
|
+
const reqPath = resolve(dir, "requirements_versions.txt");
|
|
1485
|
+
const reqContent = readFileSync(reqPath, "utf-8");
|
|
1486
|
+
const fixedReq = reqContent
|
|
1487
|
+
.replace(/scikit-image==[\d.]+/, "scikit-image>=0.21") // 0.21.0 has no wheel for py3.12
|
|
1488
|
+
.replace(/numpy==[\d.]+/, "numpy>=1.24") // relax numpy too
|
|
1489
|
+
.replace(/transformers==[\d.]+/, "transformers>=4.30") // old pin pulls tokenizers that needs Rust build
|
|
1490
|
+
.replace(/tokenizers==[\d.]+/, "tokenizers>=0.14") // force pre-built wheel version
|
|
1491
|
+
.replace(/Pillow==[\d.]+/, "Pillow>=9.5") // relax Pillow too
|
|
1492
|
+
;
|
|
1493
|
+
const fixedReqPath = resolve(dir, "requirements_notoken.txt");
|
|
1494
|
+
writeFileSync(fixedReqPath, fixedReq);
|
|
1495
|
+
// Pre-install packages that are hard to build from source
|
|
1496
|
+
console.log(`${c.dim} Pre-installing packages with pre-built wheels...${c.reset}`);
|
|
1497
|
+
await runWithProgress(venvPip, ["install", "--only-binary=:all:", "tokenizers>=0.14", "transformers>=4.30", "safetensors>=0.3"], dir);
|
|
1498
|
+
console.log(`${c.dim} Relaxed version pins for wheel compatibility${c.reset}`);
|
|
1499
|
+
await runWithProgress(venvPip, ["install", "--prefer-binary", "-r", fixedReqPath], dir);
|
|
1500
|
+
}
|
|
1501
|
+
else if (engine === "comfyui" || engine === "fooocus") {
|
|
1502
|
+
const reqFile = engine === "fooocus" ? "requirements_versions.txt" : "requirements.txt";
|
|
1503
|
+
if (existsSync(resolve(dir, reqFile))) {
|
|
1504
|
+
console.log(`${c.dim} Installing ${reqFile}...${c.reset}`);
|
|
1505
|
+
await runWithProgress(venvPip, ["install", "-r", resolve(dir, reqFile)], dir);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
console.log(`${c.green}✓${c.reset} Dependencies installed.`);
|
|
1510
|
+
}
|
|
1511
|
+
catch (err) {
|
|
1512
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1513
|
+
console.log(`${c.yellow}⚠${c.reset} Python dependency install failed: ${errMsg.split("\n")[0]}`);
|
|
1514
|
+
// Auto-fallback: try Docker if available
|
|
1515
|
+
if (tryExec("docker --version")) {
|
|
1516
|
+
console.log(`\n${c.cyan}Falling back to Docker${c.reset} — no dependency issues, everything pre-built.`);
|
|
1517
|
+
const dockerResult = await installImageEngine("docker");
|
|
1518
|
+
if (dockerResult.success)
|
|
1519
|
+
return dockerResult;
|
|
1520
|
+
}
|
|
1521
|
+
return { success: false, message: `Dependency install failed.\n\n${c.bold}Alternatives:${c.reset}\n ${c.cyan}notoken install stable-diffusion --docker${c.reset} — containerized, no deps\n ${c.cyan}notoken install stability-matrix${c.reset} — standalone, no Python needed\n ${c.dim}Or fix manually: cd ${dir} && source venv/bin/activate && pip install -r requirements_versions.txt${c.reset}` };
|
|
1522
|
+
}
|
|
1523
|
+
// Step 6: Download the base AI model
|
|
1524
|
+
{
|
|
1525
|
+
const modelsDir = resolve(dir, "models", "Stable-diffusion");
|
|
1526
|
+
mkdirSync(modelsDir, { recursive: true });
|
|
1527
|
+
// Check if any model already exists
|
|
1528
|
+
const hasModel = (() => {
|
|
1529
|
+
try {
|
|
1530
|
+
const files = readdirSync(modelsDir);
|
|
1531
|
+
return files.some(f => f.endsWith(".safetensors") || f.endsWith(".ckpt"));
|
|
1532
|
+
}
|
|
1533
|
+
catch {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
})();
|
|
1537
|
+
if (hasModel) {
|
|
1538
|
+
console.log(`${c.cyan}Step 6/${c.reset} ${c.green}Model already downloaded${c.reset}`);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
// Download SD 1.5 base model (~4.3GB) — most compatible, works on CPU
|
|
1542
|
+
const modelUrl = "https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors";
|
|
1543
|
+
const modelPath = resolve(modelsDir, "v1-5-pruned-emaonly.safetensors");
|
|
1544
|
+
console.log(`${c.cyan}Step 6/${c.reset} Downloading AI model (Stable Diffusion 1.5, ~4.3GB)...`);
|
|
1545
|
+
console.log(`${c.dim} This is the largest download — may take 5-15 minutes depending on connection.${c.reset}`);
|
|
1546
|
+
console.log(`${c.dim} Saving to: ${modelsDir}${c.reset}`);
|
|
1547
|
+
try {
|
|
1548
|
+
await runWithProgress("curl", [
|
|
1549
|
+
"-L", "--progress-bar", "-o", modelPath, modelUrl,
|
|
1550
|
+
], dir);
|
|
1551
|
+
// Verify file size (should be ~4GB)
|
|
1552
|
+
const { statSync: fstat } = await import("node:fs");
|
|
1553
|
+
const size = fstat(modelPath).size;
|
|
1554
|
+
if (size > 1_000_000_000) {
|
|
1555
|
+
console.log(`${c.green}✓${c.reset} Model downloaded (${(size / 1_073_741_824).toFixed(1)}GB)`);
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
console.log(`${c.yellow}⚠${c.reset} Model file seems small (${(size / 1_048_576).toFixed(0)}MB) — may need re-download`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
catch (err) {
|
|
1562
|
+
console.log(`${c.yellow}⚠${c.reset} Model download failed: ${err instanceof Error ? err.message : err}`);
|
|
1563
|
+
console.log(`${c.dim} You can download it manually later. The engine will prompt you on first launch.${c.reset}`);
|
|
1564
|
+
console.log(`${c.dim} Or run: curl -L -o "${modelPath}" "${modelUrl}"${c.reset}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// Step 7: Track install + verify
|
|
1569
|
+
console.log(`${c.cyan}Step 7/${c.reset} ${c.green}Installation complete${c.reset}`);
|
|
1570
|
+
const plan = getInstallPlan(engine);
|
|
1571
|
+
// Track what we installed
|
|
1572
|
+
const du = tryExec(`du -sh "${dir}" 2>/dev/null`);
|
|
1573
|
+
const installSize = du?.split("\t")[0] ?? "unknown";
|
|
1574
|
+
trackInstall({
|
|
1575
|
+
name: `stable-diffusion-${engine}`,
|
|
1576
|
+
type: "engine",
|
|
1577
|
+
method: "git-clone",
|
|
1578
|
+
path: dir,
|
|
1579
|
+
size: installSize,
|
|
1580
|
+
uninstallCmd: `rm -rf "${dir}"`,
|
|
1581
|
+
dependencies: ["torch", "torchvision", "numpy", "pillow"],
|
|
1582
|
+
notes: `Installed via notoken on ${new Date().toLocaleDateString()}`,
|
|
1583
|
+
});
|
|
1584
|
+
// Check if model was downloaded
|
|
1585
|
+
const modelsCheck = resolve(dir, "models", "Stable-diffusion");
|
|
1586
|
+
let hasModelNow = false;
|
|
1587
|
+
try {
|
|
1588
|
+
hasModelNow = readdirSync(modelsCheck).some(f => f.endsWith(".safetensors") || f.endsWith(".ckpt"));
|
|
1589
|
+
}
|
|
1590
|
+
catch { }
|
|
1591
|
+
// Store pending action so user can say "try it"
|
|
1592
|
+
const { suggestAction } = await import("../conversation/pendingActions.js");
|
|
1593
|
+
suggestAction({
|
|
1594
|
+
action: "generate a picture of a cat",
|
|
1595
|
+
description: "Generate a test image to verify the install works",
|
|
1596
|
+
type: "intent",
|
|
1597
|
+
});
|
|
1598
|
+
const modelNote = hasModelNow
|
|
1599
|
+
? `${c.green}✓${c.reset} Model downloaded — ready to generate.`
|
|
1600
|
+
: `${c.yellow}Note:${c.reset} First launch will download the AI model (~4GB). Takes 5-10 minutes.`;
|
|
1601
|
+
return {
|
|
1602
|
+
success: true,
|
|
1603
|
+
message: [
|
|
1604
|
+
`${c.green}✓${c.reset} ${plan.engine} installed at ${dir} (${installSize})`,
|
|
1605
|
+
``,
|
|
1606
|
+
modelNote,
|
|
1607
|
+
``,
|
|
1608
|
+
`${c.bold}Say "try it" or "generate a picture of a cat" to test.${c.reset}`,
|
|
1609
|
+
].join("\n"),
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
// ─── Windows Native Install ────────────────────────────────────────────────
|
|
1613
|
+
async function installStabilityMatrix(platform) {
|
|
1614
|
+
const smUrl = "https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip";
|
|
1615
|
+
try {
|
|
1616
|
+
if (platform === "wsl") {
|
|
1617
|
+
const installDir = process.env.NOTOKEN_INSTALL_DIR ?? "/mnt/d/notoken/ai";
|
|
1618
|
+
const smDir = `${installDir}/StabilityMatrix`;
|
|
1619
|
+
const smZip = "/tmp/StabilityMatrix.zip";
|
|
1620
|
+
console.log(`${c.dim} Downloading Stability Matrix (138MB)...${c.reset}`);
|
|
1621
|
+
execSync(`curl -sfL -o "${smZip}" "${smUrl}"`, { stdio: "inherit", timeout: 300000 });
|
|
1622
|
+
console.log(`${c.dim} Extracting...${c.reset}`);
|
|
1623
|
+
mkdirSync(smDir, { recursive: true });
|
|
1624
|
+
execSync(`unzip -o -q "${smZip}" -d "${smDir}"`, { stdio: "inherit", timeout: 60000 });
|
|
1625
|
+
// Launch on Windows side
|
|
1626
|
+
const winPath = tryExec(`wslpath -w "${smDir}" 2>/dev/null`);
|
|
1627
|
+
if (winPath) {
|
|
1628
|
+
console.log(`${c.dim} Launching on Windows...${c.reset}`);
|
|
1629
|
+
try {
|
|
1630
|
+
execSync(`/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -Command "Start-Process '${winPath}\\StabilityMatrix.exe'" 2>/dev/null`, { stdio: "ignore", timeout: 10000 });
|
|
1631
|
+
}
|
|
1632
|
+
catch { }
|
|
1633
|
+
}
|
|
1634
|
+
// Auto-configure SM settings to skip first-launch wizard
|
|
1635
|
+
try {
|
|
1636
|
+
const settingsPath = resolve(smDir, "Data", "settings.json");
|
|
1637
|
+
mkdirSync(resolve(smDir, "Data"), { recursive: true });
|
|
1638
|
+
let settings = {};
|
|
1639
|
+
if (existsSync(settingsPath)) {
|
|
1640
|
+
try {
|
|
1641
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
1642
|
+
}
|
|
1643
|
+
catch { }
|
|
1644
|
+
}
|
|
1645
|
+
settings.FirstLaunchSetupComplete = true;
|
|
1646
|
+
settings.HasSeenWelcomeNotification = true;
|
|
1647
|
+
settings.Theme = "Dark";
|
|
1648
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1649
|
+
console.log(`${c.dim} Auto-configured settings (skip wizard, dark theme)${c.reset}`);
|
|
1650
|
+
}
|
|
1651
|
+
catch { }
|
|
1652
|
+
trackInstall({ name: "StabilityMatrix", type: "engine", method: "curl", path: smDir, uninstallCmd: `rm -rf "${smDir}"` });
|
|
1653
|
+
// Detect driver to recommend the right package
|
|
1654
|
+
const gpuCheck = detectGpu();
|
|
1655
|
+
const maxCuda = gpuCheck.maxCudaVersion ?? "unknown";
|
|
1656
|
+
const recommended = gpuCheck.driverVersion && parseFloat(gpuCheck.driverVersion) < 570
|
|
1657
|
+
? `"Stable Diffusion WebUI Forge"` // uses cu121/cu124, works with older drivers
|
|
1658
|
+
: `"Forge Neo"`; // uses cu130, needs driver 570+
|
|
1659
|
+
const driverNote = gpuCheck.driverVersion && parseFloat(gpuCheck.driverVersion) < 570
|
|
1660
|
+
? `\n ${c.yellow}⚠ Your driver (${gpuCheck.driverVersion}) supports CUDA ${maxCuda} — do NOT choose "Forge Neo" (needs CUDA 13.0)${c.reset}`
|
|
1661
|
+
: "";
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
message: [
|
|
1665
|
+
`${c.green}✓${c.reset} Stability Matrix installed at ${smDir}`,
|
|
1666
|
+
``,
|
|
1667
|
+
`${c.bold}It's now open on your Windows desktop.${c.reset}`,
|
|
1668
|
+
` 1. Click "+" and choose ${c.bold}${recommended}${c.reset}${driverNote}`,
|
|
1669
|
+
` 2. SM downloads Python, models, and everything automatically`,
|
|
1670
|
+
` 3. Click "Launch" when it's done installing`,
|
|
1671
|
+
` 4. Once running, come back and say "generate a picture of a cat"`,
|
|
1672
|
+
``,
|
|
1673
|
+
`${c.dim}No manual setup needed — SM handles all dependencies.${c.reset}`,
|
|
1674
|
+
].join("\n"),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
// Native Windows
|
|
1678
|
+
const installDir = process.env.NOTOKEN_INSTALL_DIR ?? "D:\\notoken\\ai";
|
|
1679
|
+
const smDir = `${installDir}\\StabilityMatrix`;
|
|
1680
|
+
const smZip = `${process.env.TEMP ?? "C:\\Temp"}\\StabilityMatrix.zip`;
|
|
1681
|
+
console.log(`${c.dim} Downloading Stability Matrix (138MB)...${c.reset}`);
|
|
1682
|
+
execSync(`powershell -Command "New-Item -Path '${installDir}' -ItemType Directory -Force | Out-Null; Invoke-WebRequest -Uri '${smUrl}' -OutFile '${smZip}'"`, { stdio: "inherit", timeout: 300000, shell: "cmd.exe" });
|
|
1683
|
+
console.log(`${c.dim} Extracting...${c.reset}`);
|
|
1684
|
+
execSync(`powershell -Command "Expand-Archive -Path '${smZip}' -DestinationPath '${smDir}' -Force"`, { stdio: "inherit", timeout: 60000, shell: "cmd.exe" });
|
|
1685
|
+
console.log(`${c.dim} Launching...${c.reset}`);
|
|
1686
|
+
try {
|
|
1687
|
+
execSync(`start "" "${smDir}\\StabilityMatrix.exe"`, { stdio: "ignore", shell: "cmd.exe", timeout: 10000 });
|
|
1688
|
+
}
|
|
1689
|
+
catch { }
|
|
1690
|
+
trackInstall({ name: "StabilityMatrix", type: "engine", method: "curl", path: smDir, uninstallCmd: `rmdir /s /q "${smDir}"` });
|
|
1691
|
+
return {
|
|
1692
|
+
success: true,
|
|
1693
|
+
message: [
|
|
1694
|
+
`${c.green}✓${c.reset} Stability Matrix installed at ${smDir}`,
|
|
1695
|
+
` It's now open — choose a UI and it downloads everything.`,
|
|
1696
|
+
` Say "generate a picture of a cat" when ready.`,
|
|
1697
|
+
].join("\n"),
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
catch (err) {
|
|
1701
|
+
return { success: false, message: `Stability Matrix download failed: ${err instanceof Error ? err.message : err}` };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
async function installOnWindows(engine, gpu) {
|
|
1705
|
+
const home = process.env.USERPROFILE ?? "C:\\Users\\Default";
|
|
1706
|
+
const installDir = `${home}\\StableDiffusion`;
|
|
1707
|
+
// Strategy: try winget for prerequisites, then PowerShell for download
|
|
1708
|
+
console.log(`${c.cyan}Step 1/${c.reset} Windows detected — setting up prerequisites...`);
|
|
1709
|
+
// 1. Check/install git via winget
|
|
1710
|
+
if (!tryExec("git --version")) {
|
|
1711
|
+
console.log(`${c.cyan} 1a/${c.reset} Installing git via winget...`);
|
|
1712
|
+
try {
|
|
1713
|
+
execSync("winget install Git.Git --accept-source-agreements --accept-package-agreements -h", { stdio: "inherit", timeout: 120000 });
|
|
1714
|
+
// Refresh PATH
|
|
1715
|
+
console.log(`${c.green} ✓${c.reset} git installed. You may need to restart terminal for PATH update.`);
|
|
1716
|
+
}
|
|
1717
|
+
catch {
|
|
1718
|
+
console.log(`${c.yellow} ⚠${c.reset} winget not available — trying direct download...`);
|
|
1719
|
+
try {
|
|
1720
|
+
execSync(`powershell -Command "Invoke-WebRequest -Uri 'https://github.com/git-for-windows/git/releases/latest/download/Git-2.47.1-64-bit.exe' -OutFile '%TEMP%\\git-installer.exe'; Start-Process '%TEMP%\\git-installer.exe' -ArgumentList '/VERYSILENT /NORESTART' -Wait"`, { stdio: "inherit", timeout: 300000, shell: "cmd.exe" });
|
|
1721
|
+
}
|
|
1722
|
+
catch {
|
|
1723
|
+
return { success: false, message: "Could not install git. Install manually from https://git-scm.com/download/win" };
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
else {
|
|
1728
|
+
console.log(`${c.cyan} 1a/${c.reset} ${c.green}git found${c.reset}`);
|
|
1729
|
+
}
|
|
1730
|
+
// 2. Check/install Python via winget
|
|
1731
|
+
const python = tryExec("python --version") ?? tryExec("python3 --version");
|
|
1732
|
+
const pyMatch = python?.match(/(\d+)\.(\d+)/);
|
|
1733
|
+
const pyOk = pyMatch && parseInt(pyMatch[1]) >= 3 && parseInt(pyMatch[2]) >= 10;
|
|
1734
|
+
if (!pyOk) {
|
|
1735
|
+
console.log(`${c.cyan} 1b/${c.reset} Installing Python 3.11...`);
|
|
1736
|
+
try {
|
|
1737
|
+
execSync("winget install Python.Python.3.11 --accept-source-agreements --accept-package-agreements -h", { stdio: "inherit", timeout: 120000 });
|
|
1738
|
+
console.log(`${c.green} ✓${c.reset} Python 3.11 installed.`);
|
|
1739
|
+
}
|
|
1740
|
+
catch {
|
|
1741
|
+
try {
|
|
1742
|
+
// Direct download fallback
|
|
1743
|
+
execSync(`powershell -Command "Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe' -OutFile '%TEMP%\\python-installer.exe'; Start-Process '%TEMP%\\python-installer.exe' -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait"`, { stdio: "inherit", timeout: 300000, shell: "cmd.exe" });
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
return { success: false, message: "Could not install Python. Install manually from https://python.org/downloads/" };
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
console.log(`${c.cyan} 1b/${c.reset} ${c.green}Python found${c.reset} — ${python}`);
|
|
1752
|
+
}
|
|
1753
|
+
// 3. Clone the repo
|
|
1754
|
+
console.log(`${c.cyan}Step 2/${c.reset} Downloading ${engine}...`);
|
|
1755
|
+
const repos = {
|
|
1756
|
+
auto1111: "https://github.com/AUTOMATIC1111/stable-diffusion-webui.git",
|
|
1757
|
+
comfyui: "https://github.com/comfyanonymous/ComfyUI.git",
|
|
1758
|
+
fooocus: "https://github.com/lllyasviel/Fooocus.git",
|
|
1759
|
+
};
|
|
1760
|
+
const engineDir = `${installDir}\\${engine}`;
|
|
1761
|
+
try {
|
|
1762
|
+
execSync(`if not exist "${installDir}" mkdir "${installDir}"`, { shell: "cmd.exe", stdio: "ignore" });
|
|
1763
|
+
if (tryExec(`if exist "${engineDir}" echo yes`)) {
|
|
1764
|
+
console.log(`${c.green} ✓${c.reset} Already downloaded at ${engineDir}`);
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
execSync(`git clone "${repos[engine]}" "${engineDir}"`, { stdio: "inherit", timeout: 300000 });
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
catch (err) {
|
|
1771
|
+
return { success: false, message: `Download failed: ${err instanceof Error ? err.message : err}` };
|
|
1772
|
+
}
|
|
1773
|
+
// 4. Create venv and install deps
|
|
1774
|
+
console.log(`${c.cyan}Step 3/${c.reset} Setting up Python environment and dependencies...`);
|
|
1775
|
+
console.log(`${c.dim} This may take 5-15 minutes (downloading PyTorch)...${c.reset}`);
|
|
1776
|
+
const torchUrl = gpu.hasNvidia ? "cu121" : "cpu";
|
|
1777
|
+
const pythonExe = "python";
|
|
1778
|
+
try {
|
|
1779
|
+
console.log(`${c.dim} Creating virtual environment...${c.reset}`);
|
|
1780
|
+
execSync(`cd /d "${engineDir}" && ${pythonExe} -m venv venv`, { stdio: "inherit", timeout: 60000, shell: "cmd.exe" });
|
|
1781
|
+
console.log(`${c.dim} Upgrading pip...${c.reset}`);
|
|
1782
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install --upgrade pip`, { stdio: "inherit", timeout: 60000, shell: "cmd.exe" });
|
|
1783
|
+
console.log(`${c.dim} Installing PyTorch (${gpu.hasNvidia ? "GPU" : "CPU"})...${c.reset}`);
|
|
1784
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install torch torchvision --index-url https://download.pytorch.org/whl/${torchUrl}`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1785
|
+
// Install engine requirements
|
|
1786
|
+
if (engine === "auto1111") {
|
|
1787
|
+
console.log(`${c.dim} Installing Stable Diffusion requirements...${c.reset}`);
|
|
1788
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install -r requirements_versions.txt`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1789
|
+
}
|
|
1790
|
+
else if (engine === "comfyui") {
|
|
1791
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install -r requirements.txt`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1792
|
+
}
|
|
1793
|
+
else if (engine === "fooocus") {
|
|
1794
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install -r requirements_versions.txt`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
catch (err) {
|
|
1798
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1799
|
+
// If build fails, try installing Visual C++ Build Tools
|
|
1800
|
+
if (errMsg.includes("error") && errMsg.includes("build")) {
|
|
1801
|
+
console.log(`${c.yellow}⚠ Build failed — trying to install Visual C++ Build Tools...${c.reset}`);
|
|
1802
|
+
try {
|
|
1803
|
+
execSync("winget install Microsoft.VisualStudio.2022.BuildTools --accept-source-agreements --accept-package-agreements -h", { stdio: "inherit", timeout: 300000 });
|
|
1804
|
+
console.log(`${c.dim} Retrying pip install...${c.reset}`);
|
|
1805
|
+
execSync(`cd /d "${engineDir}" && venv\\Scripts\\pip install -r requirements_versions.txt`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1806
|
+
}
|
|
1807
|
+
catch {
|
|
1808
|
+
return { success: false, message: `Dependency install failed.\n\n${c.bold}Alternatives:${c.reset}\n ${c.cyan}Download Stability Matrix:${c.reset} https://lykos.ai (no build tools needed)\n ${c.dim}Or install Visual C++ Build Tools manually: winget install Microsoft.VisualStudio.2022.BuildTools${c.reset}` };
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
return { success: false, message: `Dependency install failed: ${errMsg.split("\n")[0]}\n\n${c.dim}Alternative: download Stability Matrix from https://lykos.ai (no Python needed)${c.reset}` };
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
// 5. Download base model
|
|
1816
|
+
{
|
|
1817
|
+
const modelsDir = `${engineDir}\\models\\Stable-diffusion`;
|
|
1818
|
+
try {
|
|
1819
|
+
execSync(`if not exist "${modelsDir}" mkdir "${modelsDir}"`, { shell: "cmd.exe", stdio: "ignore" });
|
|
1820
|
+
}
|
|
1821
|
+
catch { }
|
|
1822
|
+
const modelPath = `${modelsDir}\\v1-5-pruned-emaonly.safetensors`;
|
|
1823
|
+
const hasModel = tryExec(`if exist "${modelPath}" echo yes`);
|
|
1824
|
+
if (hasModel) {
|
|
1825
|
+
console.log(`${c.cyan}Step 4/${c.reset} ${c.green}Model already downloaded${c.reset}`);
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
console.log(`${c.cyan}Step 4/${c.reset} Downloading AI model (SD 1.5, ~4.3GB)...`);
|
|
1829
|
+
console.log(`${c.dim} This is the largest download — may take 5-15 minutes.${c.reset}`);
|
|
1830
|
+
try {
|
|
1831
|
+
execSync(`powershell -Command "Invoke-WebRequest -Uri 'https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors' -OutFile '${modelPath}'"`, { stdio: "inherit", timeout: 600000, shell: "cmd.exe" });
|
|
1832
|
+
console.log(`${c.green}✓${c.reset} Model downloaded.`);
|
|
1833
|
+
}
|
|
1834
|
+
catch {
|
|
1835
|
+
console.log(`${c.yellow}⚠${c.reset} Model download failed — the engine will download it on first launch.`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
// 6. Create a launcher script
|
|
1840
|
+
console.log(`${c.cyan}Step 5/${c.reset} Creating launcher...`);
|
|
1841
|
+
const launcherPath = `${engineDir}\\start-notoken.bat`;
|
|
1842
|
+
const launcherContent = engine === "auto1111"
|
|
1843
|
+
? `@echo off\ncd /d "${engineDir}"\ncall venv\\Scripts\\activate\npython webui.py --api --listen\npause`
|
|
1844
|
+
: engine === "comfyui"
|
|
1845
|
+
? `@echo off\ncd /d "${engineDir}"\ncall venv\\Scripts\\activate\npython main.py --listen\npause`
|
|
1846
|
+
: `@echo off\ncd /d "${engineDir}"\ncall venv\\Scripts\\activate\npython entry_with_update.py\npause`;
|
|
1847
|
+
try {
|
|
1848
|
+
writeFileSync(launcherPath, launcherContent);
|
|
1849
|
+
}
|
|
1850
|
+
catch { }
|
|
1851
|
+
console.log(`${c.green}✓${c.reset} Installation complete!`);
|
|
1852
|
+
console.log(` ${c.bold}Location:${c.reset} ${engineDir}`);
|
|
1853
|
+
console.log(` ${c.bold}Launcher:${c.reset} ${launcherPath}`);
|
|
1854
|
+
return {
|
|
1855
|
+
success: true,
|
|
1856
|
+
message: `${c.green}✓${c.reset} ${engine} installed at ${engineDir}\n\n${c.dim}Start: double-click ${launcherPath}\nOr just say "generate a picture of a cat" — it will auto-start.${c.reset}`,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
// ─── Formatting ────────────────────────────────────────────────────────────
|
|
1860
|
+
function formatNoEngineMessage(prompt) {
|
|
1861
|
+
const gpu = detectGpu();
|
|
1862
|
+
const lines = [];
|
|
1863
|
+
lines.push(`${c.bold}${c.magenta}Image Generation${c.reset}\n`);
|
|
1864
|
+
lines.push(`You asked to generate: ${c.bold}"${prompt}"${c.reset}\n`);
|
|
1865
|
+
lines.push(`${c.bold}No local image generator detected.${c.reset} Here are your options:\n`);
|
|
1866
|
+
// Online services
|
|
1867
|
+
lines.push(`${c.bold}${c.cyan}Online Services (no install needed):${c.reset}`);
|
|
1868
|
+
lines.push(` ${c.bold}ChatGPT / DALL-E${c.reset} — generate via OpenAI API or chat.openai.com`);
|
|
1869
|
+
lines.push(` ${c.bold}Midjourney${c.reset} — Discord-based, subscription ($10-60/mo)`);
|
|
1870
|
+
lines.push(` ${c.bold}Leonardo.ai${c.reset} — web-based, free tier available`);
|
|
1871
|
+
lines.push(` ${c.bold}Stability AI${c.reset} — API access to Stable Diffusion models`);
|
|
1872
|
+
lines.push(` ${c.bold}Ideogram${c.reset} — great for text in images, free tier`);
|
|
1873
|
+
lines.push("");
|
|
1874
|
+
// Local install
|
|
1875
|
+
lines.push(`${c.bold}${c.green}Local Install (free, private, unlimited, works offline):${c.reset}`);
|
|
1876
|
+
if (gpu.hasNvidia) {
|
|
1877
|
+
lines.push(` ${c.green}✓ GPU detected: ${gpu.gpuName}${gpu.vram ? ` (${gpu.vram})` : ""}${c.reset}`);
|
|
1878
|
+
if (gpu.cudaVersion)
|
|
1879
|
+
lines.push(` ${c.green}✓ CUDA: ${gpu.cudaVersion}${c.reset}`);
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
lines.push(` ${c.yellow}⚠ No GPU detected — will use CPU (slower but works)${c.reset}`);
|
|
1883
|
+
}
|
|
1884
|
+
lines.push("");
|
|
1885
|
+
// Beginner-friendly options first
|
|
1886
|
+
lines.push(` ${c.bold}${c.cyan}Easiest (no technical setup):${c.reset}`);
|
|
1887
|
+
lines.push(` ${c.bold}1. Stability Matrix${c.reset} — All-in-one launcher, manages everything`);
|
|
1888
|
+
lines.push(` ${c.dim}Download: https://lykos.ai — Windows/Mac/Linux${c.reset}`);
|
|
1889
|
+
lines.push(` ${c.dim}Or: notoken install stability-matrix${c.reset}`);
|
|
1890
|
+
lines.push(` ${c.bold}2. Easy Diffusion${c.reset} — One-click installer, simple UI`);
|
|
1891
|
+
lines.push(` ${c.dim}Download: https://easydiffusion.github.io — Windows/Mac/Linux${c.reset}`);
|
|
1892
|
+
lines.push(` ${c.dim}Or: notoken install easy-diffusion${c.reset}`);
|
|
1893
|
+
lines.push(` ${c.bold}3. Fooocus${c.reset} — Simplest, Midjourney-like experience`);
|
|
1894
|
+
lines.push(` ${c.dim}Download: https://github.com/lllyasviel/Fooocus — Windows one-click package${c.reset}`);
|
|
1895
|
+
lines.push(` ${c.dim}Or: notoken install fooocus${c.reset}`);
|
|
1896
|
+
lines.push("");
|
|
1897
|
+
// Advanced options
|
|
1898
|
+
lines.push(` ${c.bold}${c.dim}Advanced (requires Python/Docker):${c.reset}`);
|
|
1899
|
+
lines.push(` ${c.dim}4. AUTOMATIC1111 — Most popular, full API: notoken install stable-diffusion${c.reset}`);
|
|
1900
|
+
lines.push(` ${c.dim}5. ComfyUI — Node-based workflows: notoken install comfyui${c.reset}`);
|
|
1901
|
+
lines.push(` ${c.dim}6. Docker — Containerized: notoken install stable-diffusion --docker${c.reset}`);
|
|
1902
|
+
lines.push("");
|
|
1903
|
+
lines.push(`${c.dim}After installing, say "generate a picture of a cat" — works offline, private, unlimited.${c.reset}`);
|
|
1904
|
+
return lines.join("\n");
|
|
1905
|
+
}
|
|
1906
|
+
function formatStartMessage(engine) {
|
|
1907
|
+
const lines = [];
|
|
1908
|
+
lines.push(`${c.bold}${engine.engine}${c.reset} is installed but not running.\n`);
|
|
1909
|
+
if (engine.engine === "auto1111") {
|
|
1910
|
+
lines.push(`Start it with:`);
|
|
1911
|
+
lines.push(` ${c.cyan}cd ${engine.path} && bash webui.sh --api --listen${c.reset}\n`);
|
|
1912
|
+
lines.push(`Or use notoken:`);
|
|
1913
|
+
lines.push(` ${c.cyan}notoken start stable-diffusion${c.reset}`);
|
|
1914
|
+
}
|
|
1915
|
+
else if (engine.engine === "comfyui") {
|
|
1916
|
+
lines.push(`Start it with:`);
|
|
1917
|
+
lines.push(` ${c.cyan}cd ${engine.path} && python3 main.py --listen${c.reset}`);
|
|
1918
|
+
}
|
|
1919
|
+
else if (engine.engine === "fooocus") {
|
|
1920
|
+
lines.push(`Start it with:`);
|
|
1921
|
+
lines.push(` ${c.cyan}cd ${engine.path} && python3 entry_with_update.py${c.reset}`);
|
|
1922
|
+
}
|
|
1923
|
+
return lines.join("\n");
|
|
1924
|
+
}
|
|
1925
|
+
export function formatImageEngineStatus(engines) {
|
|
1926
|
+
const lines = [];
|
|
1927
|
+
const gpu = detectGpu();
|
|
1928
|
+
lines.push(`${c.bold}Image Generation Engines${c.reset}\n`);
|
|
1929
|
+
if (gpu.hasNvidia) {
|
|
1930
|
+
lines.push(` ${c.green}GPU:${c.reset} ${gpu.gpuName}${gpu.vram ? ` (${gpu.vram})` : ""}${gpu.cudaVersion ? ` CUDA ${gpu.cudaVersion}` : ""}`);
|
|
1931
|
+
}
|
|
1932
|
+
else if (gpu.hasAmd) {
|
|
1933
|
+
lines.push(` ${c.green}GPU:${c.reset} AMD (ROCm)`);
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
lines.push(` ${c.yellow}GPU:${c.reset} None detected (CPU only)`);
|
|
1937
|
+
}
|
|
1938
|
+
lines.push("");
|
|
1939
|
+
for (const e of engines) {
|
|
1940
|
+
const icon = e.running ? `${c.green}⬤${c.reset}` :
|
|
1941
|
+
e.installed ? `${c.yellow}⬤${c.reset}` :
|
|
1942
|
+
`${c.dim}○${c.reset}`;
|
|
1943
|
+
const status = e.running ? `${c.green}running${c.reset}` :
|
|
1944
|
+
e.installed ? `${c.yellow}installed (stopped)${c.reset}` :
|
|
1945
|
+
`${c.dim}not installed${c.reset}`;
|
|
1946
|
+
const url = e.url ? ` ${c.dim}${e.url}${c.reset}` : "";
|
|
1947
|
+
const plat = e.platform ? ` ${c.dim}[${e.platform}]${c.reset}` : "";
|
|
1948
|
+
const conflict = e.portConflict ? ` ${c.red}⚠ PORT CONFLICT${c.reset}` : "";
|
|
1949
|
+
const pid = e.pid ? ` ${c.dim}(pid ${e.pid})${c.reset}` : "";
|
|
1950
|
+
lines.push(` ${icon} ${c.bold}${e.engine}${c.reset}${plat} — ${status}${url}${pid}${conflict}`);
|
|
1951
|
+
}
|
|
1952
|
+
// Check for port conflicts
|
|
1953
|
+
const conflicts = engines.filter(e => e.portConflict);
|
|
1954
|
+
if (conflicts.length > 0) {
|
|
1955
|
+
lines.push("");
|
|
1956
|
+
lines.push(` ${c.red}⚠ Port conflict detected!${c.reset} Multiple engines trying to use port 7860.`);
|
|
1957
|
+
lines.push(` ${c.dim}Stop one with: "stop sd" or stop the Windows engine from Stability Matrix.${c.reset}`);
|
|
1958
|
+
}
|
|
1959
|
+
// Explain what's currently being used
|
|
1960
|
+
const running = engines.find(e => e.running);
|
|
1961
|
+
const installed = engines.find(e => e.installed && e.engine !== "docker");
|
|
1962
|
+
lines.push("");
|
|
1963
|
+
if (running) {
|
|
1964
|
+
lines.push(` ${c.bold}Currently using:${c.reset} ${c.green}${running.engine}${c.reset} (local, ${running.url})`);
|
|
1965
|
+
if (running.path)
|
|
1966
|
+
lines.push(` ${c.dim}Location: ${running.path}${c.reset}`);
|
|
1967
|
+
}
|
|
1968
|
+
else if (installed) {
|
|
1969
|
+
lines.push(` ${c.bold}Currently using:${c.reset} ${c.yellow}${installed.engine} installed but stopped${c.reset} — will auto-start on generate`);
|
|
1970
|
+
if (installed.path) {
|
|
1971
|
+
const size = tryExec(`du -sh "${installed.path}" 2>/dev/null`)?.split("\t")[0] ?? "?";
|
|
1972
|
+
lines.push(` ${c.dim}Location: ${installed.path} (${size})${c.reset}`);
|
|
1973
|
+
// Show which drive/partition
|
|
1974
|
+
const dfLine = tryExec(`df -h "${installed.path}" 2>/dev/null | tail -1`);
|
|
1975
|
+
if (dfLine) {
|
|
1976
|
+
const parts = dfLine.split(/\s+/);
|
|
1977
|
+
const mount = parts[parts.length - 1];
|
|
1978
|
+
const avail = parts[3];
|
|
1979
|
+
const pct = parts[4];
|
|
1980
|
+
lines.push(` ${c.dim}Drive: ${mount} — ${avail} free (${pct} used)${c.reset}`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
lines.push(` ${c.bold}Currently using:${c.reset} ${c.cyan}Cloud API (Pollinations.ai)${c.reset} — free, no install needed`);
|
|
1986
|
+
lines.push(` ${c.dim}Powered by Stable Diffusion via Pollinations. Images are generated on their servers.${c.reset}`);
|
|
1987
|
+
lines.push(` ${c.dim}For private/offline generation, install a local engine above.${c.reset}`);
|
|
1988
|
+
}
|
|
1989
|
+
// Check if a previously-started engine is now ready
|
|
1990
|
+
if (installed && !running) {
|
|
1991
|
+
// Quick check if it came up since we last looked
|
|
1992
|
+
const a1Check = !!tryExec("curl -sf --max-time 2 http://localhost:7860/sdapi/v1/sd-models 2>/dev/null");
|
|
1993
|
+
const comfyCheck = !!tryExec("curl -sf --max-time 2 http://localhost:8188/system_stats 2>/dev/null");
|
|
1994
|
+
const edCheck = !!tryExec("curl -sf --max-time 2 http://localhost:9000/ping 2>/dev/null");
|
|
1995
|
+
if (a1Check) {
|
|
1996
|
+
lines.push(` ${c.green}✓ auto1111 just became ready at http://localhost:7860!${c.reset}`);
|
|
1997
|
+
lines.push(` ${c.bold}Say "generate a picture of a cat" to use it.${c.reset}`);
|
|
1998
|
+
}
|
|
1999
|
+
else if (comfyCheck) {
|
|
2000
|
+
lines.push(` ${c.green}✓ ComfyUI just became ready at http://localhost:8188!${c.reset}`);
|
|
2001
|
+
}
|
|
2002
|
+
else if (edCheck) {
|
|
2003
|
+
lines.push(` ${c.green}✓ Easy Diffusion just became ready at http://localhost:9000!${c.reset}`);
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
// Check if the process is at least running
|
|
2007
|
+
const sdProcess = tryExec("ps aux 2>/dev/null | grep -E 'webui\\.py|main\\.py|entry_with_update' | grep -v grep");
|
|
2008
|
+
if (sdProcess) {
|
|
2009
|
+
lines.push(` ${c.yellow}⏳ Engine process is running — still loading (model download may be in progress)${c.reset}`);
|
|
2010
|
+
lines.push(` ${c.dim}Check again in a minute: "image status"${c.reset}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
// Docker data location warning
|
|
2015
|
+
const dockerEngine = engines.find(e => e.engine === "docker" && e.installed);
|
|
2016
|
+
if (dockerEngine) {
|
|
2017
|
+
const dockerRoot = tryExec("docker info 2>/dev/null | grep 'Docker Root Dir' | awk '{print $NF}'");
|
|
2018
|
+
if (dockerRoot) {
|
|
2019
|
+
const dockerDrive = getDriveInfo(dockerRoot);
|
|
2020
|
+
if (dockerDrive) {
|
|
2021
|
+
lines.push(`\n ${c.dim}Docker data: ${dockerRoot} (${dockerDrive.mount} — ${dockerDrive.freeGB}GB free)${c.reset}`);
|
|
2022
|
+
if (dockerDrive.freeGB < 10) {
|
|
2023
|
+
lines.push(` ${c.yellow}⚠ Docker drive is low on space!${c.reset}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
lines.push(`\n ${c.dim}Images saved to: ${OUTPUT_DIR}${c.reset}`);
|
|
2029
|
+
lines.push(` ${c.dim}Install base: ${getInstallBase()}${c.reset}`);
|
|
2030
|
+
return lines.join("\n");
|
|
2031
|
+
}
|