mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
fixes for windows monitoring, fix comfyui to run on windows
This commit is contained in:
parent
329e191a49
commit
de7ea100db
8 changed files with 527 additions and 419 deletions
|
|
@ -72,6 +72,9 @@ export const GITHUB_API = {
|
|||
get COMFYUI_DOWNLOAD_URL() {
|
||||
return `https://github.com/${this.COMFYUI_REPO}/archive/refs/heads/master.zip`;
|
||||
},
|
||||
get COMFYUI_LATEST_COMMIT_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.COMFYUI_REPO}/commits/master`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ASSET_SUFFIXES = {
|
||||
|
|
|
|||
|
|
@ -1,41 +1,23 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import { useFrontendPreferenceStore } from '@/stores/frontendPreference';
|
||||
import {
|
||||
getAvailableInterfaceOptions,
|
||||
getDefaultInterfaceTab,
|
||||
getServerInterfaceInfo,
|
||||
} from '@/utils/interface';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
||||
export function useFrontendPreference() {
|
||||
const [frontendPreference, setFrontendPreference] =
|
||||
useState<FrontendPreference>('koboldcpp');
|
||||
const { frontendPreference, setFrontendPreference, loadFromConfig } =
|
||||
useFrontendPreferenceStore();
|
||||
|
||||
useEffect(() => {
|
||||
const loadPreference = async () => {
|
||||
await safeExecute(async () => {
|
||||
const preference = (await window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
)) as FrontendPreference;
|
||||
|
||||
setFrontendPreference(preference || 'koboldcpp');
|
||||
}, 'Error loading frontend preference:');
|
||||
};
|
||||
|
||||
loadPreference();
|
||||
}, []);
|
||||
|
||||
const updateFrontendPreference = async (preference: FrontendPreference) => {
|
||||
setFrontendPreference(preference);
|
||||
await safeExecute(async () => {
|
||||
await window.electronAPI.config.set('frontendPreference', preference);
|
||||
}, 'Error saving frontend preference:');
|
||||
};
|
||||
loadFromConfig();
|
||||
}, [loadFromConfig]);
|
||||
|
||||
return {
|
||||
frontendPreference,
|
||||
setFrontendPreference: updateFrontendPreference,
|
||||
setFrontendPreference,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,83 @@ import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
|||
import { getGPUData } from '@/utils/node/gpu';
|
||||
import { getUvEnvironment } from './dependencies';
|
||||
|
||||
interface ComfyUIVersionInfo {
|
||||
sha: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
async function getLatestComfyUIVersion(): Promise<ComfyUIVersionInfo | null> {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API.COMFYUI_LATEST_COMMIT_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ComfyUI version: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
sha: data.sha,
|
||||
date: data.commit.committer.date,
|
||||
};
|
||||
} catch (error) {
|
||||
logError(
|
||||
'Failed to fetch latest ComfyUI version',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentComfyUIVersion(
|
||||
workspaceDir: string
|
||||
): Promise<ComfyUIVersionInfo | null> {
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
const versionFile = join(workspaceDir, '.version.json');
|
||||
const content = await fs.readFile(versionFile, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveComfyUIVersion(
|
||||
workspaceDir: string,
|
||||
version: ComfyUIVersionInfo
|
||||
): Promise<void> {
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
const versionFile = join(workspaceDir, '.version.json');
|
||||
await fs.writeFile(versionFile, JSON.stringify(version, null, 2));
|
||||
} catch (error) {
|
||||
logError(
|
||||
'Failed to save ComfyUI version info',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldUpdateComfyUI(workspaceDir: string): Promise<boolean> {
|
||||
const current = await getCurrentComfyUIVersion(workspaceDir);
|
||||
const latest = await getLatestComfyUIVersion();
|
||||
|
||||
if (!current || !latest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return current.sha !== latest.sha;
|
||||
}
|
||||
|
||||
let comfyUIProcess: ChildProcess | null = null;
|
||||
|
||||
function getPythonPath(workspaceDir: string): string {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const pythonExecutable = isWindows ? 'python.exe' : 'python';
|
||||
const scriptsDir = isWindows ? 'Scripts' : 'bin';
|
||||
return join(workspaceDir, '.venv', scriptsDir, pythonExecutable);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
void cleanup();
|
||||
});
|
||||
|
|
@ -24,6 +99,19 @@ process.on('SIGTERM', () => {
|
|||
void cleanup();
|
||||
});
|
||||
|
||||
async function shouldForceCPUMode(): Promise<boolean> {
|
||||
try {
|
||||
const gpus = await getGPUData();
|
||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPyTorchInstallArgs(pythonPath: string) {
|
||||
const args = [
|
||||
'pip',
|
||||
|
|
@ -39,8 +127,9 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
|||
const gpus = await getGPUData();
|
||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (hasAMD) {
|
||||
if (hasAMD && !isWindows) {
|
||||
sendKoboldOutput(
|
||||
'AMD GPU detected, installing PyTorch with ROCm support...'
|
||||
);
|
||||
|
|
@ -51,9 +140,15 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
|||
);
|
||||
args.push('--extra-index-url', 'https://download.pytorch.org/whl/cu121');
|
||||
} else {
|
||||
sendKoboldOutput(
|
||||
'No dedicated GPU detected, installing CPU-only PyTorch...'
|
||||
);
|
||||
if (hasAMD && isWindows) {
|
||||
sendKoboldOutput(
|
||||
'AMD GPU detected on Windows, installing CPU PyTorch (ROCm not available on Windows)...'
|
||||
);
|
||||
} else {
|
||||
sendKoboldOutput(
|
||||
'No dedicated GPU detected, installing CPU-only PyTorch...'
|
||||
);
|
||||
}
|
||||
args.push('--index-url', 'https://download.pytorch.org/whl/cpu');
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -63,6 +158,332 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
|||
return args;
|
||||
}
|
||||
|
||||
async function downloadAndExtractComfyUI(workspaceDir: string): Promise<void> {
|
||||
sendKoboldOutput('Downloading ComfyUI...');
|
||||
|
||||
const response = await fetch(GITHUB_API.COMFYUI_DOWNLOAD_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ComfyUI: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
const { createWriteStream } = await import('fs');
|
||||
|
||||
const zipPath = path.join(workspaceDir, 'comfyui.zip');
|
||||
const stream = createWriteStream(zipPath);
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const pump = async (): Promise<void> => {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
stream.end();
|
||||
return;
|
||||
}
|
||||
stream.write(Buffer.from(value));
|
||||
return pump();
|
||||
};
|
||||
await pump();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on('finish', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const zipStats = await fs.stat(zipPath);
|
||||
sendKoboldOutput(
|
||||
`ComfyUI downloaded (${Math.round(zipStats.size / 1024 / 1024)}MB), extracting...`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile!.readEntry();
|
||||
zipfile!.on('entry', (entry) => {
|
||||
const entryPath = entry.fileName;
|
||||
const destPath = path.join(
|
||||
workspaceDir,
|
||||
entryPath.replace(/^[^/]+\//, '')
|
||||
);
|
||||
|
||||
if (/\/$/.test(entryPath)) {
|
||||
fs.mkdir(destPath, { recursive: true })
|
||||
.then(() => zipfile!.readEntry())
|
||||
.catch(reject);
|
||||
} else {
|
||||
zipfile!.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdir(path.dirname(destPath), { recursive: true })
|
||||
.then(() => {
|
||||
const writeStream = createWriteStream(destPath);
|
||||
readStream!.pipe(writeStream);
|
||||
writeStream.on('close', () => zipfile!.readEntry());
|
||||
writeStream.on('error', reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
zipfile!.on('end', () => {
|
||||
fs.unlink(zipPath)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sendKoboldOutput('ComfyUI extracted successfully');
|
||||
|
||||
const latestVersion = await getLatestComfyUIVersion();
|
||||
if (latestVersion) {
|
||||
await saveComfyUIVersion(workspaceDir, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
async function installDependencies(
|
||||
workspaceDir: string,
|
||||
env: Record<string, string | undefined>
|
||||
): Promise<void> {
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
sendKoboldOutput('Installing ComfyUI dependencies...');
|
||||
|
||||
const requirementsPath = join(workspaceDir, 'requirements.txt');
|
||||
const requirementsExists = await fs
|
||||
.access(requirementsPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!requirementsExists) {
|
||||
throw new Error('requirements.txt not found in ComfyUI directory');
|
||||
}
|
||||
|
||||
const pythonPath = getPythonPath(workspaceDir);
|
||||
const torchInstallArgs = await getPyTorchInstallArgs(pythonPath);
|
||||
|
||||
const torchInstallProcess = spawn('uv', torchInstallArgs, {
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
torchInstallProcess.on('exit', (torchCode: number | null) => {
|
||||
if (torchCode === 0) {
|
||||
sendKoboldOutput(
|
||||
'PyTorch installed successfully, installing remaining dependencies...'
|
||||
);
|
||||
|
||||
const pipInstallProcess = spawn(
|
||||
'uv',
|
||||
[
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
getPythonPath(workspaceDir),
|
||||
'-r',
|
||||
requirementsPath,
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
pipInstallProcess.on('exit', (pipCode: number | null) => {
|
||||
if (pipCode === 0) {
|
||||
sendKoboldOutput('ComfyUI dependencies installed successfully');
|
||||
|
||||
const verifyProcess = spawn(
|
||||
getPythonPath(workspaceDir),
|
||||
[
|
||||
'-c',
|
||||
'import torch; print("PyTorch version:", torch.__version__)',
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
verifyProcess.on('exit', (verifyCode: number | null) => {
|
||||
if (verifyCode === 0) {
|
||||
sendKoboldOutput('ComfyUI installation verified successfully');
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`ComfyUI verification failed with code ${verifyCode}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
verifyProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (verifyProcess.stdout) {
|
||||
verifyProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyProcess.stderr) {
|
||||
verifyProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(
|
||||
`Verify Error: ${data.toString('utf8')}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Dependency installation failed with code ${pipCode}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
pipInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (pipInstallProcess.stdout) {
|
||||
pipInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (pipInstallProcess.stderr) {
|
||||
pipInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`PyTorch installation failed with code ${torchCode}`));
|
||||
}
|
||||
});
|
||||
|
||||
torchInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (torchInstallProcess.stdout) {
|
||||
torchInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (torchInstallProcess.stderr) {
|
||||
torchInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function installComfyUI(workspaceDir: string): Promise<void> {
|
||||
const env = await getUvEnvironment();
|
||||
|
||||
sendKoboldOutput('Creating virtual environment...');
|
||||
|
||||
const installProcess = spawn(
|
||||
'uv',
|
||||
['venv', '--python', '3.11', join(workspaceDir, '.venv')],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
installProcess.on('exit', async (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
await downloadAndExtractComfyUI(workspaceDir);
|
||||
await installDependencies(workspaceDir, env);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Virtual environment creation failed with code ${code}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
installProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (installProcess.stdout) {
|
||||
installProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (installProcess.stderr) {
|
||||
installProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateComfyUI(workspaceDir: string): Promise<void> {
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
const backupDir = join(workspaceDir, 'backup');
|
||||
await ensureDir(backupDir);
|
||||
|
||||
const filesToBackup = ['main.py', 'requirements.txt'];
|
||||
const backupPromises = filesToBackup.map(async (file) => {
|
||||
const srcPath = join(workspaceDir, file);
|
||||
const destPath = join(backupDir, file);
|
||||
try {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(backupPromises);
|
||||
|
||||
try {
|
||||
await downloadAndExtractComfyUI(workspaceDir);
|
||||
sendKoboldOutput('ComfyUI updated successfully');
|
||||
|
||||
await fs.rm(backupDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
sendKoboldOutput('Update failed, restoring backup...');
|
||||
|
||||
const restorePromises = filesToBackup.map(async (file) => {
|
||||
const srcPath = join(backupDir, file);
|
||||
const destPath = join(workspaceDir, file);
|
||||
try {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(restorePromises);
|
||||
await fs.rm(backupDir, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureComfyUIInstalled() {
|
||||
const installDir = getInstallDir();
|
||||
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
||||
|
|
@ -74,325 +495,21 @@ async function ensureComfyUIInstalled() {
|
|||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const needsUpdate =
|
||||
isComfyUIInstalled && (await shouldUpdateComfyUI(comfyUIWorkspace));
|
||||
|
||||
if (!isComfyUIInstalled) {
|
||||
sendKoboldOutput('ComfyUI not found, installing via uv...');
|
||||
|
||||
const env = await getUvEnvironment();
|
||||
|
||||
sendKoboldOutput(
|
||||
'Creating virtual environment and installing ComfyUI dependencies...'
|
||||
);
|
||||
|
||||
const installProcess = spawn(
|
||||
'uv',
|
||||
['venv', '--python', '3.11', join(comfyUIWorkspace, '.venv')],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
installProcess.on('exit', async (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
sendKoboldOutput(
|
||||
'Virtual environment created, downloading ComfyUI...'
|
||||
);
|
||||
|
||||
const response = await fetch(GITHUB_API.COMFYUI_DOWNLOAD_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to download ComfyUI: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
const { createWriteStream } = await import('fs');
|
||||
|
||||
const zipPath = path.join(comfyUIWorkspace, 'comfyui.zip');
|
||||
const stream = createWriteStream(zipPath);
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const pump = async (): Promise<void> => {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
stream.end();
|
||||
return;
|
||||
}
|
||||
stream.write(Buffer.from(value));
|
||||
return pump();
|
||||
};
|
||||
await pump();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on('finish', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const zipStats = await fs.stat(zipPath);
|
||||
sendKoboldOutput(
|
||||
`ComfyUI downloaded (${Math.round(zipStats.size / 1024 / 1024)}MB), extracting...`
|
||||
);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zipfile) {
|
||||
reject(new Error('Failed to open zip file'));
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
|
||||
zipfile.on('entry', (entry) => {
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
zipfile.readEntry();
|
||||
} else {
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readStream) {
|
||||
reject(new Error('Failed to create read stream'));
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = entry.fileName.replace(/^[^/]+\//, '');
|
||||
const outputPath = path.join(
|
||||
comfyUIWorkspace,
|
||||
fileName
|
||||
);
|
||||
|
||||
fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||
.then(() => {
|
||||
const writeStream = createWriteStream(outputPath);
|
||||
readStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('close', () => {
|
||||
zipfile.readEntry();
|
||||
});
|
||||
|
||||
writeStream.on('error', reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
zipfile.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
zipfile.on('error', reject);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await fs.unlink(zipPath);
|
||||
|
||||
sendKoboldOutput('ComfyUI extracted, installing dependencies...');
|
||||
|
||||
const requirementsPath = join(
|
||||
comfyUIWorkspace,
|
||||
'requirements.txt'
|
||||
);
|
||||
const requirementsExists = await fs
|
||||
.access(requirementsPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!requirementsExists) {
|
||||
throw new Error(
|
||||
'requirements.txt not found in ComfyUI directory'
|
||||
);
|
||||
}
|
||||
|
||||
const pythonPath = join(
|
||||
comfyUIWorkspace,
|
||||
'.venv',
|
||||
'bin',
|
||||
'python'
|
||||
);
|
||||
const torchInstallArgs = await getPyTorchInstallArgs(pythonPath);
|
||||
|
||||
const torchInstallProcess = spawn('uv', torchInstallArgs, {
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
});
|
||||
|
||||
torchInstallProcess.on('exit', (torchCode: number | null) => {
|
||||
if (torchCode === 0) {
|
||||
sendKoboldOutput(
|
||||
'PyTorch with ROCm installed, installing remaining dependencies...'
|
||||
);
|
||||
|
||||
const pipInstallProcess = spawn(
|
||||
'uv',
|
||||
[
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
||||
'-r',
|
||||
requirementsPath,
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
pipInstallProcess.on('exit', async (pipCode) => {
|
||||
if (pipCode === 0) {
|
||||
sendKoboldOutput(
|
||||
'Dependencies installation completed, verifying...'
|
||||
);
|
||||
|
||||
const verifyProcess = spawn(
|
||||
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
||||
[
|
||||
'-c',
|
||||
'import einops; print("einops found:", einops.__version__)',
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
verifyProcess.on('exit', (verifyCode) => {
|
||||
if (verifyCode === 0) {
|
||||
sendKoboldOutput('ComfyUI installed successfully!');
|
||||
resolve();
|
||||
} else {
|
||||
sendKoboldOutput(
|
||||
'Warning: einops verification failed, but continuing...'
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
if (verifyProcess.stdout) {
|
||||
verifyProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(
|
||||
`Verify: ${data.toString('utf8')}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyProcess.stderr) {
|
||||
verifyProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(
|
||||
`Verify Error: ${data.toString('utf8')}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Dependency installation failed with code ${pipCode}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
pipInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (pipInstallProcess.stdout) {
|
||||
pipInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (pipInstallProcess.stderr) {
|
||||
pipInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`PyTorch installation failed with code ${torchCode}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
torchInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (torchInstallProcess.stdout) {
|
||||
torchInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (torchInstallProcess.stderr) {
|
||||
torchInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to extract ComfyUI: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to download ComfyUI: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Virtual environment creation failed with code ${code}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
installProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (installProcess.stdout) {
|
||||
installProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (installProcess.stderr) {
|
||||
installProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
sendKoboldOutput('ComfyUI not found, installing...');
|
||||
await installComfyUI(comfyUIWorkspace);
|
||||
} else if (needsUpdate) {
|
||||
sendKoboldOutput('ComfyUI update available, updating...');
|
||||
await updateComfyUI(comfyUIWorkspace);
|
||||
} else {
|
||||
sendKoboldOutput('ComfyUI found in workspace');
|
||||
sendKoboldOutput('ComfyUI found in workspace and up to date');
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForComfyUIToStart() {
|
||||
sendKoboldOutput('Waiting for ComfyUI to start...');
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkForOutput = (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
|
|
@ -420,106 +537,80 @@ export async function startFrontend(args: string[]) {
|
|||
name: 'comfyui',
|
||||
port: COMFYUI.PORT,
|
||||
};
|
||||
const {
|
||||
host: koboldHost,
|
||||
port: koboldPort,
|
||||
isImageMode,
|
||||
} = parseKoboldConfig(args);
|
||||
const { host: koboldHost, port: koboldPort } = parseKoboldConfig(args);
|
||||
|
||||
if (!isImageMode) {
|
||||
throw new Error('ComfyUI requires image generation mode to be enabled');
|
||||
}
|
||||
|
||||
const [, appVersion] = await Promise.all([stopFrontend(), getAppVersion()]);
|
||||
|
||||
sendKoboldOutput(
|
||||
`Preparing ComfyUI to connect at ${koboldHost}:${koboldPort} for image generation...`
|
||||
);
|
||||
|
||||
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||
sendKoboldOutput(`Starting ComfyUI frontend on port ${config.port}...`);
|
||||
|
||||
await ensureComfyUIInstalled();
|
||||
|
||||
const installDir = getInstallDir();
|
||||
const comfyUIDataDir = join(installDir, 'comfyui-data');
|
||||
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
||||
const comfyUIMainPath = join(comfyUIWorkspace, 'main.py');
|
||||
|
||||
await Promise.all([ensureDir(comfyUIDataDir), ensureDir(comfyUIWorkspace)]);
|
||||
const pythonPath = getPythonPath(comfyUIWorkspace);
|
||||
|
||||
const comfyUIArgs = [
|
||||
comfyUIMainPath,
|
||||
join(comfyUIWorkspace, 'main.py'),
|
||||
'--port',
|
||||
config.port.toString(),
|
||||
'--disable-auto-launch',
|
||||
'--listen',
|
||||
'0.0.0.0',
|
||||
'--enable-cors-header',
|
||||
];
|
||||
|
||||
sendKoboldOutput('Starting ComfyUI directly...');
|
||||
const forceCPU = await shouldForceCPUMode();
|
||||
if (forceCPU) {
|
||||
comfyUIArgs.push('--cpu');
|
||||
sendKoboldOutput(
|
||||
'Forcing ComfyUI to use CPU mode (no compatible GPU acceleration available)'
|
||||
);
|
||||
}
|
||||
|
||||
const envConfig: Record<string, string> = {
|
||||
KOBOLDCPP_API_URL: `${koboldUrl}/comfyapi/v1`,
|
||||
USER_AGENT: `Gerbil/${appVersion}`,
|
||||
};
|
||||
if (koboldHost && koboldPort) {
|
||||
sendKoboldOutput(
|
||||
`Connecting to KoboldCpp at ${koboldHost}:${koboldPort}`
|
||||
);
|
||||
}
|
||||
|
||||
const env = await getUvEnvironment();
|
||||
Object.assign(env, envConfig);
|
||||
sendKoboldOutput(`Running ComfyUI with Python: ${pythonPath}`);
|
||||
sendKoboldOutput(`ComfyUI args: ${comfyUIArgs.join(' ')}`);
|
||||
|
||||
const pythonPath = join(comfyUIWorkspace, '.venv', 'bin', 'python');
|
||||
comfyUIProcess = spawn(pythonPath, comfyUIArgs, {
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
cwd: comfyUIWorkspace,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
const version = await getAppVersion();
|
||||
sendKoboldOutput(`ComfyUI started via Gerbil v${version}`);
|
||||
|
||||
if (comfyUIProcess.stdout) {
|
||||
comfyUIProcess.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const output = data.toString('utf8');
|
||||
sendKoboldOutput(output, true);
|
||||
} catch (error) {
|
||||
logError('Error processing stdout data:', error as Error);
|
||||
}
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (comfyUIProcess.stderr) {
|
||||
comfyUIProcess.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const output = data.toString('utf8');
|
||||
sendKoboldOutput(output, true);
|
||||
} catch (error) {
|
||||
logError('Error processing stderr data:', error as Error);
|
||||
}
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
comfyUIProcess.on('exit', (code: number | null, signal: string | null) => {
|
||||
const message = signal
|
||||
? `ComfyUI terminated with signal ${signal}`
|
||||
: `ComfyUI exited with code ${code}`;
|
||||
sendKoboldOutput(message);
|
||||
if (code === null && signal) {
|
||||
sendKoboldOutput(`ComfyUI process terminated by signal: ${signal}`);
|
||||
} else {
|
||||
sendKoboldOutput(`ComfyUI process exited with code: ${code}`);
|
||||
}
|
||||
comfyUIProcess = null;
|
||||
});
|
||||
|
||||
comfyUIProcess.on('error', (error) => {
|
||||
logError('ComfyUI process error:', error);
|
||||
sendKoboldOutput(`ComfyUI error: ${error.message}`);
|
||||
comfyUIProcess.on('error', (error: Error) => {
|
||||
logError('ComfyUI process error', error);
|
||||
comfyUIProcess = null;
|
||||
});
|
||||
|
||||
await waitForComfyUIToStart();
|
||||
|
||||
sendKoboldOutput(`ComfyUI is ready and auto-configured!`);
|
||||
sendKoboldOutput(`Access ComfyUI at: http://localhost:${config.port}`);
|
||||
sendKoboldOutput(
|
||||
`Image Generation API: ${koboldUrl}/comfyapi/v1 (auto-configured)`
|
||||
);
|
||||
} catch (error) {
|
||||
logError('Failed to start ComfyUI frontend:', error as Error);
|
||||
sendKoboldOutput(`Failed to start ComfyUI: ${(error as Error).message}`);
|
||||
comfyUIProcess = null;
|
||||
logError(
|
||||
'Failed to start ComfyUI',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -527,20 +618,22 @@ export async function startFrontend(args: string[]) {
|
|||
export async function stopFrontend() {
|
||||
if (comfyUIProcess) {
|
||||
sendKoboldOutput('Stopping ComfyUI...');
|
||||
|
||||
try {
|
||||
await terminateProcess(comfyUIProcess, {
|
||||
logError: (message, error) => logError(message, error),
|
||||
});
|
||||
await terminateProcess(comfyUIProcess);
|
||||
comfyUIProcess = null;
|
||||
sendKoboldOutput('ComfyUI stopped');
|
||||
} catch (error) {
|
||||
logError('Error stopping ComfyUI:', error as Error);
|
||||
logError(
|
||||
'Error stopping ComfyUI',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
comfyUIProcess = null;
|
||||
}
|
||||
|
||||
comfyUIProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
await stopFrontend();
|
||||
if (comfyUIProcess) {
|
||||
await stopFrontend();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { logError } from '@/main/modules/logging';
|
||||
import { readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||
import { getConfigDir } from '@/utils/node/path';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { nativeTheme } from 'electron';
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
|
||||
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -353,8 +353,7 @@ function parseClInfoOutput(output: string) {
|
|||
const deviceName = findDeviceNameInClInfo(lines, i);
|
||||
|
||||
if (deviceName && currentPlatform) {
|
||||
const deviceLabel = `${formatDeviceName(deviceName)} (${currentPlatform})`;
|
||||
devices.push(deviceLabel);
|
||||
devices.push(formatDeviceName(deviceName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ async function createUvProcess(args: string[], env?: Record<string, string>) {
|
|||
}
|
||||
|
||||
async function waitForOpenWebUIToStart() {
|
||||
sendKoboldOutput('Waiting for Open WebUI to start...');
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkForOutput = (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
|
|
|
|||
33
src/stores/frontendPreference.ts
Normal file
33
src/stores/frontendPreference.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { create } from 'zustand';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
||||
interface FrontendPreferenceState {
|
||||
frontendPreference: FrontendPreference;
|
||||
setFrontendPreference: (preference: FrontendPreference) => void;
|
||||
loadFromConfig: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useFrontendPreferenceStore = create<FrontendPreferenceState>()(
|
||||
(set) => ({
|
||||
frontendPreference: 'koboldcpp',
|
||||
|
||||
setFrontendPreference: (preference: FrontendPreference) => {
|
||||
set({ frontendPreference: preference });
|
||||
|
||||
safeExecute(async () => {
|
||||
await window.electronAPI.config.set('frontendPreference', preference);
|
||||
}, 'Error saving frontend preference:');
|
||||
},
|
||||
|
||||
loadFromConfig: async () => {
|
||||
await safeExecute(async () => {
|
||||
const preference = (await window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
)) as FrontendPreference;
|
||||
|
||||
set({ frontendPreference: preference || 'koboldcpp' });
|
||||
}, 'Error loading frontend preference:');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
@ -182,7 +182,7 @@ foreach ($gpu in $gpus) {
|
|||
setTimeout(() => {
|
||||
powershell.kill('SIGTERM');
|
||||
resolve([]);
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue