feat: enhance Python environment setup with improved detection and verification methods
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
<p align="center">
|
||||
<img src="build/icon.png" alt="Dekupai logo — stylized letter D with orange and red wing elements" width="180" />
|
||||
</p>
|
||||
|
||||
<p align="center"><em>Logo: Stylized “D” mark with vibrant orange and red wing motifs — glossy 3D finish.</em></p>
|
||||
|
||||
# Dekupai 🍊
|
||||
### *Premium Glassmorphic AI Background Remover & Decoupage Studio*
|
||||
|
||||
@@ -130,4 +136,6 @@ To package and compile **Dekupai** into standalone production binaries or instal
|
||||
|
||||
**Dekupai** is developed under the corporate vision of **Trunçgil Teknoloji** by the software engineering and design expertise of **Ümit Tunç**.
|
||||
|
||||
**Logo & Brand Identity:** The Dekupai app icon features a stylized capital “D” combined with wing-like elements in orange and red gradients, rendered with a glossy three-dimensional finish — representing speed, precision, and creative elevation.
|
||||
|
||||
It integrates state-of-the-art AI-powered image segmentation, combining the deep-learning capabilities of **BiRefNet** with high-end glassmorphic client-side controls to deliver professional desktop matting pipelines directly to local workstations.
|
||||
|
||||
+46
-24
@@ -1,12 +1,13 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import venv
|
||||
|
||||
# Force line buffering
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
|
||||
def run_command(command, description):
|
||||
def run_command(command, description, required=True):
|
||||
print(f"[STATUS] {description}...")
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
@@ -18,10 +19,8 @@ def run_command(command, description):
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Read output in real-time
|
||||
for line in process.stdout:
|
||||
line_str = line.strip()
|
||||
# If the output has pip download status, keep it readable but don't spam too much
|
||||
if "Downloading" in line_str or "Installing" in line_str:
|
||||
print(f"[STATUS] {line_str}")
|
||||
elif line_str:
|
||||
@@ -29,18 +28,32 @@ def run_command(command, description):
|
||||
|
||||
process.wait()
|
||||
if process.returncode != 0:
|
||||
print(f"[ERROR] Command failed with code {process.returncode}", file=sys.stderr)
|
||||
print(f"[ERROR] {description} failed (exit code {process.returncode})", file=sys.stderr)
|
||||
if required:
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception occurred: {str(e)}", file=sys.stderr)
|
||||
return False
|
||||
print(f"[ERROR] Exception during {description}: {str(e)}", file=sys.stderr)
|
||||
return not required
|
||||
|
||||
def verify_install(python_path):
|
||||
print("[STATUS] Verifying PyTorch installation...")
|
||||
verify_cmd = (
|
||||
f'"{python_path}" -c "import torch; import transformers; import PIL; '
|
||||
f'print(f\\"torch={{torch.__version__}}\\")"'
|
||||
)
|
||||
return run_command(verify_cmd, "Verifying installed packages", required=True)
|
||||
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
venv_dir = os.path.join(base_dir, ".venv")
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--data-dir', help='Writable directory for .venv (used by packaged app builds)')
|
||||
args = parser.parse_args()
|
||||
|
||||
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
base_dir = os.path.abspath(args.data_dir) if args.data_dir else os.path.dirname(backend_dir)
|
||||
venv_dir = os.path.join(base_dir, ".venv")
|
||||
requirements_path = os.path.join(backend_dir, "requirements.txt")
|
||||
|
||||
# 1. Create venv if not exists
|
||||
if not os.path.exists(venv_dir):
|
||||
print("[STATUS] Creating Python virtual environment...")
|
||||
try:
|
||||
@@ -52,33 +65,42 @@ def main():
|
||||
else:
|
||||
print("[STATUS] Virtual environment already exists.")
|
||||
|
||||
# 2. Path to pip inside the venv
|
||||
if os.name == 'nt':
|
||||
pip_path = os.path.join(venv_dir, "Scripts", "pip.exe")
|
||||
python_path = os.path.join(venv_dir, "Scripts", "python.exe")
|
||||
else:
|
||||
pip_path = os.path.join(venv_dir, "bin", "pip")
|
||||
python_path = os.path.join(venv_dir, "bin", "python")
|
||||
|
||||
if not os.path.exists(pip_path):
|
||||
print("[ERROR] pip executable not found inside virtual environment!", file=sys.stderr)
|
||||
if not os.path.exists(python_path):
|
||||
print("[ERROR] Python executable not found inside virtual environment!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Upgrade pip
|
||||
run_command(f'"{pip_path}" install --upgrade pip', "Upgrading pip to latest version")
|
||||
run_command(
|
||||
f'"{python_path}" -m pip install --upgrade pip',
|
||||
"Upgrading pip to latest version",
|
||||
required=False
|
||||
)
|
||||
|
||||
# 4. Install dependencies
|
||||
requirements_path = os.path.join(base_dir, "backend", "requirements.txt")
|
||||
torch_index = "https://download.pytorch.org/whl/cpu"
|
||||
torch_cmd = (
|
||||
f'"{python_path}" -m pip install torch torchvision '
|
||||
f'--index-url {torch_index}'
|
||||
)
|
||||
if not run_command(torch_cmd, "Installing PyTorch AI Engine (CPU version)"):
|
||||
print("[ERROR] PyTorch installation failed. Ensure Python 3.10–3.12 is installed and you have internet access.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Use extra index URL for CPU-only torch to download faster (150MB instead of 2.5GB+ GPU version)
|
||||
# This is perfect for single images on desktop!
|
||||
pip_install_cmd = f'"{pip_path}" install -r "{requirements_path}" --extra-index-url https://download.pytorch.org/whl/cpu'
|
||||
|
||||
success = run_command(pip_install_cmd, "Installing PyTorch, Transformers, and PIL dependencies")
|
||||
if not success:
|
||||
other_deps_cmd = (
|
||||
f'"{python_path}" -m pip install '
|
||||
f'"transformers<5" timm einops kornia pillow accelerate'
|
||||
)
|
||||
if not run_command(other_deps_cmd, "Installing HuggingFace Transformers & PIL library"):
|
||||
print("[ERROR] Dependency installation failed!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not verify_install(python_path):
|
||||
print("[ERROR] Package verification failed after installation!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("[STATUS] Setup completed successfully!")
|
||||
print("[STATUS] DONE")
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
@@ -42,14 +42,195 @@ app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
let cachedSystemPython = null;
|
||||
|
||||
function getResourceRoot() {
|
||||
if (app.isPackaged) {
|
||||
const unpacked = path.join(process.resourcesPath, 'app.asar.unpacked');
|
||||
if (fs.existsSync(unpacked)) return unpacked;
|
||||
return process.resourcesPath;
|
||||
}
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
function getDataRoot() {
|
||||
return app.isPackaged ? app.getPath('userData') : __dirname;
|
||||
}
|
||||
|
||||
function getBackendPath(...segments) {
|
||||
return path.join(getResourceRoot(), 'backend', ...segments);
|
||||
}
|
||||
|
||||
function readRegistryValue(key, valueName) {
|
||||
try {
|
||||
const result = spawnSync('reg', ['query', key, '/v', valueName], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (result.status !== 0) return '';
|
||||
const match = result.stdout.match(new RegExp(`${valueName}\\s+REG_(?:EXPAND_)?SZ\\s+(.*)`, 'i'));
|
||||
return match ? match[1].trim() : '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getSpawnEnv() {
|
||||
const env = { ...process.env };
|
||||
if (process.platform !== 'win32') return env;
|
||||
|
||||
const pathEntries = new Set();
|
||||
const addPaths = (value) => {
|
||||
if (!value) return;
|
||||
value.split(';').forEach((entry) => {
|
||||
const trimmed = entry.trim();
|
||||
if (trimmed) pathEntries.add(trimmed);
|
||||
});
|
||||
};
|
||||
|
||||
addPaths(process.env.PATH);
|
||||
addPaths(readRegistryValue('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path'));
|
||||
addPaths(readRegistryValue('HKCU\\Environment', 'Path'));
|
||||
|
||||
const localAppData = process.env.LOCALAPPDATA
|
||||
|| (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
||||
if (localAppData) {
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', 'Launcher'));
|
||||
for (const version of ['312', '311', '310']) {
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', `Python${version}`));
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', `Python${version}`, 'Scripts'));
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.ProgramFiles) {
|
||||
for (const version of ['312', '311', '310']) {
|
||||
pathEntries.add(path.join(process.env.ProgramFiles, `Python${version}`));
|
||||
pathEntries.add(path.join(process.env.ProgramFiles, `Python${version}`, 'Scripts'));
|
||||
}
|
||||
}
|
||||
|
||||
env.PATH = [...pathEntries].join(';');
|
||||
return env;
|
||||
}
|
||||
|
||||
function getWindowsPythonCandidates() {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (command, args = []) => {
|
||||
const key = `${command}::${args.join(' ')}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
candidates.push({ command, args });
|
||||
};
|
||||
|
||||
const localAppData = process.env.LOCALAPPDATA
|
||||
|| (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
||||
|
||||
if (localAppData) {
|
||||
const pyLauncher = path.join(localAppData, 'Programs', 'Python', 'Launcher', 'py.exe');
|
||||
if (fs.existsSync(pyLauncher)) {
|
||||
for (const flag of ['-3.12', '-3.11', '-3.10', '-3']) {
|
||||
addCandidate(pyLauncher, [flag]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of ['312', '311', '310']) {
|
||||
const pythonExe = path.join(localAppData, 'Programs', 'Python', `Python${version}`, 'python.exe');
|
||||
if (fs.existsSync(pythonExe)) addCandidate(pythonExe, []);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.ProgramFiles) {
|
||||
for (const version of ['312', '311', '310']) {
|
||||
const pythonExe = path.join(process.env.ProgramFiles, `Python${version}`, 'python.exe');
|
||||
if (fs.existsSync(pythonExe)) addCandidate(pythonExe, []);
|
||||
}
|
||||
}
|
||||
|
||||
addCandidate('py', ['-3.12']);
|
||||
addCandidate('py', ['-3.11']);
|
||||
addCandidate('py', ['-3.10']);
|
||||
addCandidate('py', ['-3']);
|
||||
addCandidate('python', []);
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function verifyPython(command, args, env) {
|
||||
try {
|
||||
const result = spawnSync(command, [...args, '--version'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 6000,
|
||||
env,
|
||||
});
|
||||
return result.status === 0 && !result.error;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSystemPython() {
|
||||
if (cachedSystemPython) return cachedSystemPython;
|
||||
|
||||
const env = getSpawnEnv();
|
||||
const candidates = process.platform === 'win32'
|
||||
? getWindowsPythonCandidates()
|
||||
: [
|
||||
{ command: 'python3', args: [] },
|
||||
{ command: 'python', args: [] },
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (verifyPython(candidate.command, candidate.args, env)) {
|
||||
cachedSystemPython = candidate;
|
||||
console.log(`[Python Resolver] Selected: ${candidate.command} ${candidate.args.join(' ')}`);
|
||||
return cachedSystemPython;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[Python Resolver] No working Python found; falling back to "python".');
|
||||
cachedSystemPython = { command: 'python', args: [], unverified: true };
|
||||
return cachedSystemPython;
|
||||
}
|
||||
|
||||
function spawnPython(command, args, options = {}) {
|
||||
return spawn(command, args, {
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
env: getSpawnEnv(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function attachProcessListeners(child, { onLine, onClose, onError }) {
|
||||
child.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
for (const line of output.split('\n')) {
|
||||
if (line.trim()) onLine(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
for (const line of errStr.split('\n')) {
|
||||
if (line.trim()) onLine(`[ERROR] ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => onError(err));
|
||||
child.on('close', (code) => onClose(code));
|
||||
}
|
||||
|
||||
// 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');
|
||||
? path.join(getDataRoot(), '.venv', 'Scripts', 'python.exe')
|
||||
: path.join(getDataRoot(), '.venv', 'bin', 'python');
|
||||
|
||||
const systemPython = 'python';
|
||||
const systemPython = resolveSystemPython();
|
||||
const hasVenv = fs.existsSync(venvPython);
|
||||
|
||||
return { venvPython, systemPython, hasVenv };
|
||||
@@ -64,36 +245,41 @@ ipcMain.handle('check-python-setup', async () => {
|
||||
// IPC: Run Setup
|
||||
ipcMain.handle('run-python-setup', async (event) => {
|
||||
const { systemPython } = getPythonPaths();
|
||||
const setupScript = path.join(__dirname, 'backend', 'setup.py');
|
||||
const setupScript = getBackendPath('setup.py');
|
||||
const dataDir = getDataRoot();
|
||||
|
||||
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 });
|
||||
const { command, args: pythonArgs } = systemPython;
|
||||
const spawnArgs = [...pythonArgs, setupScript, '--data-dir', dataDir];
|
||||
console.log(`[Main Process] Spawning setup: ${command} ${spawnArgs.join(' ')}`);
|
||||
|
||||
let stderrData = '';
|
||||
let settled = false;
|
||||
const settle = (payload) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
const setupProcess = spawnPython(command, spawnArgs, { cwd: dataDir });
|
||||
|
||||
attachProcessListeners(setupProcess, {
|
||||
onLine: (line) => {
|
||||
console.log(`[Setup Output] ${line}`);
|
||||
event.sender.send('setup-progress', line);
|
||||
if (line.startsWith('[ERROR]')) stderrData += `${line}\n`;
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(`[Setup Error] Failed to start process: ${err.message}`);
|
||||
settle({
|
||||
code: 1,
|
||||
error: `Python baslatilamadi: ${err.message}. Python 3.10-3.12 kurulu oldugundan emin olun.`,
|
||||
});
|
||||
|
||||
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) => {
|
||||
},
|
||||
onClose: (code) => {
|
||||
console.log(`[Main Process] Setup completed with exit code ${code}`);
|
||||
resolve({ code, error: stderrData });
|
||||
settle({ code, error: stderrData });
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -106,41 +292,38 @@ ipcMain.handle('remove-background', async (event, { inputPath, outputPath }) =>
|
||||
return { success: false, error: 'Python virtual environment not set up. Please run setup first.' };
|
||||
}
|
||||
|
||||
const processScript = path.join(__dirname, 'backend', 'process.py');
|
||||
const processScript = getBackendPath('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: false });
|
||||
|
||||
let stderrData = '';
|
||||
let settled = false;
|
||||
const settle = (payload) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
const bgProcess = spawnPython(venvPython, [processScript, ...args], { cwd: getDataRoot() });
|
||||
|
||||
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) => {
|
||||
attachProcessListeners(bgProcess, {
|
||||
onLine: (line) => {
|
||||
console.log(`[Python Output] ${line}`);
|
||||
event.sender.send('process-progress', line);
|
||||
if (line.startsWith('[ERROR]')) stderrData += `${line}\n`;
|
||||
},
|
||||
onError: (err) => {
|
||||
settle({ success: false, error: `Python baslatilamadi: ${err.message}` });
|
||||
},
|
||||
onClose: (code) => {
|
||||
console.log(`[Main Process] Process completed with exit code ${code}`);
|
||||
if (code === 0) {
|
||||
resolve({ success: true, outputPath });
|
||||
settle({ success: true, outputPath });
|
||||
} else {
|
||||
resolve({ success: false, error: stderrData || `Process failed with exit code ${code}` });
|
||||
settle({ success: false, error: stderrData || `Process failed with exit code ${code}` });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"!.venv/**",
|
||||
"!node_modules/**"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"backend/**"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/icon.ico"
|
||||
|
||||
+14
-2
@@ -246,6 +246,15 @@ elements.btnStartSetup.addEventListener('click', async () => {
|
||||
setStepDone(elements.stepPip);
|
||||
setStepActive(elements.stepDeps);
|
||||
elements.setupProgressBar.style.width = '60%';
|
||||
} else if (progressText.includes("Installing HuggingFace")) {
|
||||
elements.setupStatusText.innerText = getTrans('setupInstallingDeps');
|
||||
setStepDone(elements.stepDeps);
|
||||
setStepActive(elements.stepLibs);
|
||||
elements.setupProgressBar.style.width = '80%';
|
||||
} else if (progressText.includes("Verifying PyTorch")) {
|
||||
elements.setupStatusText.innerText = getTrans('setupInstallingDeps');
|
||||
setStepActive(elements.stepLibs);
|
||||
elements.setupProgressBar.style.width = '90%';
|
||||
} else if (progressText.includes("Downloading torch")) {
|
||||
elements.setupStatusText.innerText = getTrans('setupDownloadingTorch');
|
||||
elements.setupProgressBar.style.width = '75%';
|
||||
@@ -274,9 +283,12 @@ elements.btnStartSetup.addEventListener('click', async () => {
|
||||
}, 1500);
|
||||
} else {
|
||||
setStepFailed(elements.stepDeps);
|
||||
elements.setupStatusText.innerText = getTrans('setupFailed');
|
||||
const errorDetail = (result.error || '').trim();
|
||||
elements.setupStatusText.innerText = errorDetail
|
||||
? `${getTrans('setupFailed')} ${errorDetail.slice(0, 200)}`
|
||||
: getTrans('setupFailed');
|
||||
elements.btnStartSetup.disabled = false;
|
||||
showToast(getTrans('toastSetupError'), getTrans('toastSetupErrorMsg'), "error");
|
||||
showToast(getTrans('toastSetupError'), errorDetail || getTrans('toastSetupErrorMsg'), "error");
|
||||
}
|
||||
} catch (err) {
|
||||
elements.setupStatusText.innerText = `${getTrans('toastSetupError')}: ${err.message}`;
|
||||
|
||||
Reference in New Issue
Block a user