diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f3166178 --- /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/.github/workflows/ci.yml b/.github/workflows/ci.yml index df5dddac..ea58a42a 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 5a634664..92828aea 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 acc7f4b2..a5453180 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 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) +- 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 797e3bf8..a546d244 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 cf64b66b..7b84dc89 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 0999f709..f4f8ba00 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 07a19c05..c49d255e 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 a642e8bb..b0f9dedf 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 fce6f70f..f501b8df 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,15 @@ release/ └── LTX Desktop--arm64.dmg ``` +### Linux +``` +release/ + ├── LTX Desktop-x86_64.AppImage + ├── LTX Desktop-amd64.deb + ├── LTX Desktop-arm64.AppImage + └── LTX Desktop-arm64.deb +``` + ### Windows ``` release/ @@ -118,7 +104,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 +123,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 @@ -156,10 +143,28 @@ pnpm install pnpm build:frontend # 4. Build DMG -npx electron-builder --mac +pnpm exec electron-builder --mac + +# Or build unpacked app (faster, for testing) +pnpm exec 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 +pnpm exec electron-builder --linux # Or build unpacked app (faster, for testing) -npx electron-builder --mac --dir +pnpm exec electron-builder --linux --dir ``` ### Windows @@ -174,5 +179,5 @@ pnpm install pnpm build:frontend # 4. Build installer -npx electron-builder --win +pnpm exec electron-builder --win ``` diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index b1e8a3eb..02324a0a 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 94753cc6..6fb6ff23 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -92,6 +92,26 @@ dmg: type: link path: /Applications +linux: + target: + - AppImage + - deb + icon: resources/icons + 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 ddf10e6f..40a36115 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/export/ffmpeg-utils.ts b/electron/export/ffmpeg-utils.ts index c8f4f631..cf511276 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')) diff --git a/electron/python-setup.ts b/electron/python-setup.ts index c429b4d3..19f59092 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,16 @@ 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' + 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}`) +} + 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 +266,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 +326,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 +407,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 +435,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 b3f33aaf..8caeb293 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 0005158e..010c6dcc 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 000eb816..0d7223fb 100644 --- a/package.json +++ b/package.json @@ -4,27 +4,26 @@ "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", "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/resources/icons/1024x1024.png b/resources/icons/1024x1024.png new file mode 100644 index 00000000..fa9c6bab Binary files /dev/null and b/resources/icons/1024x1024.png differ diff --git a/resources/icons/128x128.png b/resources/icons/128x128.png new file mode 100644 index 00000000..d2ca80cf Binary files /dev/null and b/resources/icons/128x128.png differ diff --git a/resources/icons/16x16.png b/resources/icons/16x16.png new file mode 100644 index 00000000..54cb7692 Binary files /dev/null and b/resources/icons/16x16.png differ diff --git a/resources/icons/24x24.png b/resources/icons/24x24.png new file mode 100644 index 00000000..132f4537 Binary files /dev/null and b/resources/icons/24x24.png differ diff --git a/resources/icons/256x256.png b/resources/icons/256x256.png new file mode 100644 index 00000000..6a6a5de7 Binary files /dev/null and b/resources/icons/256x256.png differ diff --git a/resources/icons/32x32.png b/resources/icons/32x32.png new file mode 100644 index 00000000..5e73f03d Binary files /dev/null and b/resources/icons/32x32.png differ diff --git a/resources/icons/48x48.png b/resources/icons/48x48.png new file mode 100644 index 00000000..00860d85 Binary files /dev/null and b/resources/icons/48x48.png differ diff --git a/resources/icons/512x512.png b/resources/icons/512x512.png new file mode 100644 index 00000000..e8cfd537 Binary files /dev/null and b/resources/icons/512x512.png differ diff --git a/resources/icons/64x64.png b/resources/icons/64x64.png new file mode 100644 index 00000000..2b7cf223 Binary files /dev/null and b/resources/icons/64x64.png differ diff --git a/scripts/Dockerfile.linux-build b/scripts/Dockerfile.linux-build new file mode 100644 index 00000000..b6a2f2d8 --- /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"] diff --git a/scripts/create-installer.sh b/scripts/create-installer.sh index 02310f09..28f6fe93 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,13 @@ if [ "$UNPACK" = true ]; then echo "Unpacked app ready!" echo "Run: $RELEASE_DIR/win-unpacked/LTX Desktop.exe" ;; + linux) + echo "" + echo "Unpacked app ready!" + 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 echo "" diff --git a/scripts/prepare-python.sh b/scripts/prepare-python.sh index 544bfacf..53b59b47 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) @@ -236,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 "========================================" diff --git a/scripts/run-script.js b/scripts/run-script.js new file mode 100644 index 00000000..85b7c931 --- /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 46c7f070..476a6d8f 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