From e750df947f59091c2773a498268394377f30be32 Mon Sep 17 00:00:00 2001 From: Machine King <47542160+taskmasterpeace@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:12:23 -0500 Subject: [PATCH 01/48] rebrand: rename LTX Desktop to Director's Desktop with new logo and icons Replace LTX branding throughout the app with Director's Desktop. New colorful palette+clapperboard logo (SVG + generated PNG/ICO icons). Updated product name, window titles, loading text, about section, and electron-builder config. --- CLAUDE.md | 114 +++++++++++++++++- electron-builder.yml | 4 +- frontend/App.tsx | 30 ++--- frontend/components/FirstRunSetup.tsx | 26 ++-- frontend/components/LtxLogo.tsx | 31 ++++- frontend/components/PythonSetup.tsx | 2 +- frontend/components/SettingsModal.tsx | 62 +++++++--- frontend/components/VideoPlayer.tsx | 21 +++- frontend/lib/keyboard-shortcuts.ts | 2 +- frontend/views/Home.tsx | 6 +- frontend/views/editor/buildMenuDefinitions.ts | 2 +- index.html | 3 +- public/logo.svg | 21 ++++ resources/icon.ico | Bin 12093 -> 36787 bytes resources/icon.png | Bin 21050 -> 13519 bytes 15 files changed, 264 insertions(+), 60 deletions(-) create mode 100644 public/logo.svg diff --git a/CLAUDE.md b/CLAUDE.md index 47dc3e3d..7f25e3e6 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,113 @@ -AGENTS.md \ No newline at end of file +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +LTX Desktop is an open-source Electron app for AI video generation using LTX models. It supports local generation on Windows NVIDIA GPUs (32GB+ VRAM) and API-only mode for unsupported hardware and macOS. + +Three-layer architecture: + +``` +Renderer (React + TS) --HTTP: localhost:8000--> Backend (FastAPI + Python) +Renderer (React + TS) --IPC: window.electronAPI--> Electron main (TS) +Electron main --> OS integration (files, dialogs, ffmpeg, process mgmt) +Backend --> Local models + GPU | External APIs (when API-backed) +``` + +- **Frontend** (`frontend/`): React 18 + TypeScript + Tailwind CSS renderer +- **Electron** (`electron/`): Main process managing app lifecycle, IPC, Python backend process, ffmpeg export. Renderer is sandboxed (`contextIsolation: true`, `nodeIntegration: false`). +- **Backend** (`backend/`): Python FastAPI server (port 8000) handling ML model orchestration and generation + +## Common Commands + +| Command | Purpose | +|---|---| +| `pnpm dev` | Start dev server (Vite + Electron + Python backend) | +| `pnpm dev:debug` | Dev with Electron inspector (port 9229) + Python debugpy | +| `pnpm typecheck` | Run TypeScript (`tsc --noEmit`) and Python (`pyright`) type checks | +| `pnpm typecheck:ts` | TypeScript only | +| `pnpm typecheck:py` | Python pyright only (`cd backend && uv run pyright`) | +| `pnpm backend:test` | Run Python pytest tests (`cd backend && uv sync --frozen --extra test --extra dev && uv run pytest -v --tb=short`) | +| `pnpm build:frontend` | Vite frontend build only | +| `pnpm build:win` / `pnpm build:mac` | Full platform builds (installer) | +| `pnpm build:fast:win` / `pnpm build:fast:mac` | Unpacked build, skip Python bundling | +| `pnpm setup:dev:win` / `pnpm setup:dev:mac` | One-time dev environment setup | + +Run a single backend test: `cd backend && uv run pytest tests/test_generation.py -v --tb=short` + +Run a single test function: `cd backend && uv run pytest tests/test_generation.py::test_name -v --tb=short` + +## CI Checks + +PRs must pass: `pnpm typecheck` + `pnpm backend:test` + frontend Vite build. + +## Frontend Architecture + +- **Path alias**: `@/*` maps to `frontend/*` (configured in `tsconfig.json` and `vite.config.ts`) +- **State management**: React contexts only (`ProjectContext`, `AppSettingsContext`, `KeyboardShortcutsContext`) — no Redux/Zustand +- **Routing**: View-based via `ProjectContext` with views: `home`, `project`, `playground` +- **IPC bridge**: All Electron communication through `window.electronAPI` (defined in `electron/preload.ts`). Key methods: `getBackendUrl`, `readLocalFile`, `checkGpu`, `getAppInfo`, `exportVideo`, `showSaveDialog`, `showItemInFolder` +- **Backend calls**: Frontend calls `http://localhost:8000` directly +- **Styling**: Tailwind with custom semantic color tokens via CSS variables; utilities from `class-variance-authority` + `clsx` + `tailwind-merge` +- **Views**: `Home.tsx`, `GenSpace.tsx`, `Project.tsx`, `Playground.tsx`, `VideoEditor.tsx` (largest frontend file), `editor/` subdirectory +- **No frontend tests** currently exist + +## Backend Architecture + +Request flow: `_routes/* (thin) -> AppHandler -> handlers/* (logic) -> services/* (side effects) + state/* (mutations)` + +Key patterns: +- **Routes** (`_routes/`): Thin plumbing only — parse input, call handler, return typed output. No business logic. +- **AppHandler** (`app_handler.py`): Single composition root owning all sub-handlers, state, and lock. Sub-handlers accessed as `handler.health`, `handler.models`, `handler.downloads`, etc. +- **State** (`state/`): Centralized `AppState` using discriminated union types for state machines (e.g., `GenerationState = GenerationRunning | GenerationComplete | GenerationError | GenerationCancelled`) +- **Services** (`services/`): Protocol interfaces with real implementations and fake test implementations. The test boundary for heavy side effects (GPU, network). +- **Concurrency**: Thread pool with shared `RLock`. Pattern: lock -> read/validate -> unlock -> heavy work -> lock -> write. Never hold lock during heavy compute/IO. Use `handlers.base.with_state_lock` decorator. +- **Exception handling**: Boundary-owned traceback policy. Handlers raise `HTTPError` with `from exc` chaining; `app_factory.py` owns logging. Don't `logger.exception()` then rethrow. +- **Naming**: `*Payload` for DTOs/TypedDicts, `*Like` for structural wrappers, `Fake*` for test implementations + +### Backend Composition Roots + +- `ltx2_server.py`: Runtime bootstrap (logging, `RuntimeConfig`, `AppHandler`, `uvicorn`) +- `app_factory.py`: FastAPI app factory (routers, DI init, exception handling) — importable from tests +- `state/deps.py`: FastAPI dependency hook (`get_state_service()` returns shared `AppHandler`; tests override via `set_state_service_for_tests()`) + +### Backend Testing + +- Integration-first using Starlette `TestClient` against real FastAPI app +- **No mocks**: `test_no_mock_usage.py` enforces no `unittest.mock`. Swap services via `ServiceBundle` fakes only. +- Fakes live in `tests/fakes/`; `conftest.py` wires fresh `AppHandler` per test +- Pyright strict mode is also enforced as a test (`test_pyright.py`) + +### Adding a Backend Feature + +1. Define request/response models in `api_types.py` +2. Add endpoint in `_routes/.py` delegating to handler +3. Implement logic in `handlers/_handler.py` with lock-aware state transitions +4. If new heavy side effect needed, add service in `services/` with Protocol + real + fake implementations +5. Add integration test in `tests/` using fake services + +## TypeScript Config + +- Strict mode with `noUnusedLocals`, `noUnusedParameters` +- Frontend: ES2020 target, React JSX +- Electron main process: ESNext, compiled to `dist-electron/` +- Preload script must be CommonJS (configured in `vite.config.ts` rollup output) + +## Python Config + +- Python 3.12+ required (`.python-version` pins 3.13), managed with `uv` +- Pyright strict mode (`backend/pyrightconfig.json`) — tests are excluded from pyright +- Dependencies in `backend/pyproject.toml`, lock in `backend/uv.lock` +- PyTorch uses CUDA 12.8 index on Windows/Linux (`tool.uv.sources`) + +## Key File Locations + +- Backend architecture doc: `backend/architecture.md` +- Default app settings schema: `settings.json` +- Electron builder config: `electron-builder.yml` +- Video editor (largest frontend file): `frontend/views/VideoEditor.tsx` +- Project types: `frontend/types/project.ts` +- IPC API surface: `electron/preload.ts` +- Python backend entry: `backend/ltx2_server.py` +- Build/setup scripts: `scripts/` (platform-specific `.sh` and `.ps1` variants) diff --git a/electron-builder.yml b/electron-builder.yml index 94753cc6..3d22141a 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,5 +1,5 @@ appId: com.lightricks.ltx-desktop -productName: LTX Desktop +productName: Director's Desktop copyright: Copyright © 2026 Lightricks directories: @@ -55,7 +55,7 @@ nsis: installerHeaderIcon: resources/icon.ico createDesktopShortcut: true createStartMenuShortcut: true - shortcutName: LTX Desktop + shortcutName: Director's Desktop mac: hardenedRuntime: true diff --git a/frontend/App.tsx b/frontend/App.tsx index 425217c5..ff59f7f7 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -22,7 +22,7 @@ type RequiredModelsGateState = 'checking' | 'missing' | 'ready' function AppContent() { const { currentView } = useProjects() const { status, processStatus, isLoading: backendLoading, error: backendError } = useBackend() - const { settings, saveLtxApiKey, saveFalApiKey, forceApiGenerations, isLoaded, runtimePolicyLoaded } = useAppSettings() + const { settings, saveLtxApiKey, saveReplicateApiKey, forceApiGenerations, isLoaded, runtimePolicyLoaded } = useAppSettings() const [pythonReady, setPythonReady] = useState(null) const [backendStarted, setBackendStarted] = useState(false) @@ -36,7 +36,7 @@ function AppContent() { const setupCompletionInFlightRef = useRef | null>(null) type ApiGatewayRequest = { - requiredKeys: Array<'ltx' | 'fal'> + requiredKeys: Array<'ltx' | 'replicate'> title: string description: string blocking?: boolean @@ -299,16 +299,16 @@ function AppContent() { getKeyLabel: 'Get LTX API key', }, { - keyType: 'fal', - title: 'FAL AI', - description: 'Required to generate images with Z Image Turbo.', - required: apiGatewayRequest.requiredKeys.includes('fal'), - isConfigured: settings.hasFalApiKey, - inputLabel: 'FAL AI API key', - placeholder: 'Enter your FAL AI API key...', - onSave: saveFalApiKey, - onGetKey: () => window.electronAPI.openFalApiKeyPage(), - getKeyLabel: 'Get FAL API key', + keyType: 'replicate', + title: 'Replicate', + description: 'Required for cloud image generation.', + required: apiGatewayRequest.requiredKeys.includes('replicate'), + isConfigured: settings.hasReplicateApiKey, + inputLabel: 'Replicate API key', + placeholder: 'Enter your Replicate API key...', + onSave: saveReplicateApiKey, + onGetKey: () => window.electronAPI.openReplicateApiKeyPage(), + getKeyLabel: 'Get Replicate API key', }, ] @@ -321,9 +321,9 @@ function AppContent() { apiGatewayRequest, isForcedFirstRun, saveApiKeyForFirstRun, - saveFalApiKey, + saveReplicateApiKey, saveLtxApiKey, - settings.hasFalApiKey, + settings.hasReplicateApiKey, settings.hasLtxApiKey, ]) @@ -372,7 +372,7 @@ function AppContent() {
-

