From cec4844245a3948a7f3e354ee0afe905a3978496 Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 12:01:47 +0000 Subject: [PATCH 1/8] add linux support --- .github/workflows/ci.yml | 2 + AGENTS.md | 4 +- README.md | 22 ++++-- backend/pyproject.toml | 1 + backend/runtime_config/runtime_policy.py | 2 +- backend/tests/test_runtime_policy_decision.py | 18 ++++- backend/uv.lock | 2 + docs/CONTRIBUTING.md | 6 +- docs/INSTALLER.md | 79 ++++++++++--------- docs/TELEMETRY.md | 1 + electron-builder.yml | 24 ++++++ electron/app-paths.ts | 3 +- electron/python-setup.ts | 32 +++++--- electron/updater.ts | 8 +- frontend/views/editor/ClipContextMenu.tsx | 4 +- package.json | 15 ++-- scripts/create-installer.sh | 7 +- scripts/prepare-python.sh | 58 ++++++++++---- scripts/run-script.js | 34 ++++++++ scripts/setup-dev.sh | 19 +++-- 20 files changed, 238 insertions(+), 103 deletions(-) create mode 100644 scripts/run-script.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df5ddda..ea58a42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,8 @@ jobs: runner: macos-latest - os: windows runner: windows-latest + - os: linux + runner: ubuntu-latest defaults: run: working-directory: backend diff --git a/AGENTS.md b/AGENTS.md index 5a63466..92828ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,8 +21,8 @@ LTX Desktop is an Electron app for AI video generation using LTX models. Three-l | `pnpm typecheck:py` | Python pyright only | | `pnpm backend:test` | Run Python pytest tests | | `pnpm build:frontend` | Vite frontend build only | -| `pnpm build:mac` / `pnpm build:win` | Full platform builds | -| `pnpm setup:dev:mac` / `pnpm setup:dev:win` | One-time dev environment setup | +| `pnpm build` | Full platform build (auto-detects platform) | +| `pnpm setup:dev` | One-time dev environment setup (auto-detects platform) | Run a single backend test file via pnpm: `pnpm backend:test -- tests/test_ic_lora.py` diff --git a/README.md b/README.md index acc7f4b..8602310 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LTX Desktop -LTX Desktop is an open-source desktop app for generating videos with LTX models — locally on supported Windows NVIDIA GPUs, with an API mode for unsupported hardware and macOS. +LTX Desktop is an open-source desktop app for generating videos with LTX models — locally on supported Windows/Linux NVIDIA GPUs, with an API mode for unsupported hardware and macOS. > **Status: Beta.** Expect breaking changes. > Frontend architecture is under active refactor; large UI PRs may be declined for now (see [`CONTRIBUTING.md`](docs/CONTRIBUTING.md)). @@ -32,8 +32,9 @@ LTX Desktop is an open-source desktop app for generating videos with LTX models | --- | --- | --- | | Windows + CUDA GPU with **≥32GB VRAM** | Local generation | Downloads model weights locally | | Windows (no CUDA, <32GB VRAM, or unknown VRAM) | API-only | **LTX API key required** | +| Linux + CUDA GPU with **≥32GB VRAM** | Local generation | Downloads model weights locally | +| Linux (no CUDA, <32GB VRAM, or unknown VRAM) | API-only | **LTX API key required** | | macOS (Apple Silicon builds) | API-only | **LTX API key required** | -| Linux | Not officially supported | No official builds | In API-only mode, available resolutions/durations may be limited to what the API supports. @@ -46,6 +47,14 @@ In API-only mode, available resolutions/durations may be limited to what the API - 16GB+ RAM (32GB recommended) - **160GB+ free disk space** (for model weights, Python environment, and outputs) +### Linux (local generation) + +- Ubuntu 22.04+ or similar distro (x64) +- NVIDIA GPU with CUDA support and **≥32GB VRAM** (more is better) +- NVIDIA driver installed (PyTorch bundles the CUDA runtime) +- 16GB+ RAM (32GB recommended) +- Plenty of free disk space for model weights and outputs + ### macOS (API-only) - Apple Silicon (arm64) @@ -64,6 +73,7 @@ LTX Desktop stores app data (settings, models, logs) in: - **Windows:** `%LOCALAPPDATA%\LTXDesktop\` - **macOS:** `~/Library/Application Support/LTXDesktop/` +- **Linux:** `$XDG_DATA_HOME/LTXDesktop/` (default: `~/.local/share/LTXDesktop/`) Model weights are downloaded into the `models/` subfolder (this can be large and may take time). @@ -84,7 +94,7 @@ The LTX API is used for: - API-based video generations (required on macOS and on unsupported Windows hardware) — paid - Retake — paid -An LTX API key is required in API-only mode, but optional on Windows local mode if you enable the Local Text Encoder. +An LTX API key is required in API-only mode, but optional on Windows/Linux local mode if you enable the Local Text Encoder. Generate a FREE API key at the [LTX Console](https://console.ltx.video/). Text encoding is free; video generation API usage is paid. [Read more](https://ltx.io/model/model-blog/ltx-2-better-control-for-real-workflows). @@ -137,11 +147,7 @@ Prereqs: Setup: ```bash -# macOS -pnpm setup:dev:mac - -# Windows -pnpm setup:dev:win +pnpm setup:dev ``` Run: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 797e3bf..a546d24 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "uvicorn[standard]>=0.30.0", "python-multipart>=0.0.9", "triton-windows; sys_platform == 'win32'", + "triton; sys_platform == 'linux'", ] [[tool.uv.index]] diff --git a/backend/runtime_config/runtime_policy.py b/backend/runtime_config/runtime_policy.py index cf64b66..7b84dc8 100644 --- a/backend/runtime_config/runtime_policy.py +++ b/backend/runtime_config/runtime_policy.py @@ -8,7 +8,7 @@ def decide_force_api_generations(system: str, cuda_available: bool, vram_gb: int if system == "Darwin": return True - if system == "Windows": + if system in ("Windows", "Linux"): if not cuda_available: return True if vram_gb is None: diff --git a/backend/tests/test_runtime_policy_decision.py b/backend/tests/test_runtime_policy_decision.py index 0999f70..f4f8ba0 100644 --- a/backend/tests/test_runtime_policy_decision.py +++ b/backend/tests/test_runtime_policy_decision.py @@ -26,5 +26,21 @@ def test_windows_with_required_vram_allows_local_mode() -> None: assert decide_force_api_generations(system="Windows", cuda_available=True, vram_gb=31) is False +def test_linux_without_cuda_forces_api() -> None: + assert decide_force_api_generations(system="Linux", cuda_available=False, vram_gb=24) is True + + +def test_linux_with_low_vram_forces_api() -> None: + assert decide_force_api_generations(system="Linux", cuda_available=True, vram_gb=30) is True + + +def test_linux_with_unknown_vram_forces_api() -> None: + assert decide_force_api_generations(system="Linux", cuda_available=True, vram_gb=None) is True + + +def test_linux_with_required_vram_allows_local_mode() -> None: + assert decide_force_api_generations(system="Linux", cuda_available=True, vram_gb=31) is False + + def test_other_systems_fail_closed() -> None: - assert decide_force_api_generations(system="Linux", cuda_available=True, vram_gb=48) is True + assert decide_force_api_generations(system="FreeBSD", cuda_available=True, vram_gb=48) is True diff --git a/backend/uv.lock b/backend/uv.lock index 07a19c0..c49d255 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -531,6 +531,7 @@ dependencies = [ { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tqdm" }, { name = "transformers" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "triton-windows", marker = "sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -574,6 +575,7 @@ requires-dist = [ { name = "torch", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = ">=2.3.0", index = "https://download.pytorch.org/whl/cu128" }, { name = "tqdm", specifier = ">=4.66.0" }, { name = "transformers", specifier = ">=4.52,<5" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "triton-windows", marker = "sys_platform == 'win32'" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, ] diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a642e8b..b0f9ded 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -14,11 +14,7 @@ Prereqs: Setup: ```bash -# macOS -pnpm setup:dev:mac - -# Windows -pnpm setup:dev:win +pnpm setup:dev ``` Run: diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md index fce6f70..f2b6f31 100644 --- a/docs/INSTALLER.md +++ b/docs/INSTALLER.md @@ -10,7 +10,7 @@ This guide explains how to build a distributable installer for **LTX Desktop**. The installer includes: - **Electron app** (React frontend + Electron shell) - **Embedded Python** (version from [`backend/.python-version`](../backend/.python-version)) with all dependencies pre-installed: - - PyTorch (CUDA on Windows, MPS on macOS) + - PyTorch (CUDA on Windows/Linux, MPS on macOS) - FastAPI, Diffusers, Transformers - LTX-2 inference packages - All other required libraries @@ -18,6 +18,7 @@ The installer includes: **NOT bundled** (downloaded at runtime): - Model weights (downloaded on first run; can be large) from Hugging Face +- On **Linux** and **Windows**: the Python environment itself is downloaded on first launch (keeps installer small) The embedded Python is **fully isolated** from the target system's Python — it lives inside `{install_dir}/resources/python/` and never modifies system settings. @@ -35,67 +36,43 @@ Before building, ensure you have: - **Windows**: PowerShell 5.1+ (comes with Windows 10/11) - **macOS**: Xcode Command Line Tools (`xcode-select --install`) +- **Linux**: `build-essential` (or equivalent) for native extensions ## Quick Build -### macOS ```bash -pnpm build:mac -``` - -### Windows -```powershell -pnpm build:win +pnpm build ``` -This will: +This auto-detects your platform and will: 1. Download a standalone Python distribution (version from [`backend/.python-version`](../backend/.python-version)) -2. Install all Python dependencies (~10GB on Windows with CUDA, ~2-3GB on macOS with MPS) +2. Install all Python dependencies (~10GB on Windows/Linux with CUDA, ~2-3GB on macOS with MPS) 3. Build the frontend 4. Package everything with electron-builder -5. Create a DMG (macOS) or NSIS installer (Windows) in the `release/` folder +5. Create a DMG (macOS), AppImage + deb (Linux), or NSIS installer (Windows) in the `release/` folder ## Build Options -### macOS - ```bash # Full build -pnpm build:mac +pnpm build # Skip Python setup (if already prepared) -pnpm build:mac:skip-python +pnpm build:skip-python # Fast rebuild (unpacked, skip Python + pnpm install) -pnpm build:fast:mac +pnpm build:fast # Just prepare Python environment -pnpm prepare:python:mac +pnpm prepare:python ``` -### Windows - -```powershell -# Full build -pnpm build:win - -# Skip Python setup (if already prepared) -pnpm build:win:skip-python - -# Just prepare Python environment -pnpm prepare:python:win - -# Fast rebuild (unpacked, skip Python + pnpm install) -pnpm build:fast:win - -# Clean build -powershell -File scripts/local-build.ps1 -Clean -``` +All commands auto-detect the current platform (macOS, Linux, or Windows). ### Build Script Options -The `local-build.sh` script accepts: -- `--platform mac|win` — Target platform (auto-detected if omitted) +The underlying `local-build.sh` / `local-build.ps1` scripts also accept: +- `--platform mac|linux|win` — Target platform (auto-detected if omitted) - `--skip-python` — Use existing `python-embed/` directory - `--clean` — Remove build artifacts before starting - `--unpack` — Build unpacked app only (faster, no installer/DMG) @@ -108,6 +85,13 @@ release/ └── LTX Desktop--arm64.dmg ``` +### Linux +``` +release/ + ├── LTX Desktop-x86_64.AppImage + └── LTX Desktop-amd64.deb +``` + ### Windows ``` release/ @@ -118,7 +102,7 @@ release/ Place icon files in `resources/` before building: - `icon.ico` — Windows (multi-size ICO: 256x256, 128x128, 64x64, 48x48, 32x32, 16x16) -- `icon.png` — macOS (1024x1024 recommended) +- `icon.png` — macOS and Linux (1024x1024 recommended) ## Troubleshooting @@ -137,6 +121,7 @@ xattr -dr com.apple.quarantine /Applications/LTX\ Desktop.app ### Installer is too large Expected installer sizes (does not include model weights): - **Windows**: ~10GB (PyTorch CUDA ~2.5GB + ML libraries ~5GB + Python ~200MB + Electron ~100MB) +- **Linux**: ~10GB (similar to Windows; PyTorch CUDA variant) - **macOS**: ~2-3GB (PyTorch MPS is much smaller than CUDA variant) ### Runtime / first-run issues @@ -162,6 +147,24 @@ npx electron-builder --mac npx electron-builder --mac --dir ``` +### Linux +```bash +# 1. Prepare Python environment +bash scripts/prepare-python.sh + +# 2. Install dependencies +pnpm install + +# 3. Build frontend +pnpm build:frontend + +# 4. Build AppImage + deb +npx electron-builder --linux + +# Or build unpacked app (faster, for testing) +npx electron-builder --linux --dir +``` + ### Windows ```powershell # 1. Prepare Python environment diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index b1e8a3e..02324a0 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -17,6 +17,7 @@ App data folder locations: - **Windows:** `%LOCALAPPDATA%\LTXDesktop\` - **macOS:** `~/Library/Application Support/LTXDesktop/` +- **Linux:** `$XDG_DATA_HOME/LTXDesktop/` (default: `~/.local/share/LTXDesktop/`) Your preference is respected immediately — no restart required. diff --git a/electron-builder.yml b/electron-builder.yml index 94753cc..05275b5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -92,6 +92,30 @@ dmg: type: link path: /Applications +linux: + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + icon: resources/icon.png + category: Video + artifactName: ${productName}-${arch}.${ext} + +deb: + depends: + - libgtk-3-0 + - libnotify4 + - libnss3 + - libxss1 + - libxtst6 + - xdg-utils + - libatspi2.0-0 + - libuuid1 + - libsecret-1-0 + publish: provider: github owner: Lightricks diff --git a/electron/app-paths.ts b/electron/app-paths.ts index ddf10e6..40a3611 100644 --- a/electron/app-paths.ts +++ b/electron/app-paths.ts @@ -18,7 +18,8 @@ function resolveUserDataPath(): string { APP_FOLDER_NAME, ) } - return path.join(os.homedir(), `.${APP_FOLDER_NAME}`) + const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share') + return path.join(xdgData, APP_FOLDER_NAME) } app.setPath('userData', resolveUserDataPath()) diff --git a/electron/python-setup.ts b/electron/python-setup.ts index c429b4d..f5868ad 100644 --- a/electron/python-setup.ts +++ b/electron/python-setup.ts @@ -66,7 +66,7 @@ function getInstalledHashPath(): string { /** Directory where python-embed lives at runtime. */ export function getPythonDir(): string { - if (process.platform === 'win32') { + if (process.platform === 'win32' || process.platform === 'linux') { if (isDev) { return path.join(process.cwd(), 'python-embed') } @@ -81,7 +81,7 @@ export function getPythonDir(): string { * Also promotes a staged python-next/ directory if it matches the expected hash. */ export function isPythonReady(): { ready: boolean } { - if (process.platform !== 'win32') { + if (process.platform === 'darwin') { return { ready: true } } @@ -112,7 +112,7 @@ export function isPythonReady(): { ready: boolean } { const installedHash = readHash(getInstalledHashPath()) if (!bundledHash) { - const pythonExe = path.join(getPythonDir(), 'python.exe') + const pythonExe = path.join(getPythonDir(), process.platform === 'win32' ? 'python.exe' : 'bin/python3') return { ready: fs.existsSync(pythonExe) } } @@ -128,7 +128,7 @@ export async function preDownloadPythonForUpdate( newVersion: string, onProgress?: (progress: PythonSetupProgress) => void ): Promise { - if (process.platform !== 'win32') { + if (process.platform === 'darwin') { return false } @@ -190,7 +190,8 @@ export async function preDownloadPythonForUpdate( try { await acquireArchive(baseUrl, archivePath, cleanupFiles, progressCb) } catch (primaryErr) { - const fallbackUrl = newHash ? `${FALLBACK_CDN_BASE}/python-embed-win32/${newHash}/python-embed-win32.tar.gz` : null + const prefix = getPythonArchivePrefix() + const fallbackUrl = newHash ? `${FALLBACK_CDN_BASE}/${prefix}/${newHash}/${prefix}.tar.gz` : null if (!fallbackUrl || isLocalPath(baseUrl)) { throw primaryErr } @@ -242,6 +243,15 @@ function readHash(filePath: string): string | null { const FALLBACK_CDN_BASE = 'https://storage.googleapis.com/ltx-desktop-artifacts' +function getPythonArchivePrefix(): string { + if (process.platform === 'win32') return 'python-embed-win32' + if (process.platform === 'linux') { + if (process.arch === 'x64') return 'python-embed-linux-x64' + throw new Error(`Unsupported Linux architecture: ${process.arch}`) + } + throw new Error(`Python download is not supported on ${process.platform}`) +} + function getArchiveBase(): string { // LTX_PYTHON_URL is a dev-only override for testing with local archives. // Disabled in production to prevent code injection into a signed app. @@ -255,7 +265,8 @@ function getArchiveBase(): string { function getFallbackArchiveUrl(): string | null { const hash = readHash(getBundledHashPath()) if (!hash) return null - return `${FALLBACK_CDN_BASE}/python-embed-win32/${hash}/python-embed-win32.tar.gz` + const prefix = getPythonArchivePrefix() + return `${FALLBACK_CDN_BASE}/${prefix}/${hash}/${prefix}.tar.gz` } function isLocalPath(source: string): boolean { @@ -314,7 +325,7 @@ export async function downloadPythonEmbed( ): Promise { const destDir = path.join(app.getPath('userData'), 'python') const tempDir = path.join(app.getPath('userData'), 'python-tmp') - const archivePath = path.join(app.getPath('userData'), 'python-embed-win32.tar.gz') + const archivePath = path.join(app.getPath('userData'), `${getPythonArchivePrefix()}.tar.gz`) try { if (fs.existsSync(tempDir)) { @@ -395,7 +406,7 @@ async function acquirePartsLocal( cleanupFiles: string[], onProgress: (progress: PythonSetupProgress) => void ): Promise { - const manifestPath = path.join(dirPath, 'python-embed-win32.manifest.json') + const manifestPath = path.join(dirPath, `${getPythonArchivePrefix()}.manifest.json`) const manifest: ArchiveManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) const partPaths: string[] = [] @@ -423,8 +434,9 @@ async function acquirePartsRemote( onProgress: (progress: PythonSetupProgress) => void ): Promise { // Fetch manifest - const manifestUrl = `${baseUrl}/python-embed-win32.manifest.json` - const manifestDest = path.join(app.getPath('userData'), 'python-embed-win32.manifest.json') + const prefix = getPythonArchivePrefix() + const manifestUrl = `${baseUrl}/${prefix}.manifest.json` + const manifestDest = path.join(app.getPath('userData'), `${prefix}.manifest.json`) cleanupFiles.push(manifestDest) await downloadFileRaw(manifestUrl, manifestDest) const manifest: ArchiveManifest = JSON.parse(fs.readFileSync(manifestDest, 'utf-8')) diff --git a/electron/updater.ts b/electron/updater.ts index b3f33aa..8caeb29 100644 --- a/electron/updater.ts +++ b/electron/updater.ts @@ -13,20 +13,20 @@ export function initAutoUpdater( autoUpdater.allowPrerelease = true } - // On Windows, don't auto-install — we need to pre-download python-embed first. + // On Windows/Linux, don't auto-install — we need to pre-download python-embed first. // On macOS, python is bundled in the DMG so auto-install is fine. - if (process.platform === 'win32') { + if (process.platform !== 'darwin') { autoUpdater.autoInstallOnAppQuit = false } autoUpdater.on('update-downloaded', async (info: UpdateDownloadedEvent) => { - if (process.platform !== 'win32') { + if (process.platform === 'darwin') { // macOS: python is bundled, just install normally autoUpdater.quitAndInstall(false, true) return } - // Windows: pre-download python-embed if deps changed before restarting + // Windows/Linux: pre-download python-embed if deps changed before restarting const newVersion = info.version logger.info( `[updater] Update downloaded: v${newVersion}, checking python deps...`) diff --git a/frontend/views/editor/ClipContextMenu.tsx b/frontend/views/editor/ClipContextMenu.tsx index 0005158..010c6dc 100644 --- a/frontend/views/editor/ClipContextMenu.tsx +++ b/frontend/views/editor/ClipContextMenu.tsx @@ -586,7 +586,9 @@ function SingleClipMenu({ filePath = liveAsset.takes[Math.max(0, Math.min(takeIdx, liveAsset.takes.length - 1))].path } if (!filePath) return null - const label = window.electronAPI?.platform === 'darwin' ? 'Reveal in Finder' : 'Show in Explorer' + const label = window.electronAPI?.platform === 'darwin' ? 'Reveal in Finder' + : window.electronAPI?.platform === 'linux' ? 'Show in Files' + : 'Show in Explorer' return { window.electronAPI?.showItemInFolder(filePath); close() }} /> })()} diff --git a/package.json b/package.json index 000eb81..03e9091 100644 --- a/package.json +++ b/package.json @@ -9,22 +9,17 @@ "private": true, "packageManager": "pnpm@10.30.3", "scripts": { - "setup:dev:mac": "bash scripts/setup-dev.sh", - "setup:dev:win": "powershell -ExecutionPolicy Bypass -File scripts/setup-dev.ps1", + "setup:dev": "node scripts/run-script.js scripts/setup-dev", "dev": "vite", "dev:debug": "cross-env BACKEND_DEBUG=1 ELECTRON_DEBUG=1 vite", "typecheck": "concurrently \"pnpm run typecheck:ts\" \"pnpm run typecheck:py\"", "typecheck:ts": "tsc --noEmit", "typecheck:py": "cd backend && uv run pyright", "backend:test": "cd backend && uv sync --frozen --extra test --extra dev && uv run pytest -v --tb=short", - "build:mac": "bash scripts/local-build.sh --platform mac", - "build:win": "powershell -ExecutionPolicy Bypass -File scripts/local-build.ps1", - "build:mac:skip-python": "bash scripts/local-build.sh --platform mac --skip-python", - "build:win:skip-python": "powershell -ExecutionPolicy Bypass -File scripts/local-build.ps1 -SkipPython", - "build:fast:mac": "bash scripts/local-build.sh --platform mac --unpack --skip-python", - "build:fast:win": "powershell -ExecutionPolicy Bypass -File scripts/local-build.ps1 -Unpack -SkipPython", - "prepare:python:mac": "bash scripts/prepare-python.sh", - "prepare:python:win": "powershell -ExecutionPolicy Bypass -File scripts/prepare-python.ps1", + "build": "node scripts/run-script.js scripts/local-build", + "build:skip-python": "node scripts/run-script.js scripts/local-build --skip-python", + "build:fast": "node scripts/run-script.js scripts/local-build --unpack --skip-python", + "prepare:python": "node scripts/run-script.js scripts/prepare-python", "build:frontend": "vite build", "start:unpacked:mac": "open release/mac-arm64/LTX\\ Desktop.app", "start:unpacked:win": "powershell -Command \"& '.\\release\\win-unpacked\\LTX Desktop.exe'\"" diff --git a/scripts/create-installer.sh b/scripts/create-installer.sh index 02310f0..31a074d 100755 --- a/scripts/create-installer.sh +++ b/scripts/create-installer.sh @@ -67,7 +67,7 @@ if [ ! -d "dist" ] || [ ! -d "dist-electron" ]; then exit 1 fi -if [ ! -d "python-embed" ]; then +if [ "$PLATFORM" != "linux" ] && [ ! -d "python-embed" ]; then echo "ERROR: Python environment not found. Run local-build.sh or prepare-python.sh first." exit 1 fi @@ -114,6 +114,11 @@ if [ "$UNPACK" = true ]; then echo "Unpacked app ready!" echo "Run: $RELEASE_DIR/win-unpacked/LTX Desktop.exe" ;; + linux) + echo "" + echo "Unpacked app ready!" + echo "Run: $RELEASE_DIR/linux-unpacked/ltx-desktop" + ;; esac else echo "" diff --git a/scripts/prepare-python.sh b/scripts/prepare-python.sh index 544bfac..17aa3e1 100755 --- a/scripts/prepare-python.sh +++ b/scripts/prepare-python.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # prepare-python.sh -# Downloads a standalone Python and installs all dependencies for macOS distribution. +# Downloads a standalone Python and installs all dependencies for macOS/Linux distribution. # # Dependencies are read from uv.lock (via `uv export`) — pyproject.toml is the # single source of truth. No hardcoded dependency lists. # # Uses python-build-standalone (https://github.com/astral-sh/python-build-standalone) -# which provides relocatable Python builds for macOS. +# which provides relocatable Python builds for macOS and Linux. # # Prerequisites: # - uv must be installed (https://docs.astral.sh/uv/) @@ -36,11 +36,20 @@ case "$ARCH" in *) echo "ERROR: Unsupported architecture: $ARCH"; exit 1 ;; esac -PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}/cpython-${PYTHON_VERSION}+${PBS_TAG}-${PBS_ARCH}-apple-darwin-install_only_stripped.tar.gz" +# Detect OS for python-build-standalone target triple +case "$(uname -s)" in + Darwin) PBS_OS="apple-darwin" ;; + Linux) PBS_OS="unknown-linux-gnu" ;; + *) echo "ERROR: Unsupported OS: $(uname -s)"; exit 1 ;; +esac + +PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}/cpython-${PYTHON_VERSION}+${PBS_TAG}-${PBS_ARCH}-${PBS_OS}-install_only_stripped.tar.gz" + +PLATFORM_LABEL="$(uname -s) ($ARCH)" echo "========================================" echo " LTX Video - Python Environment Setup" -echo " Platform: macOS ($ARCH)" +echo " Platform: $PLATFORM_LABEL" echo " Python: $PYTHON_VERSION" echo "========================================" @@ -77,8 +86,8 @@ echo "Step 2: Generating requirements.txt from uv.lock..." REQUIREMENTS_FILE="$BACKEND_DIR/requirements-dist.txt" # Export pinned deps, excluding the project itself. -# Running on macOS auto-excludes Windows-only deps (triton-windows, pynvml, sageattention) -# via sys_platform markers in pyproject.toml. +# Platform markers in pyproject.toml auto-exclude irrelevant deps +# (e.g. triton-windows on Linux/macOS, triton on macOS/Windows). uv export --frozen --no-hashes --no-editable --no-emit-project \ --no-header --no-annotate \ --project "$BACKEND_DIR" \ @@ -150,9 +159,14 @@ echo "" echo "Step 6: Installing dependencies from requirements.txt..." echo " (This may take a while — PyTorch + ML libraries are large)" -# No --extra-index-url needed on macOS: standard PyPI torch includes MPS support +PIP_EXTRA_ARGS=() +if [ "$PBS_OS" = "unknown-linux-gnu" ]; then + # Linux needs CUDA PyTorch wheels from the PyTorch index + PIP_EXTRA_ARGS+=(--extra-index-url "https://download.pytorch.org/whl/cu128") +fi +# On macOS, no --extra-index-url needed: standard PyPI torch includes MPS support "$PYTHON_EXE" -m pip install -r "$REQUIREMENTS_FILE" \ - --no-warn-script-location --quiet + --no-warn-script-location --quiet "${PIP_EXTRA_ARGS[@]+"${PIP_EXTRA_ARGS[@]}"}" echo " All dependencies installed" @@ -178,17 +192,22 @@ find "$OUTPUT_PATH/lib" -type d -name "test" -exec rm -rf {} + 2>/dev/null || tr # Remove files only needed for building native extensions, not at runtime. # This cuts ~14k files and speeds up macOS codesigning dramatically. -# NOTE: Windows needs .h files for sageattention/triton — this script is macOS only. -rm -rf "$OUTPUT_PATH/include" "$OUTPUT_PATH/share" 2>/dev/null || true -find "$OUTPUT_PATH/lib" -type d -name "include" -exec rm -rf {} + 2>/dev/null || true +# NOTE: Linux needs .h files for sageattention/triton JIT compilation. +if [ "$PBS_OS" = "apple-darwin" ]; then + rm -rf "$OUTPUT_PATH/include" "$OUTPUT_PATH/share" 2>/dev/null || true + find "$OUTPUT_PATH/lib" -type d -name "include" -exec rm -rf {} + 2>/dev/null || true + find "$OUTPUT_PATH" -name "*.h" -delete 2>/dev/null || true + find "$OUTPUT_PATH" -name "*.cuh" -delete 2>/dev/null || true + find "$OUTPUT_PATH" -name "*.cu" -delete 2>/dev/null || true +else + # Linux: keep .h/.cuh/.cu files for triton/sageattention JIT, but remove other build artifacts + rm -rf "$OUTPUT_PATH/share" 2>/dev/null || true +fi find "$OUTPUT_PATH" -name "*.pyi" -delete 2>/dev/null || true find "$OUTPUT_PATH" -name "*.pxd" -delete 2>/dev/null || true find "$OUTPUT_PATH" -name "*.pyx" -delete 2>/dev/null || true find "$OUTPUT_PATH" -name "*.hpp" -delete 2>/dev/null || true find "$OUTPUT_PATH" -name "*.cpp" -delete 2>/dev/null || true -find "$OUTPUT_PATH" -name "*.h" -delete 2>/dev/null || true -find "$OUTPUT_PATH" -name "*.cuh" -delete 2>/dev/null || true -find "$OUTPUT_PATH" -name "*.cu" -delete 2>/dev/null || true find "$OUTPUT_PATH" -name "*.cmake" -delete 2>/dev/null || true # Remove temp directory and generated requirements file @@ -205,12 +224,19 @@ echo "Step 8: Verifying installation..." "$PYTHON_EXE" -c " import sys +import platform print(f' Python: {sys.version}') try: import torch print(f' PyTorch: {torch.__version__}') - mps = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available() - print(f' MPS available: {mps}') + if platform.system() == 'Darwin': + mps = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available() + print(f' MPS available: {mps}') + elif platform.system() == 'Linux': + cuda = torch.cuda.is_available() + print(f' CUDA available: {cuda}') + if cuda: + print(f' CUDA version: {torch.version.cuda}') except ImportError as e: print(f' PyTorch import FAILED: {e}') sys.exit(1) diff --git a/scripts/run-script.js b/scripts/run-script.js new file mode 100644 index 0000000..85b7c93 --- /dev/null +++ b/scripts/run-script.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Runs a bash script on macOS/Linux or a PowerShell script on Windows. +// Usage: node scripts/run-script.js [args...] +// +// Example: node scripts/run-script.js scripts/local-build --skip-python +// macOS/Linux → bash scripts/local-build.sh --skip-python +// Windows → powershell -ExecutionPolicy Bypass -File scripts/local-build.ps1 -SkipPython +// +// Arg conversion for PowerShell: --foo-bar → -FooBar, --foo → -Foo + +import { execSync } from 'child_process' + +let [,, baseName, ...args] = process.argv + +if (!baseName) { + console.error('Usage: node scripts/run-script.js [args...]') + process.exit(1) +} + +// Strip platform-specific extensions so tab-completed paths work +baseName = baseName.replace(/\.(sh|ps1)$/, '') + +if (process.platform === 'win32') { + // Convert --kebab-case args to -PascalCase for PowerShell + const psArgs = args.map(arg => { + if (!arg.startsWith('--')) return arg + return '-' + arg.slice(2).split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('') + }) + const cmd = `powershell -ExecutionPolicy Bypass -File ${baseName}.ps1 ${psArgs.join(' ')}` + execSync(cmd, { stdio: 'inherit' }) +} else { + const cmd = `bash ${baseName}.sh ${args.join(' ')}` + execSync(cmd, { stdio: 'inherit' }) +} diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh index 46c7f07..476a6d8 100755 --- a/scripts/setup-dev.sh +++ b/scripts/setup-dev.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# macOS development setup for LTX Desktop +# macOS / Linux development setup for LTX Desktop set -euo pipefail RED='\033[0;31m' @@ -34,17 +34,26 @@ cd "$PROJECT_DIR/backend" uv sync --extra dev ok "uv sync complete" -# Verify torch + MPS +# Verify torch + accelerator echo "" -echo "Verifying PyTorch MPS support..." -.venv/bin/python -c "import torch; mps=hasattr(torch.backends,'mps') and torch.backends.mps.is_available(); print(f'MPS available: {mps}')" || true +if [ "$(uname -s)" = "Darwin" ]; then + echo "Verifying PyTorch MPS support..." + .venv/bin/python -c "import torch; mps=hasattr(torch.backends,'mps') and torch.backends.mps.is_available(); print(f'MPS available: {mps}')" || true +else + echo "Verifying PyTorch CUDA support..." + .venv/bin/python -c "import torch; cuda=torch.cuda.is_available(); print(f'CUDA available: {cuda}')" || true +fi # ── ffmpeg check ──────────────────────────────────────────────────── echo "" if command -v ffmpeg >/dev/null 2>&1; then ok "ffmpeg found: $(ffmpeg -version 2>&1 | head -1)" else - echo "⚠ ffmpeg not found — install with: brew install ffmpeg" + if [ "$(uname -s)" = "Darwin" ]; then + echo "⚠ ffmpeg not found — install with: brew install ffmpeg" + else + echo "⚠ ffmpeg not found — install with: sudo apt install ffmpeg (or sudo dnf install ffmpeg)" + fi echo " (imageio-ffmpeg bundled binary will be used as fallback)" fi From 9535137a33fe6d7b39e1b982761d1801c46c72ae Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 14:42:48 +0000 Subject: [PATCH 2/8] ffmpeg-util: fix python path lookup --- electron/export/ffmpeg-utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electron/export/ffmpeg-utils.ts b/electron/export/ffmpeg-utils.ts index c8f4f63..cf51127 100644 --- a/electron/export/ffmpeg-utils.ts +++ b/electron/export/ffmpeg-utils.ts @@ -3,6 +3,7 @@ import path from 'path' import fs from 'fs' import { isDev, getCurrentDir } from '../config' import { logger } from '../logger' +import { getPythonDir } from '../python-setup' let activeExportProcess: ChildProcess | null = null @@ -13,12 +14,12 @@ export function findFfmpegPath(): string | null { const imageioRelPath = path.join('Lib', 'site-packages', 'imageio_ffmpeg', 'binaries') binDir = isDev ? path.join(getCurrentDir(), 'backend', '.venv', imageioRelPath) - : path.join(process.resourcesPath, 'python', imageioRelPath) + : path.join(getPythonDir(), imageioRelPath) } else { // macOS/Linux: find lib/python3.X/site-packages dynamically const venvBase = isDev ? path.join(getCurrentDir(), 'backend', '.venv') - : path.join(process.resourcesPath, 'python') + : getPythonDir() const libDir = path.join(venvBase, 'lib') if (fs.existsSync(libDir)) { const pythonDir = fs.readdirSync(libDir).find(e => e.startsWith('python3')) From 1d950124903fb359fe9a08ca75197704529ae948 Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 15:29:48 +0000 Subject: [PATCH 3/8] prepare-python.sh: replace bc with awk the latter is more commonly installed --- scripts/prepare-python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-python.sh b/scripts/prepare-python.sh index 17aa3e1..53b59b4 100755 --- a/scripts/prepare-python.sh +++ b/scripts/prepare-python.sh @@ -262,7 +262,7 @@ except ImportError as e: # Calculate size SIZE_BYTES=$(du -sb "$OUTPUT_PATH" 2>/dev/null | cut -f1 || du -sk "$OUTPUT_PATH" | awk '{print $1 * 1024}') -SIZE_GB=$(echo "scale=2; $SIZE_BYTES / 1073741824" | bc) +SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES / 1073741824}") echo "" echo "========================================" From 253f6cdd388b42ab77aa535a297799269f384609 Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 15:30:33 +0000 Subject: [PATCH 4/8] package.json: add author email and homepage helps to make .dep packages, where this meta is required --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 03e9091..0d7223f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,11 @@ "description": "LTX-2 Video Generation - Desktop App", "type": "module", "main": "dist-electron/main.js", - "author": "Lightricks", + "author": { + "name": "Lightricks", + "email": "ltx-desktop-dev@lightricks.com" + }, + "homepage": "https://github.com/Lightricks/LTX-Desktop", "license": "Apache-2.0", "private": true, "packageManager": "pnpm@10.30.3", From e69fb2679293942c31b3c01ff40e856bf4c7f33e Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 15:31:41 +0000 Subject: [PATCH 5/8] add docker image for buidling linux packages while not inteded for production CI workflows, it nevertheless allows cross-platform builds. --- .dockerignore | 9 +++++++++ scripts/Dockerfile.linux-build | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .dockerignore create mode 100644 scripts/Dockerfile.linux-build diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f316617 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +python-embed +dist +dist-electron +release +backend/.venv +backend/outputs +backend/models +.git diff --git a/scripts/Dockerfile.linux-build b/scripts/Dockerfile.linux-build new file mode 100644 index 0000000..b6a2f2d --- /dev/null +++ b/scripts/Dockerfile.linux-build @@ -0,0 +1,26 @@ +FROM node:24-bookworm + +# System deps for electron-builder (AppImage/deb) + Python build +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates \ + # electron-builder needs these for AppImage/deb packaging + libarchive-tools rpm \ + && rm -rf /var/lib/apt/lists/* + +# uv (Python package manager) +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Layer 1: Node dependencies (cached unless package.json/lockfile change) +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile + +# Layer 2: Everything else +COPY . . + +# Build frontend + prepare Python env + package installer +CMD ["bash", "-c", "bash scripts/prepare-python.sh && pnpm build:frontend && bash scripts/create-installer.sh --platform linux"] From ce452822b4087a871d48a9c9a37a2cf821f6df1c Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Mon, 9 Mar 2026 15:48:08 +0000 Subject: [PATCH 6/8] docs/INSTALLER.md: npx -> pnpm exec --- docs/INSTALLER.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md index f2b6f31..da3316c 100644 --- a/docs/INSTALLER.md +++ b/docs/INSTALLER.md @@ -141,10 +141,10 @@ pnpm install pnpm build:frontend # 4. Build DMG -npx electron-builder --mac +pnpm exec electron-builder --mac # Or build unpacked app (faster, for testing) -npx electron-builder --mac --dir +pnpm exec electron-builder --mac --dir ``` ### Linux @@ -159,10 +159,10 @@ pnpm install pnpm build:frontend # 4. Build AppImage + deb -npx electron-builder --linux +pnpm exec electron-builder --linux # Or build unpacked app (faster, for testing) -npx electron-builder --linux --dir +pnpm exec electron-builder --linux --dir ``` ### Windows @@ -177,5 +177,5 @@ pnpm install pnpm build:frontend # 4. Build installer -npx electron-builder --win +pnpm exec electron-builder --win ``` From bdcb445472882b238217238411620a8c3efe0569 Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Tue, 10 Mar 2026 13:38:04 +0000 Subject: [PATCH 7/8] add support for arm64 on linux --- README.md | 2 +- docs/INSTALLER.md | 4 +++- electron-builder.yml | 8 ++------ electron/python-setup.ts | 1 + scripts/create-installer.sh | 4 +++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8602310..a545318 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ In API-only mode, available resolutions/durations may be limited to what the API ### Linux (local generation) -- Ubuntu 22.04+ or similar distro (x64) +- Ubuntu 22.04+ or similar distro (x64 or arm64) - NVIDIA GPU with CUDA support and **≥32GB VRAM** (more is better) - NVIDIA driver installed (PyTorch bundles the CUDA runtime) - 16GB+ RAM (32GB recommended) diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md index da3316c..f501b8d 100644 --- a/docs/INSTALLER.md +++ b/docs/INSTALLER.md @@ -89,7 +89,9 @@ release/ ``` release/ ├── LTX Desktop-x86_64.AppImage - └── LTX Desktop-amd64.deb + ├── LTX Desktop-amd64.deb + ├── LTX Desktop-arm64.AppImage + └── LTX Desktop-arm64.deb ``` ### Windows diff --git a/electron-builder.yml b/electron-builder.yml index 05275b5..5b4d4d5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -94,12 +94,8 @@ dmg: linux: target: - - target: AppImage - arch: - - x64 - - target: deb - arch: - - x64 + - AppImage + - deb icon: resources/icon.png category: Video artifactName: ${productName}-${arch}.${ext} diff --git a/electron/python-setup.ts b/electron/python-setup.ts index f5868ad..19f5909 100644 --- a/electron/python-setup.ts +++ b/electron/python-setup.ts @@ -247,6 +247,7 @@ function getPythonArchivePrefix(): string { if (process.platform === 'win32') return 'python-embed-win32' if (process.platform === 'linux') { if (process.arch === 'x64') return 'python-embed-linux-x64' + if (process.arch === 'arm64') return 'python-embed-linux-arm64' throw new Error(`Unsupported Linux architecture: ${process.arch}`) } throw new Error(`Python download is not supported on ${process.platform}`) diff --git a/scripts/create-installer.sh b/scripts/create-installer.sh index 31a074d..28f6fe9 100755 --- a/scripts/create-installer.sh +++ b/scripts/create-installer.sh @@ -117,7 +117,9 @@ if [ "$UNPACK" = true ]; then linux) echo "" echo "Unpacked app ready!" - echo "Run: $RELEASE_DIR/linux-unpacked/ltx-desktop" + LINUX_UNPACKED="$RELEASE_DIR/linux-unpacked" + [ -d "$RELEASE_DIR/linux-arm64-unpacked" ] && LINUX_UNPACKED="$RELEASE_DIR/linux-arm64-unpacked" + echo "Run: $LINUX_UNPACKED/ltx-desktop" ;; esac else From 52337a5831654f168504615b6177363995b000d7 Mon Sep 17 00:00:00 2001 From: Alex Gershovich Date: Wed, 11 Mar 2026 17:45:07 +0000 Subject: [PATCH 8/8] linux: update icons --- electron-builder.yml | 2 +- resources/icons/1024x1024.png | Bin 0 -> 5305 bytes resources/icons/128x128.png | Bin 0 -> 720 bytes resources/icons/16x16.png | Bin 0 -> 203 bytes resources/icons/24x24.png | Bin 0 -> 244 bytes resources/icons/256x256.png | Bin 0 -> 1278 bytes resources/icons/32x32.png | Bin 0 -> 271 bytes resources/icons/48x48.png | Bin 0 -> 334 bytes resources/icons/512x512.png | Bin 0 -> 2518 bytes resources/icons/64x64.png | Bin 0 -> 419 bytes 10 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/icons/1024x1024.png create mode 100644 resources/icons/128x128.png create mode 100644 resources/icons/16x16.png create mode 100644 resources/icons/24x24.png create mode 100644 resources/icons/256x256.png create mode 100644 resources/icons/32x32.png create mode 100644 resources/icons/48x48.png create mode 100644 resources/icons/512x512.png create mode 100644 resources/icons/64x64.png diff --git a/electron-builder.yml b/electron-builder.yml index 5b4d4d5..6fb6ff2 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -96,7 +96,7 @@ linux: target: - AppImage - deb - icon: resources/icon.png + icon: resources/icons category: Video artifactName: ${productName}-${arch}.${ext} diff --git a/resources/icons/1024x1024.png b/resources/icons/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..fa9c6babbe4b92db16671942f3ab673d809174fe GIT binary patch literal 5305 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 JYCC?)-vHSKIKuz{ literal 0 HcmV?d00001 diff --git a/resources/icons/128x128.png b/resources/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ca80cf00fc45c6374a3253f36312ee49efcc9a GIT binary patch literal 720 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&B?S0{xB_YX;Q#;s@87?_fB*jZ z^XK>P-@ktS`UMLX%%4Ah!h{L^{rwFM4fXZ)1qB89`S}S63Gwmq0RaIH4h{we1_}xa z0s;b|dM~7b7I2pY`33)H5KwUFpD=&H`TK{oJslVrm_$5X978H@y}kW*(kuso)`!L` zBy!k#CJH>Nx${5PZS%B-vl*EQYP+wVkH5Kg`?od9QL0yNR&91JIa;>#+=7s8j!LuN z2u%?6Saw3>3L9sxNSH@fukNH%feI^STLhdq6v4!i=gDVzg9@%#D>SxMMKP&7@Min* z?pVT+Qt|3nQ}0ea9KURL;pF`nAMLdLYQ?i-(jlI6I=piVo_>9?Yq>_r1&OK!$>CGv zPIaIXp z88ZLiuoo~taB;@BiOf8`Q}yy1u9qzDlP*wM)+>2IFnpIQ(~0KdS!yA5>l&u9Nfofq zDydOlV3geVw=w#)o{e;a@p|?G-Gt@x3Csq!&I<%E?_0t-OSb7w;yi{c{Y-Ti11?{< z@V=7CN1y^E}a!>(2`k@>9G6v!B>?`IR+2Z|8KXy7+~$qV3lMdzu;kh*~%@O zSspBjWRPBPh<$y$jlk;Xj19m2lCP>}|B^eiry@JWwD8Z1uXp$D+WmCn(=We2JUgo1 zWbP=IocE`}*xc^J%~jL=XSKp3f@QmN5Nnaj)}vCMYZnx$O!?0r|2HhM&*hVk=8DZ% iw=-RQ`)O;C*I&loq^eDhTn=oYbm{5p=d#Wzp$Py#|1yXG literal 0 HcmV?d00001 diff --git a/resources/icons/16x16.png b/resources/icons/16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..54cb7692a165c02ce9bd05e479b39f63f171f699 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6CV9FzhE&{Yoyg1StSI8PpQAfr zig+ULHnzwU>u9G#6urF=I_d<7+udf>cw~#om%y%UnTw4lNq;iUeB1I zy6j5mf$$B*vg{X+^m;u!=XPZ4^!JP|E!!0e76mlEH5YhVTm8UczN4bBg@($Uzbw<` z-W+K=zERlXHV?lO|FRY9nryE6ubsZMY{|kiMVGj1ehFJVFWj;d=sE^ZS3j3^P6mO3-w*;Cxb*jo?4W2Xkpn&!SxT5 zXD*++*jdYI`Wo>E<_*>ca*Ug^9X#H#y?NI;ai($5AKnE%X9HdzVVO}qXO@GHX+p6- zN7Mt+Os0kGTlFt1Zf)ScIh85mMB#qXgiMB|W?SAFw4GloyCqAhxHpeuzChTPypQtv rC#x9WcHQ6igC*|7zrYC9ZW~5RpQ=^+ZTG|iy}{t=>gTe~DWM4f*F<0d literal 0 HcmV?d00001 diff --git a/resources/icons/256x256.png b/resources/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6a5de7ec0852b3d472d58f092e48def4fbc57f GIT binary patch literal 1278 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo`_;1OBOz`!jG!i)^F=172) z6bHFGF|0c$^AgC(5AX?b1=7TW|NsBLfB*je{rl(7pWnZK|Aq}4)~{c`V8Md<^XE^P zFrmM{zoDU_zP`Sopddd#KOrF@K0ZDmAi%-F!N9;kK|w)4K)~#}kQdNC{*oZS;QtH) z3JLiI^B1h&u>bu1_uDnJW-u_Y@Orv9hE&{od*@-$Vh@Sdhs}wlvCZQN?)ud>d6|!wiu}7v?L2}v?UtQnowRxXJi)m-e|a`kI!)O& z+q=QH%_DVoKSQ+3>z6VLu1DSpt4Jz+t7#Vr^#0hf!Zx9MMXG`l5nz$v--UlxZS80^ zH9GR8!$tFOmdl|N8=4p;8cW~2y?YST=aUlMhFdGN zK4hvVmz`U06x1T&P~W3mS2n@0^Z;X(a-Z@Bqhntf*&WpTlnqY#OJp&G+~WAc$Rgt) z-)P6s0;U=om|rNrkYN)jV5ntT7S1kE0;H=;86L7b`Gr7H!%yWEVA7!W0^_s(0}wUJ zAe9LlUNQXQcyo&R%PZXrr>75RO}+Aa5>tsB z;}^y&4vXisdUH11F?PtGvR-FS@I5n!w~`6^i+irV6P(4$`l5I}!`!mCIQfH_mW&sE zuxvQ?(09Y!o~oDAg&A7i8-gl@efIA7#?6&*_6ft<+unZykCmS0YzVe9Y%uLxpPzq< z_n{Ee4snebwg9X7Q=c|59hjxb_%GDY;rQW4wT_Gl_wO*@s5!Lz!PWVD6-_oX*JSl( zHw0NS&UFt++!nRx>VYFH>vw2OaXF`4(W<|9{kxX+yUdjzN4;HTR(5N0NTS_tVSCfI zxwr0ifBNz*?bhG7vx@}Im1MnoIg9W5jdwTx{o3GOvnkhh!CS*ctK0X_`+s3d2QlE9 zmh+WuKcoYHvuiGz_UN6Y;AtV1?;R1d6g=k%tj#Ur+i+4~WzJ_og_}8oOPBi=@fA!< t>p1thwy5oiQtcfjrJK%G literal 0 HcmV?d00001 diff --git a/resources/icons/32x32.png b/resources/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..5e73f03d645d12a2230e4d87c9d42bba60006c59 GIT binary patch literal 271 zcmV+q0r38bP)kdg0002iNklCP-AfXbFFbPP&0TPOXNr%0r6{IA zLO5+ttaUAySrWn%DjbMVWvK<=0{jx-(7~Z82h2!nKLb`1E#G1mBn>e!2;H*Fnt2OQ z*+!37jmvrZC3R;3ZO(b+7|?)ik^|dC4#=9p2Y}s6Y#=~BehHvORyYET=+!p^_?Vr} zpYhavA7Dz5)x^=h-XTfATY$U)2!pMKRn2RjuHX6J08$GGRrTo$k848sqH-W6dH`ij VbHUb2t1SQk002ovPDHLkV1nzTYqkIY literal 0 HcmV?d00001 diff --git a/resources/icons/48x48.png b/resources/icons/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..00860d8569d7d66b61243d58faaa5a329d086e9e GIT binary patch literal 334 zcmV-U0kQsxP)2`gLnk~Z0gKIaRkP5Oh9 zh6G7+3QD4QEpqGfNmQzIB^FV!f<)CywE-H?fCe<66cAVwY6J1>fykPRjr810kC?3M z5W-{qV}Qe6d+x;}9Lb*RgdqSU)}nJoB-(xf4C(Hqs%c?Rv0z8)=`+1~i}nUkIeyKoAw{qWV8W guEbJh@gOJt1LEQrH2bO&ApigX07*qoM6N<$f)taCY5)KL literal 0 HcmV?d00001 diff --git a/resources/icons/512x512.png b/resources/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..e8cfd537f4523a08dd1a9e54a242ed327fb8be0f GIT binary patch literal 2518 zcmb_edpMM7AAa7M!I;5JJ95aS#+RimQG<`k;dRaySrS$sW0RsLBa)KlWii)=SS3=7 zHitrHkQ@pV?Utn|=ko~3$T?zm%zUHW@B91vu4{jPJoo+l?%(y?&vU>3yg#|QIw|6| z;Q#=N&Ijz>0YFJ93SiMvXfD)#DTQ-xhmJT%@&7G}L^3}=KRP`vGX|G#`e*9`^Q~20LY`9?QM=k4a^RwMBe*N9U1#sbH8(E)de_e<#+~`@R^xJ zzi#>h!^z__m@LpoOu@(&o zM}K&5Wz7JfY2~?IC43AwkWj)sCsRXX9~~Rf)9;UodW1c=s&=JZx`uOQydO>R|B!sW zytoISa$PK%OIC!Z{KkE*9sPLRdC?*xh4-Q&<)omr zpY`@RW%glz1-j3h0+9g*0Yre}#r#EJfCbQhx?eU7hY$!01BaE_0A^hC-h_h@tc^6i z-06aihU2OYx>ZF2A>*dFFLxrV$zrxggIc?GjAAC~BBGt4?}i4-sTQu4$@ChfnW(Zd9_A%i5xu(e&cVdyx_KOJ1f5Ld?uu`WGy z9CSmTT)Lddt3l_uj=o15w?@#PLs4{#R5 zeVg(I8kN;z6(7ukGd&CbtJ?q>lv*@)f`DjCk3K}f)Ro$O#WRsfD}Jh#W(_b5Pbl85 z?ac`SfZBqsyW2el%1s;}Ic>yzw&;&vLn>cS>tOi;6qaRnTO0XCfo$xc^nzqV>GSVC zjnC#TfQIL_=e3Tt6Ysx$i~jJgO;92n6Kd&OBHWZ^s$b`m)222cz*nHScajC?U>0@^ zAKZHKZG_j#YPu(v308OvrXKC%{vArSv;#h5!S?fIf*QvNBiH}dp3Q) zMeS0P`Z2J7`gUNT8+fVj{p$cH>vjeE$OaT?L|<%`e(=BSsq*|n^Z`|AenS!9?lo0s zl=rH79*IYn<0fVL;HBcJ@q$QH%@}akH0+V*N22?lw|3WqvnGh?wE$g2Ly_DartAa_ z1p&icIqwJ{xq3Vp<7SL3wPUtp7|~Eym_&FuGXA@~{-*xx* z7}9afMU_gES3w%LIUda!>dwmVOwQa=8DJHkb(?xHC!%j4|PtY#Z}?a zK=2$8w7QP1+tGB9yTF!m9IT^hlqRhUC^cN(Z4)G7F`B>3f`{=mGO}>uH4tP?8a%yj zjlmcx!^J^3G824ss!O-PNo7Ws0kS8^hxPL>57WU9*<(}cv-hjPjPbzmgd?zn@OrD$ryN&(IcoXdusrR6 z|6xsd|6Ipw>j_tI-MXOr+T%Av$&$c}!+7!&L$-}RV6Q0U_s-xnTK+3n% zG<=gngmFa|p9}-{ty9(VMbiyO_WH8J%34PXHP4X@I0pM5*GQ53lw;(g4)@k-Lecpe zC6-xrj9Kg8-3N{}m-o9?LN6N=ZZ%~baJkd1b!UFIhTfy<*Dz1H%$ns)jP>OcL`AcKzmamZeQ0lhnCTDaHHjjsh1+;u1XmP( zNz^PT;R>67tmoTazT~~3RXS436iv1)Lpuhhrr0~?N15*>vSPHC3+0p-YxBj<4zBiP IwzRl^1LAcJr~m)} literal 0 HcmV?d00001 diff --git a/resources/icons/64x64.png b/resources/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..2b7cf223ad9d57dd704e01b867b5039732b846a9 GIT binary patch literal 419 zcmV;U0bKrxP)40?L5-7nXxP%*tU4hB)(1wTeF?YV!8swv~ zS!uNZVBrwJ+pB|pZXduz6<~o5Ojr(J(h8{oRDc2$pa2CZKmiJP8}Pw)ESm!0IOS6T z=K|yz8zQQfj*fv-`d42)bQ-pc&pyQJCLm58ty{{>JIxH&joVN0*8;$baSHKNNn8hz zHzd%|b4p8G2gt^*5EEwOx5ofrK2*;s&2w$sUL1oM{~>@Ja!G7;i1A+y5XT_Ke-8lc zI8Q^|JAvN`S52TID9cmE{S8pk#wEtl05O|p@s-T?0dD&aysD%Vci>A<<*_vT0MM?M zS?i2G%T5!o>+b>uC_n)UP=Epy@E-voHNb%h>l^9+yap_~B%3=};0K?|`1u(C$UXo7 N002ovPDHLkV1gvHuwDQF literal 0 HcmV?d00001