Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/state/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ class AppSettings(SettingsBaseModel):
locked_seed: int = 42
models_dir: str = ""

# Generation defaults (persisted across sessions)
default_model: str = "fast"
default_duration: int = 5
default_video_resolution: str = "540p"
default_fps: int = 24
default_aspect_ratio: str = "16:9"
default_camera_motion: str = "none"

# Player preferences
player_muted: bool = False

@field_validator("prompt_cache_size", mode="before")
@classmethod
def _clamp_prompt_cache_size(cls, value: Any) -> int:
Expand All @@ -86,6 +97,16 @@ def _clamp_prompt_cache_size(cls, value: Any) -> int:
def _clamp_locked_seed(cls, value: Any) -> int:
return _clamp_int(value, minimum=0, maximum=2_147_483_647, default=42)

@field_validator("default_duration", mode="before")
@classmethod
def _clamp_default_duration(cls, value: Any) -> int:
return _clamp_int(value, minimum=1, maximum=20, default=5)

@field_validator("default_fps", mode="before")
@classmethod
def _clamp_default_fps(cls, value: Any) -> int:
return _clamp_int(value, minimum=1, maximum=60, default=24)


SettingsModelT = TypeVar("SettingsModelT", bound=SettingsBaseModel)
_PARTIAL_MODEL_CACHE: dict[type[SettingsBaseModel], type[SettingsPatchModel]] = {}
Expand Down Expand Up @@ -148,6 +169,17 @@ class SettingsResponse(SettingsBaseModel):
locked_seed: int = 42
models_dir: str = ""

# Generation defaults
default_model: str = "fast"
default_duration: int = 5
default_video_resolution: str = "540p"
default_fps: int = 24
default_aspect_ratio: str = "16:9"
default_camera_motion: str = "none"

# Player preferences
player_muted: bool = False


def to_settings_response(settings: AppSettings) -> SettingsResponse:
data = settings.model_dump(by_alias=False)
Expand Down
10 changes: 7 additions & 3 deletions frontend/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'
import { Play, Pause, Download, RefreshCw, RotateCcw, Volume2, VolumeX, Maximize2 } from 'lucide-react'
import { Button } from './ui/button'
import { logger } from '../lib/logger'
import { useAppSettings } from '../contexts/AppSettingsContext'

