diff --git a/README.md b/README.md index 439bf54..6c08e43 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +
+
+
Logo: Stylized “D” mark with vibrant orange and red wing motifs — glossy 3D finish.
+ # 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. diff --git a/backend/setup.py b/backend/setup.py index be33059..e98bb5c 100644 --- a/backend/setup.py +++ b/backend/setup.py @@ -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( @@ -17,30 +18,42 @@ def run_command(command, description): shell=True, 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: print(f"[INFO] {line_str}") - + process.wait() if process.returncode != 0: - print(f"[ERROR] Command failed with code {process.returncode}", file=sys.stderr) - return False + 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__))) + 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") - - # 1. Create venv if not exists + requirements_path = os.path.join(backend_dir, "requirements.txt") + 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") - - # 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: + 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) + + 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") diff --git a/docs/usage.mp4 b/docs/usage.mp4 new file mode 100644 index 0000000..b68d73e Binary files /dev/null and b/docs/usage.mp4 differ diff --git a/main.js b/main.js index f5eea35..10d46a4 100644 --- a/main.js +++ b/main.js @@ -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'); - - const systemPython = 'python'; + const venvPython = isWindows + ? path.join(getDataRoot(), '.venv', 'Scripts', 'python.exe') + : path.join(getDataRoot(), '.venv', 'bin', '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 }); - 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 }); + 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.`, + }); + }, + 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.' }; } - 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() }); + + 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) { + 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}` }); - } + }, }); }); }); diff --git a/package.json b/package.json index 910f032..9acf8d6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "!.venv/**", "!node_modules/**" ], + "asarUnpack": [ + "backend/**" + ], "win": { "target": "nsis", "icon": "build/icon.ico" diff --git a/renderer.js b/renderer.js index 27a19b2..e0584fb 100644 --- a/renderer.js +++ b/renderer.js @@ -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}`;