feat: develop Electron wrapper, safe IPC bridge, and subprocess spawn pipelines
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 820,
|
||||
minWidth: 1000,
|
||||
minHeight: 700,
|
||||
frame: false, // Sleek frameless design
|
||||
transparent: false, // Set to false to avoid rendering bugs on some Windows setups
|
||||
backgroundColor: '#0a0a0f', // Dark baseline background
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
|
||||
// Open DevTools if running in dev (optional)
|
||||
// mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
// Helper: Get Python Path
|
||||
function getPythonPaths() {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const venvPython = isWindows
|
||||
? path.join(__dirname, '.venv', 'Scripts', 'python.exe')
|
||||
: path.join(__dirname, '.venv', 'bin', 'python');
|
||||
|
||||
const systemPython = 'python';
|
||||
const hasVenv = fs.existsSync(venvPython);
|
||||
|
||||
return { venvPython, systemPython, hasVenv };
|
||||
}
|
||||
|
||||
// IPC: Check Python Setup Status
|
||||
ipcMain.handle('check-python-setup', async () => {
|
||||
const { hasVenv } = getPythonPaths();
|
||||
return { hasVenv };
|
||||
});
|
||||
|
||||
// IPC: Run Setup
|
||||
ipcMain.handle('run-python-setup', async (event) => {
|
||||
const { systemPython } = getPythonPaths();
|
||||
const setupScript = path.join(__dirname, 'backend', 'setup.py');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[Main Process] Spawning setup: ${systemPython} ${setupScript}`);
|
||||
// Spawn host python to run setup.py
|
||||
const process = spawn(systemPython, [setupScript], { shell: true });
|
||||
|
||||
let stderrData = '';
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`[Setup Output] ${output.trim()}`);
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
event.sender.send('setup-progress', line.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
stderrData += errStr;
|
||||
console.error(`[Setup Error] ${errStr.trim()}`);
|
||||
event.sender.send('setup-progress', `[ERROR] ${errStr.trim()}`);
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`[Main Process] Setup completed with exit code ${code}`);
|
||||
resolve({ code, error: stderrData });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// IPC: Run Background Removal Process
|
||||
ipcMain.handle('remove-background', async (event, { inputPath, outputPath }) => {
|
||||
const { venvPython, hasVenv } = getPythonPaths();
|
||||
|
||||
if (!hasVenv) {
|
||||
return { success: false, error: 'Python virtual environment not set up. Please run setup first.' };
|
||||
}
|
||||
|
||||
const processScript = path.join(__dirname, 'backend', 'process.py');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[Main Process] Spawning background removal: ${venvPython} ${processScript}`);
|
||||
// Spawn venv python to run process.py
|
||||
const args = ['--input', inputPath, '--output', outputPath];
|
||||
const process = spawn(venvPython, [processScript, ...args], { shell: true });
|
||||
|
||||
let stderrData = '';
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`[Python Output] ${output.trim()}`);
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
event.sender.send('process-progress', line.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
stderrData += errStr;
|
||||
console.error(`[Python Error] ${errStr.trim()}`);
|
||||
event.sender.send('process-progress', `[ERROR] ${errStr.trim()}`);
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`[Main Process] Process completed with exit code ${code}`);
|
||||
if (code === 0) {
|
||||
resolve({ success: true, outputPath });
|
||||
} else {
|
||||
resolve({ success: false, error: stderrData || `Process failed with exit code ${code}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// IPC: Select Image File Dialog
|
||||
ipcMain.handle('select-image', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'webp', 'bmp'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return result.filePaths[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// IPC: Get Temp Output File Path
|
||||
ipcMain.handle('get-temp-path', async (event, fileName) => {
|
||||
const tempDir = app.getPath('temp');
|
||||
// Ensure unique name to avoid cache issues
|
||||
const uniqueName = `birefnet_${Date.now()}_${fileName}`;
|
||||
return path.join(tempDir, uniqueName);
|
||||
});
|
||||
|
||||
// IPC: Export Image Dialog
|
||||
ipcMain.handle('export-image', async (event, { base64Data, defaultName }) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
defaultPath: defaultName || 'processed_image.png',
|
||||
filters: [
|
||||
{ name: 'PNG Image', extensions: ['png'] },
|
||||
{ name: 'JPEG Image', extensions: ['jpg', 'jpeg'] },
|
||||
{ name: 'WebP Image', extensions: ['webp'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
try {
|
||||
// Strip base64 headers if present
|
||||
const base64Content = base64Data.replace(/^data:image\/\w+;base64,/, "");
|
||||
fs.writeFileSync(result.filePath, base64Content, 'base64');
|
||||
return { success: true, filePath: result.filePath };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
return { success: false, canceled: true };
|
||||
});
|
||||
|
||||
// IPC: Show in File Explorer
|
||||
ipcMain.handle('show-in-explorer', async (event, filePath) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
shell.showItemInFolder(filePath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// IPC: Frameless Window Controls
|
||||
ipcMain.on('window-minimize', () => {
|
||||
if (mainWindow) mainWindow.minimize();
|
||||
});
|
||||
|
||||
ipcMain.on('window-maximize', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
if (mainWindow) mainWindow.close();
|
||||
});
|
||||
Reference in New Issue
Block a user