interface VideoPlayerProps {
videoUrl: string | null
Expand All @@ -19,14 +20,15 @@ function formatTime(seconds: number): string {
}

export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating, progress, statusMessage }: VideoPlayerProps) {
const { settings: appSettings, updateSettings: updateAppSettings } = useAppSettings()
const videoRef = useRef<HTMLVideoElement>(null)
const progressRef = useRef<HTMLDivElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const [isLooping, setIsLooping] = useState(true)
const [isMuted, setIsMuted] = useState(false)
const [isMuted, setIsMuted] = useState(appSettings.playerMuted)
const [isHovering, setIsHovering] = useState(false)
const [hasBeenUpscaled, setHasBeenUpscaled] = useState(false)
const [_currentResolution, setCurrentResolution] = useState<string | null>(null)
Expand Down Expand Up @@ -203,8 +205,10 @@ export function VideoPlayer({ videoUrl, videoPath, videoResolution, isGenerating

const toggleMute = () => {
if (videoRef.current) {
videoRef.current.muted = !isMuted
setIsMuted(!isMuted)
const newMuted = !isMuted
videoRef.current.muted = newMuted
setIsMuted(newMuted)
updateAppSettings({ playerMuted: newMuted })
}
}

Expand Down
33 changes: 33 additions & 0 deletions frontend/contexts/AppSettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export interface AppSettings {
seedLocked: boolean
lockedSeed: number
modelsDir: string

// Generation defaults (persisted across sessions)
defaultModel: string
defaultDuration: number
defaultVideoResolution: string
defaultFps: number
defaultAspectRatio: string
defaultCameraMotion: string

// Player preferences
playerMuted: boolean
}

export const DEFAULT_APP_SETTINGS: AppSettings = {
Expand All @@ -44,6 +55,17 @@ export const DEFAULT_APP_SETTINGS: AppSettings = {
seedLocked: false,
lockedSeed: 42,
modelsDir: '',

// Generation defaults
defaultModel: 'fast',
defaultDuration: 5,
defaultVideoResolution: '540p',
defaultFps: 24,
defaultAspectRatio: '16:9',
defaultCameraMotion: 'none',

// Player preferences
playerMuted: false,
}

type BackendProcessStatus = 'alive' | 'restarting' | 'dead'
Expand Down Expand Up @@ -92,6 +114,17 @@ function normalizeAppSettings(data: Partial<AppSettings>): AppSettings {
seedLocked: data.seedLocked ?? DEFAULT_APP_SETTINGS.seedLocked,
lockedSeed: data.lockedSeed ?? DEFAULT_APP_SETTINGS.lockedSeed,
modelsDir: data.modelsDir ?? DEFAULT_APP_SETTINGS.modelsDir,

// Generation defaults
defaultModel: data.defaultModel ?? DEFAULT_APP_SETTINGS.defaultModel,
defaultDuration: data.defaultDuration ?? DEFAULT_APP_SETTINGS.defaultDuration,
defaultVideoResolution: data.defaultVideoResolution ?? DEFAULT_APP_SETTINGS.defaultVideoResolution,
defaultFps: data.defaultFps ?? DEFAULT_APP_SETTINGS.defaultFps,
defaultAspectRatio: data.defaultAspectRatio ?? DEFAULT_APP_SETTINGS.defaultAspectRatio,
defaultCameraMotion: data.defaultCameraMotion ?? DEFAULT_APP_SETTINGS.defaultCameraMotion,

// Player preferences
playerMuted: data.playerMuted ?? DEFAULT_APP_SETTINGS.playerMuted,
}
}

Expand Down
49 changes: 40 additions & 9 deletions frontend/views/GenSpace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -836,12 +836,8 @@ const gallerySizeClasses: Record<GallerySize, string> = {
large: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3',
}

const DEFAULT_VIDEO_SETTINGS = {
model: 'fast',
duration: 5,
videoResolution: '540p',
fps: 24,
aspectRatio: '16:9',
// Fallback defaults for fields not persisted in appSettings
const IMAGE_SETTINGS_DEFAULTS = {
imageResolution: '1080p',
variations: 1,
audio: true,
Expand All @@ -867,7 +863,7 @@ export function GenSpace() {
setGenSpaceIcLoraSource,
setPendingIcLoraUpdate,
} = useProjects()
const { shouldVideoGenerateWithLtxApi, forceApiGenerations, settings: appSettings } = useAppSettings()
const { shouldVideoGenerateWithLtxApi, forceApiGenerations, settings: appSettings, updateSettings: updateAppSettings, isLoaded: appSettingsLoaded } = useAppSettings()
const [mode, setMode] = useState<'image' | 'video' | 'retake' | 'ic-lora'>('video')
const [prompt, setPrompt] = useState('')
const [inputImage, setInputImage] = useState<string | null>(null)
Expand Down Expand Up @@ -897,7 +893,42 @@ export function GenSpace() {
conditioningStrength: number
}
} | null>(null)
const [settings, setSettings] = useState(() => ({ ...DEFAULT_VIDEO_SETTINGS }))

// Initialize settings from persisted appSettings
const [settings, setSettings] = useState(() => ({
model: appSettings.defaultModel,
duration: appSettings.defaultDuration,
videoResolution: appSettings.defaultVideoResolution,
fps: appSettings.defaultFps,
aspectRatio: appSettings.defaultAspectRatio,
...IMAGE_SETTINGS_DEFAULTS,
}))

// Sync settings from appSettings when loaded
useEffect(() => {
if (!appSettingsLoaded) return
setSettings(prev => ({
...prev,
model: appSettings.defaultModel,
duration: appSettings.defaultDuration,
videoResolution: appSettings.defaultVideoResolution,
fps: appSettings.defaultFps,
aspectRatio: appSettings.defaultAspectRatio,
}))
}, [appSettingsLoaded]) // eslint-disable-line react-hooks/exhaustive-deps

// Persist generation defaults when user changes them
const handleSettingsChange = useCallback((newSettings: typeof settings) => {
setSettings(newSettings)
// Sync persisted fields to appSettings
updateAppSettings({
defaultModel: newSettings.model,
defaultDuration: newSettings.duration,
defaultVideoResolution: newSettings.videoResolution,
defaultFps: newSettings.fps,
defaultAspectRatio: newSettings.aspectRatio,
})
}, [updateAppSettings])
const applyForcedVideoSettings = useCallback(
(next: { model: string; duration: number; videoResolution: string; fps: number; audio: boolean; aspectRatio: string; imageResolution: string; variations: number }) => {
if (!shouldVideoGenerateWithLtxApi || mode !== 'video') return next
Expand Down Expand Up @@ -1661,7 +1692,7 @@ export function GenSpace() {
inputAudio={inputAudio}
onInputAudioChange={setInputAudio}
settings={settings}
onSettingsChange={(nextSettings) => setSettings(applyForcedVideoSettings(nextSettings))}
onSettingsChange={(nextSettings) => handleSettingsChange(applyForcedVideoSettings(nextSettings))}
shouldVideoGenerateWithLtxApi={shouldVideoGenerateWithLtxApi}
icLoraCondType={icLoraCondType}
onIcLoraCondTypeChange={setIcLoraCondType}
Expand Down
70 changes: 56 additions & 14 deletions frontend/views/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { Sparkles, Trash2, Square, ImageIcon, ArrowLeft, Scissors } from 'lucide-react'
import { logger } from '../lib/logger'
import { ImageUploader } from '../components/ImageUploader'
Expand All @@ -22,28 +22,60 @@ import { sanitizeForcedApiVideoSettings } from '../lib/api-video-options'
import { RetakePanel } from '../components/RetakePanel'
import { ICLoraPanel, CONDITIONING_TYPES, type ICLoraConditioningType } from '../components/ICLoraPanel'

const DEFAULT_SETTINGS: GenerationSettings = {
model: 'fast',
duration: 5,
videoResolution: '540p',
fps: 24,
audio: true,
cameraMotion: 'none',
aspectRatio: '16:9',
// Image settings
// Fallback defaults for fields not persisted in appSettings
const IMAGE_SETTINGS_DEFAULTS = {
imageResolution: '1080p',
imageAspectRatio: '16:9',
imageSteps: 4,
}

export function Playground() {
const { goHome } = useProjects()
const { forceApiGenerations, shouldVideoGenerateWithLtxApi } = useAppSettings()
const { settings: appSettings, updateSettings: updateAppSettings, isLoaded: appSettingsLoaded, forceApiGenerations, shouldVideoGenerateWithLtxApi } = useAppSettings()
const [mode, setMode] = useState<GenerationMode>('text-to-video')
const [prompt, setPrompt] = useState('')
const [selectedImage, setSelectedImage] = useState<string | null>(null)
const [selectedAudio, setSelectedAudio] = useState<string | null>(null)
const [settings, setSettings] = useState<GenerationSettings>(() => ({ ...DEFAULT_SETTINGS }))

// Initialize settings from persisted appSettings
const [settings, setSettings] = useState<GenerationSettings>(() => ({
model: appSettings.defaultModel as 'fast' | 'pro',
duration: appSettings.defaultDuration,
videoResolution: appSettings.defaultVideoResolution as '540p' | '720p' | '1080p',
fps: appSettings.defaultFps,
audio: true,
cameraMotion: appSettings.defaultCameraMotion,
aspectRatio: appSettings.defaultAspectRatio as '16:9' | '9:16' | '1:1',
...IMAGE_SETTINGS_DEFAULTS,
}))

// Sync settings from appSettings when loaded
useEffect(() => {
if (!appSettingsLoaded) return
setSettings(prev => ({
...prev,
model: appSettings.defaultModel as 'fast' | 'pro',
duration: appSettings.defaultDuration,
videoResolution: appSettings.defaultVideoResolution as '540p' | '720p' | '1080p',
fps: appSettings.defaultFps,
cameraMotion: appSettings.defaultCameraMotion,
aspectRatio: appSettings.defaultAspectRatio as '16:9' | '9:16' | '1:1',
}))
}, [appSettingsLoaded]) // eslint-disable-line react-hooks/exhaustive-deps

// Persist generation defaults when user changes them
const handleSettingsChange = useCallback((newSettings: GenerationSettings) => {
setSettings(newSettings)
// Sync persisted fields to appSettings
updateAppSettings({
defaultModel: newSettings.model,
defaultDuration: newSettings.duration,
defaultVideoResolution: newSettings.videoResolution,
defaultFps: newSettings.fps,
defaultAspectRatio: newSettings.aspectRatio,
defaultCameraMotion: newSettings.cameraMotion,
})
}, [updateAppSettings])

const { status, processStatus } = useBackend()

Expand Down Expand Up @@ -187,7 +219,17 @@ export function Playground() {
setPrompt('')
setSelectedImage(null)
setSelectedAudio(null)
const baseDefaults = { ...DEFAULT_SETTINGS }
// Reset to user's persisted defaults (not hardcoded defaults)
const baseDefaults: GenerationSettings = {
model: appSettings.defaultModel as 'fast' | 'pro',
duration: appSettings.defaultDuration,
videoResolution: appSettings.defaultVideoResolution as '540p' | '720p' | '1080p',
fps: appSettings.defaultFps,
audio: true,
cameraMotion: appSettings.defaultCameraMotion,
aspectRatio: appSettings.defaultAspectRatio as '16:9' | '9:16' | '1:1',
...IMAGE_SETTINGS_DEFAULTS,
}
const shouldSanitizeVideoSettings = shouldVideoGenerateWithLtxApi && mode !== 'text-to-image'
setSettings(shouldSanitizeVideoSettings ? sanitizeForcedApiVideoSettings(baseDefaults) : baseDefaults)
if (mode !== 'text-to-image') setMode('text-to-video')
Expand Down Expand Up @@ -358,7 +400,7 @@ export function Playground() {
{!isRetakeMode && !isIcLoraMode && (
<SettingsPanel
settings={settings}
onSettingsChange={setSettings}
onSettingsChange={handleSettingsChange}
disabled={isBusy}
mode={mode}
forceApiGenerations={shouldVideoGenerateWithLtxApi}
Expand Down