feat: enhance Python environment setup with improved detection and verification methods

This commit is contained in:
Ümit Tunç
2026-05-23 14:00:36 +03:00
parent 5a5e07d6fb
commit 83ea8cc4f8
6 changed files with 316 additions and 88 deletions
+8
View File
@@ -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 🍊 # Dekupai 🍊
### *Premium Glassmorphic AI Background Remover & Decoupage Studio* ### *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ç**. **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. 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.
+50 -28
View File
@@ -1,12 +1,13 @@
import os import os
import sys import sys
import argparse
import subprocess import subprocess
import venv import venv
# Force line buffering # Force line buffering
sys.stdout.reconfigure(line_buffering=True) sys.stdout.reconfigure(line_buffering=True)
def run_command(command, description): def run_command(command, description, required=True):
print(f"[STATUS] {description}...") print(f"[STATUS] {description}...")
try: try:
process = subprocess.Popen( process = subprocess.Popen(
@@ -17,30 +18,42 @@ def run_command(command, description):
shell=True, shell=True,
bufsize=1 bufsize=1
) )
# Read output in real-time
for line in process.stdout: for line in process.stdout:
line_str = line.strip() 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: if "Downloading" in line_str or "Installing" in line_str:
print(f"[STATUS] {line_str}") print(f"[STATUS] {line_str}")
elif line_str: elif line_str:
print(f"[INFO] {line_str}") print(f"[INFO] {line_str}")
process.wait() process.wait()
if process.returncode != 0: 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)
return False if required:
return False
return True return True
except Exception as e: except Exception as e:
print(f"[ERROR] Exception occurred: {str(e)}", file=sys.stderr) print(f"[ERROR] Exception during {description}: {str(e)}", file=sys.stderr)
return False 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(): def main():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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") 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): if not os.path.exists(venv_dir):
print("[STATUS] Creating Python virtual environment...") print("[STATUS] Creating Python virtual environment...")
try: try:
@@ -52,33 +65,42 @@ def main():
else: else:
print("[STATUS] Virtual environment already exists.") print("[STATUS] Virtual environment already exists.")
# 2. Path to pip inside the venv
if os.name == 'nt': if os.name == 'nt':
pip_path = os.path.join(venv_dir, "Scripts", "pip.exe")
python_path = os.path.join(venv_dir, "Scripts", "python.exe") python_path = os.path.join(venv_dir, "Scripts", "python.exe")
else: else:
pip_path = os.path.join(venv_dir, "bin", "pip")
python_path = os.path.join(venv_dir, "bin", "python") python_path = os.path.join(venv_dir, "bin", "python")
if not os.path.exists(pip_path): if not os.path.exists(python_path):
print("[ERROR] pip executable not found inside virtual environment!", file=sys.stderr) print("[ERROR] Python executable not found inside virtual environment!", file=sys.stderr)
sys.exit(1) sys.exit(1)
# 3. Upgrade pip run_command(
run_command(f'"{pip_path}" install --upgrade pip', "Upgrading pip to latest version") f'"{python_path}" -m pip install --upgrade pip',
"Upgrading pip to latest version",
required=False
)
# 4. Install dependencies torch_index = "https://download.pytorch.org/whl/cpu"
requirements_path = os.path.join(base_dir, "backend", "requirements.txt") torch_cmd = (
f'"{python_path}" -m pip install torch torchvision '
# Use extra index URL for CPU-only torch to download faster (150MB instead of 2.5GB+ GPU version) f'--index-url {torch_index}'
# 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' if not run_command(torch_cmd, "Installing PyTorch AI Engine (CPU version)"):
print("[ERROR] PyTorch installation failed. Ensure Python 3.103.12 is installed and you have internet access.", file=sys.stderr)
success = run_command(pip_install_cmd, "Installing PyTorch, Transformers, and PIL dependencies") sys.exit(1)
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) print("[ERROR] Dependency installation failed!", file=sys.stderr)
sys.exit(1) 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] Setup completed successfully!")
print("[STATUS] DONE") print("[STATUS] DONE")
BIN
View File
Binary file not shown.
+241 -58
View File
@@ -1,6 +1,6 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path'); const path = require('path');
const { spawn } = require('child_process'); const { spawn, spawnSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
let mainWindow; let mainWindow;
@@ -42,14 +42,195 @@ app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit(); 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 // Helper: Get Python Path
function getPythonPaths() { function getPythonPaths() {
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const venvPython = isWindows const venvPython = isWindows
? path.join(__dirname, '.venv', 'Scripts', 'python.exe') ? path.join(getDataRoot(), '.venv', 'Scripts', 'python.exe')
: path.join(__dirname, '.venv', 'bin', 'python'); : path.join(getDataRoot(), '.venv', 'bin', 'python');
const systemPython = 'python'; const systemPython = resolveSystemPython();
const hasVenv = fs.existsSync(venvPython); const hasVenv = fs.existsSync(venvPython);
return { venvPython, systemPython, hasVenv }; return { venvPython, systemPython, hasVenv };
@@ -64,36 +245,41 @@ ipcMain.handle('check-python-setup', async () => {
// IPC: Run Setup // IPC: Run Setup
ipcMain.handle('run-python-setup', async (event) => { ipcMain.handle('run-python-setup', async (event) => {
const { systemPython } = getPythonPaths(); const { systemPython } = getPythonPaths();
const setupScript = path.join(__dirname, 'backend', 'setup.py'); const setupScript = getBackendPath('setup.py');
const dataDir = getDataRoot();
return new Promise((resolve) => { return new Promise((resolve) => {
console.log(`[Main Process] Spawning setup: ${systemPython} ${setupScript}`); const { command, args: pythonArgs } = systemPython;
// Spawn host python to run setup.py const spawnArgs = [...pythonArgs, setupScript, '--data-dir', dataDir];
const process = spawn(systemPython, [setupScript], { shell: true }); console.log(`[Main Process] Spawning setup: ${command} ${spawnArgs.join(' ')}`);
let stderrData = ''; let stderrData = '';
let settled = false;
const settle = (payload) => {
if (settled) return;
settled = true;
resolve(payload);
};
process.stdout.on('data', (data) => { const setupProcess = spawnPython(command, spawnArgs, { cwd: dataDir });
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) => { attachProcessListeners(setupProcess, {
const errStr = data.toString(); onLine: (line) => {
stderrData += errStr; console.log(`[Setup Output] ${line}`);
console.error(`[Setup Error] ${errStr.trim()}`); event.sender.send('setup-progress', line);
event.sender.send('setup-progress', `[ERROR] ${errStr.trim()}`); if (line.startsWith('[ERROR]')) stderrData += `${line}\n`;
}); },
onError: (err) => {
process.on('close', (code) => { console.error(`[Setup Error] Failed to start process: ${err.message}`);
console.log(`[Main Process] Setup completed with exit code ${code}`); settle({
resolve({ code, error: stderrData }); code: 1,
error: `Python baslatilamadi: ${err.message}. Python 3.10-3.12 kurulu oldugundan emin olun.`,
});
},
onClose: (code) => {
console.log(`[Main Process] Setup completed with exit code ${code}`);
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.' }; 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) => { return new Promise((resolve) => {
console.log(`[Main Process] Spawning background removal: ${venvPython} ${processScript}`); console.log(`[Main Process] Spawning background removal: ${venvPython} ${processScript}`);
// Spawn venv python to run process.py
const args = ['--input', inputPath, '--output', outputPath]; const args = ['--input', inputPath, '--output', outputPath];
const process = spawn(venvPython, [processScript, ...args], { shell: false });
let stderrData = ''; let stderrData = '';
let settled = false;
const settle = (payload) => {
if (settled) return;
settled = true;
resolve(payload);
};
process.stdout.on('data', (data) => { const bgProcess = spawnPython(venvPython, [processScript, ...args], { cwd: getDataRoot() });
const output = data.toString();
console.log(`[Python Output] ${output.trim()}`); attachProcessListeners(bgProcess, {
const lines = output.split('\n'); onLine: (line) => {
for (const line of lines) { console.log(`[Python Output] ${line}`);
if (line.trim()) { event.sender.send('process-progress', line);
event.sender.send('process-progress', line.trim()); 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) {
settle({ success: true, outputPath });
} else {
settle({ success: false, error: stderrData || `Process failed with exit code ${code}` });
} }
} },
});
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}` });
}
}); });
}); });
}); });
+3
View File
@@ -36,6 +36,9 @@
"!.venv/**", "!.venv/**",
"!node_modules/**" "!node_modules/**"
], ],
"asarUnpack": [
"backend/**"
],
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "build/icon.ico" "icon": "build/icon.ico"
+14 -2
View File
@@ -246,6 +246,15 @@ elements.btnStartSetup.addEventListener('click', async () => {
setStepDone(elements.stepPip); setStepDone(elements.stepPip);
setStepActive(elements.stepDeps); setStepActive(elements.stepDeps);
elements.setupProgressBar.style.width = '60%'; 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")) { } else if (progressText.includes("Downloading torch")) {
elements.setupStatusText.innerText = getTrans('setupDownloadingTorch'); elements.setupStatusText.innerText = getTrans('setupDownloadingTorch');
elements.setupProgressBar.style.width = '75%'; elements.setupProgressBar.style.width = '75%';
@@ -274,9 +283,12 @@ elements.btnStartSetup.addEventListener('click', async () => {
}, 1500); }, 1500);
} else { } else {
setStepFailed(elements.stepDeps); 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; elements.btnStartSetup.disabled = false;
showToast(getTrans('toastSetupError'), getTrans('toastSetupErrorMsg'), "error"); showToast(getTrans('toastSetupError'), errorDetail || getTrans('toastSetupErrorMsg'), "error");
} }
} catch (err) { } catch (err) {
elements.setupStatusText.innerText = `${getTrans('toastSetupError')}: ${err.message}`; elements.setupStatusText.innerText = `${getTrans('toastSetupError')}: ${err.message}`;