Starting LTX Desktop...

+

Starting Director's Desktop...

Initializing the inference engine

diff --git a/frontend/components/FirstRunSetup.tsx b/frontend/components/FirstRunSetup.tsx index 750ada31..5dbc129a 100644 --- a/frontend/components/FirstRunSetup.tsx +++ b/frontend/components/FirstRunSetup.tsx @@ -305,13 +305,25 @@ export function LaunchGate({ borderBottom: '1px solid #1a1a1a' }}>
- {/* LTX Logo */} - - - - + {/* Director's Desktop Logo */} + + + + + + + + + + + + + + + + - Desktop + Director's Desktop
@@ -792,7 +804,7 @@ export function LaunchGate({ Ready to Create

- LTX Video is installed. Start generating. + Director's Desktop is ready. Start generating.

{/* Install Summary */} diff --git a/frontend/components/LtxLogo.tsx b/frontend/components/LtxLogo.tsx index 90416414..fe32c403 100644 --- a/frontend/components/LtxLogo.tsx +++ b/frontend/components/LtxLogo.tsx @@ -4,15 +4,34 @@ interface LtxLogoProps { export function LtxLogo({ className = "h-6" }: LtxLogoProps) { return ( - - - - + {/* Palette shape */} + + {/* Clapperboard stripe on top */} + + + + + + + {/* Paint blobs */} + + + + + + + {/* Thumb hole */} + + ) } diff --git a/frontend/components/PythonSetup.tsx b/frontend/components/PythonSetup.tsx index 3b1746f7..69ea5e07 100644 --- a/frontend/components/PythonSetup.tsx +++ b/frontend/components/PythonSetup.tsx @@ -107,7 +107,7 @@ export function PythonSetup({ onReady }: PythonSetupProps) { // @ts-expect-error - Electron-specific CSS property WebkitAppRegion: 'drag' }}> - LTX Desktop + Director's Desktop {/* Main Container */} diff --git a/frontend/components/SettingsModal.tsx b/frontend/components/SettingsModal.tsx index db35f98a..cdc81124 100644 --- a/frontend/components/SettingsModal.tsx +++ b/frontend/components/SettingsModal.tsx @@ -20,14 +20,14 @@ interface SettingsModalProps { type TabId = 'general' | 'apiKeys' | 'inference' | 'promptEnhancer' | 'about' export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProps) { - const { settings, updateSettings, saveLtxApiKey, saveFalApiKey, saveGeminiApiKey, forceApiGenerations } = useAppSettings() + const { settings, updateSettings, saveLtxApiKey, saveReplicateApiKey, saveGeminiApiKey, forceApiGenerations } = useAppSettings() const onSettingsChange = (next: AppSettings) => updateSettings(next) const [activeTab, setActiveTab] = useState('general') const [ltxApiKeyInput, setLtxApiKeyInput] = useState('') const ltxApiKeyInputRef = useRef(null) const [focusLtxApiKeyInputOnTabChange, setFocusLtxApiKeyInputOnTabChange] = useState(false) - const [falApiKeyInput, setFalApiKeyInput] = useState('') - const falApiKeyInputRef = useRef(null) + const [replicateApiKeyInput, setReplicateApiKeyInput] = useState('') + const replicateApiKeyInputRef = useRef(null) const [geminiApiKeyInput, setGeminiApiKeyInput] = useState('') const geminiApiKeyInputRef = useRef(null) const [textEncoderStatus, setTextEncoderStatus] = useState(null) @@ -692,7 +692,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp

- Share anonymous usage data to help improve LTX Desktop. + Share anonymous usage data to help improve Director's Desktop. Only basic technical information is collected — never personal data or generated content.

@@ -780,32 +780,32 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
-

FAL AI

+

Replicate

Optional

- Your FAL AI key is used for generating images with Z Image Turbo when API generations are enabled. + Your Replicate key is used for cloud image generation when API generations are enabled.

setFalApiKeyInput(e.target.value)} - placeholder={settings.hasFalApiKey ? 'Enter new key to replace...' : 'Enter your FAL AI API key...'} + ref={replicateApiKeyInputRef} + value={replicateApiKeyInput} + onChange={(e) => setReplicateApiKeyInput(e.target.value)} + placeholder={settings.hasReplicateApiKey ? 'Enter new key to replace...' : 'Enter your Replicate API key...'} stopPropagation className="flex-1" />
window.electronAPI.openFalApiKeyPage()} + label="Get Replicate API key" + onOpenKey={() => window.electronAPI.openReplicateApiKeyPage()} />
- {settings.hasFalApiKey ? ( + {settings.hasReplicateApiKey ? ( <> Key configured @@ -835,6 +835,30 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp )}
+ +
+ + +
+ +
+ + +
@@ -1144,7 +1168,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
{/* App Identity */}
-

LTX Desktop

+

Director's Desktop

Version {appVersion || '...'}

AI-Powered Video Editor

diff --git a/frontend/components/VideoPlayer.tsx b/frontend/components/VideoPlayer.tsx index 2c3e1cb6..998f5b8b 100644 --- a/frontend/components/VideoPlayer.tsx +++ b/frontend/components/VideoPlayer.tsx @@ -10,6 +10,7 @@ interface VideoPlayerProps { isGenerating: boolean progress: number statusMessage: string + modelName?: string | null } function formatTime(seconds: number): string { @@ -18,7 +19,13 @@ function formatTime(seconds: number): string { return `${mins}:${secs.toString().padStart(2, '0')}` } -export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating, progress, statusMessage }: VideoPlayerProps) { +const MODEL_DISPLAY_NAMES: Record = { + 'ltx-fast': 'LTX Fast', + 'ltx-pro': 'LTX Pro', + 'seedance-1.5-pro': 'Seedance 1.5 Pro', +} + +export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating, progress, statusMessage, modelName }: VideoPlayerProps) { const videoRef = useRef(null) const progressRef = useRef(null) const [isPlaying, setIsPlaying] = useState(false) @@ -254,7 +261,7 @@ export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating const a = document.createElement('a') a.href = displayedVideoUrl const suffix = showingUpscaled ? '-upscaled' : '' - a.download = `ltx-desktop${suffix}-${Date.now()}.mp4` + a.download = `directors-desktop${suffix}-${Date.now()}.mp4` document.body.appendChild(a) a.click() document.body.removeChild(a) @@ -438,7 +445,15 @@ export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating
)} - + + {/* Model badge */} + {modelName && ( +
+
+ {MODEL_DISPLAY_NAMES[modelName] || modelName} +
+ )} +
{/* Video controls bar */} diff --git a/frontend/lib/keyboard-shortcuts.ts b/frontend/lib/keyboard-shortcuts.ts index 47e114fa..1a833321 100644 --- a/frontend/lib/keyboard-shortcuts.ts +++ b/frontend/lib/keyboard-shortcuts.ts @@ -363,7 +363,7 @@ export const BUILT_IN_PRESETS: KeyboardPreset[] = [ { id: 'ltx-default', name: 'LTX Default', - description: 'Default keyboard layout for LTX Desktop', + description: "Default keyboard layout for Director's Desktop", layout: LTX_DEFAULT_LAYOUT, builtIn: true, }, diff --git a/frontend/views/Home.tsx b/frontend/views/Home.tsx index 05c3aa04..ffab8c7d 100644 --- a/frontend/views/Home.tsx +++ b/frontend/views/Home.tsx @@ -132,7 +132,7 @@ export function Home() { const getDefaultAssetPath = (name: string) => { if (!defaultDownloadsPath) return '' const sep = defaultDownloadsPath.includes('\\') ? '\\' : '/' - return `${defaultDownloadsPath}${sep}Ltx Desktop Assets${sep}${name}` + return `${defaultDownloadsPath}${sep}Directors Desktop Assets${sep}${name}` } const handleCreateProject = () => { @@ -244,7 +244,7 @@ export function Home() { {/* Dark overlay for text readability */}
-

LTX Desktop

+

Director's Desktop

Create and manage your video projects

@@ -313,7 +313,7 @@ export function Home() { type="text" value={newProjectAssetPath || (newProjectName.trim() ? getDefaultAssetPath(newProjectName.trim()) : '')} onChange={(e) => setNewProjectAssetPath(e.target.value)} - placeholder={newProjectName.trim() ? getDefaultAssetPath(newProjectName.trim()) : 'Downloads/Ltx Desktop Assets/...'} + placeholder={newProjectName.trim() ? getDefaultAssetPath(newProjectName.trim()) : 'Downloads/Directors Desktop Assets/...'} className="flex-1 px-3 py-2 rounded-lg bg-zinc-800 border border-zinc-700 text-zinc-300 text-sm placeholder:text-zinc-600 focus:outline-none focus:border-blue-500 truncate" /> + + {label} + + + ) : ( +
+ + {label} + Paste, drop, or click +
+ )} + + ) +} diff --git a/frontend/components/SettingsPanel.tsx b/frontend/components/SettingsPanel.tsx index a788d99c..cd9f7406 100644 --- a/frontend/components/SettingsPanel.tsx +++ b/frontend/components/SettingsPanel.tsx @@ -81,12 +81,13 @@ export function SettingsPanel({ onChange={(e) => handleChange('imageAspectRatio', e.target.value)} disabled={disabled} > - - - - - - + + + + + + + + + {/* Variations Slider */} +
+
+ + {settings.variations || 1} +
+ handleChange('variations', parseInt(e.target.value))} + disabled={disabled} + className="w-full h-1.5 bg-zinc-700 rounded-full appearance-none cursor-pointer accent-blue-500" + /> +
+ 1 + 12 +
+
) } @@ -179,11 +201,11 @@ export function SettingsPanel({ disabled={disabled} > {hasAudio ? ( - + ) : ( <> - - + + )} diff --git a/frontend/hooks/use-generation.ts b/frontend/hooks/use-generation.ts index e3be5d12..03c86955 100644 --- a/frontend/hooks/use-generation.ts +++ b/frontend/hooks/use-generation.ts @@ -30,7 +30,7 @@ interface GenerationState { } interface UseGenerationReturn extends GenerationState { - generate: (prompt: string, imagePath: string | null, settings: GenerationSettings, audioPath?: string | null) => Promise + generate: (prompt: string, imagePath: string | null, settings: GenerationSettings, audioPath?: string | null, lastFramePath?: string | null) => Promise generateImage: (prompt: string, settings: GenerationSettings) => Promise cancel: () => void reset: () => void @@ -49,6 +49,7 @@ const IMAGE_ASPECT_RATIO_VALUE: Record = { '9:16': 9 / 16, '4:3': 4 / 3, '3:4': 3 / 4, + '4:5': 4 / 5, '21:9': 21 / 9, } @@ -213,6 +214,7 @@ export function useGeneration(): UseGenerationReturn { imagePath: string | null, settings: GenerationSettings, audioPath?: string | null, + lastFramePath?: string | null, ) => { const statusMsg = settings.model === 'pro' ? 'Loading Pro model & generating...' @@ -248,6 +250,9 @@ export function useGeneration(): UseGenerationReturn { if (audioPath) { params.audioPath = audioPath } + if (lastFramePath) { + params.lastFramePath = lastFramePath + } const response = await fetch(`${backendUrl}/api/queue/submit`, { method: 'POST', diff --git a/frontend/views/Playground.tsx b/frontend/views/Playground.tsx index 5239b27f..f269e81f 100644 --- a/frontend/views/Playground.tsx +++ b/frontend/views/Playground.tsx @@ -1,10 +1,11 @@ import { useState, useRef, useEffect } from 'react' -import { Sparkles, Trash2, Square, ImageIcon, ArrowLeft, Scissors } from 'lucide-react' +import { Sparkles, Trash2, Square, ImageIcon, ArrowLeft, Scissors, Wand2 } from 'lucide-react' import { logger } from '../lib/logger' import { ImageUploader } from '../components/ImageUploader' import { AudioUploader } from '../components/AudioUploader' import { VideoPlayer } from '../components/VideoPlayer' import { ImageResult } from '../components/ImageResult' +import { FrameSlot } from '../components/FrameSlot' import { SettingsPanel, type GenerationSettings } from '../components/SettingsPanel' import { ModeTabs, type GenerationMode } from '../components/ModeTabs' import { LtxLogo } from '../components/LtxLogo' @@ -42,6 +43,11 @@ export function Playground() { const [selectedImage, setSelectedImage] = useState(null) const [selectedAudio, setSelectedAudio] = useState(null) const [settings, setSettings] = useState(() => ({ ...DEFAULT_SETTINGS })) + const [firstFrameUrl, setFirstFrameUrl] = useState(null) + const [firstFramePath, setFirstFramePath] = useState(null) + const [lastFrameUrl, setLastFrameUrl] = useState(null) + const [lastFramePath, setLastFramePath] = useState(null) + const [isEnhancing, setIsEnhancing] = useState(false) const { status, processStatus } = useBackend() @@ -103,6 +109,27 @@ export function Playground() { // Ref to store generated image URL for "Create video" flow const generatedImageRef = useRef(null) + const handleEnhancePrompt = async () => { + if (!prompt.trim() || isEnhancing) return + setIsEnhancing(true) + try { + const backendUrl = await window.electronAPI.getBackendUrl() + const res = await fetch(`${backendUrl}/api/enhance-prompt`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, mode }), + }) + if (res.ok) { + const data = await res.json() + if (data.enhancedPrompt) setPrompt(data.enhancedPrompt) + } + } catch (err) { + logger.error(`Failed to enhance prompt: ${err}`) + } finally { + setIsEnhancing(false) + } + } + const handleGenerate = () => { if (mode === 'retake') { if (!retakeInput.videoPath || retakeInput.duration < 2) return @@ -118,18 +145,16 @@ export function Playground() { if (mode === 'text-to-image') { if (!prompt.trim()) return - // Text-to-image behavior remains tied to raw forceApiGenerations in useGeneration. generateImage(prompt, settings) } else { const effectiveVideoSettings = shouldVideoGenerateWithLtxApi ? sanitizeForcedApiVideoSettings(settings) : settings - // Auto-detect: if image is loaded → I2V, otherwise → T2V if (!prompt.trim()) return - const imagePath = selectedImage ? fileUrlToPath(selectedImage) : null + const imagePath = selectedImage ? fileUrlToPath(selectedImage) : (firstFramePath || null) const audioPath = selectedAudio ? fileUrlToPath(selectedAudio) : null if (audioPath) effectiveVideoSettings.model = 'pro' - generate(prompt, imagePath, effectiveVideoSettings, audioPath) + generate(prompt, imagePath, effectiveVideoSettings, audioPath, lastFramePath) } } @@ -150,6 +175,10 @@ export function Playground() { setPrompt('') setSelectedImage(null) setSelectedAudio(null) + setFirstFrameUrl(null) + setFirstFramePath(null) + setLastFrameUrl(null) + setLastFramePath(null) const baseDefaults = { ...DEFAULT_SETTINGS } const shouldSanitizeVideoSettings = shouldVideoGenerateWithLtxApi && mode !== 'text-to-image' setSettings(shouldSanitizeVideoSettings ? sanitizeForcedApiVideoSettings(baseDefaults) : baseDefaults) @@ -242,17 +271,45 @@ export function Playground() { /> )} + {/* First / Last Frame Slots */} + {isVideoMode && !isRetakeMode && ( +
+ { setFirstFrameUrl(url); setFirstFramePath(path) }} + disabled={isBusy} + /> + { setLastFrameUrl(url); setLastFramePath(path) }} + disabled={isBusy} + /> +
+ )} + {/* Prompt Input */} -