import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { Upload, Settings2, X, Play, Download, Copy, RefreshCw, Trash2, Film, Image, ChevronDown, ChevronRight, Maximize2, Clock, Sliders, Loader2, Info, ArrowUp } from 'lucide-react'; import { resolveFileUrl } from './resolveFileUrl'; // Motion strength presets const MOTION_PRESETS = [ { label: 'Subtle', value: 'low', description: 'Gentle, minimal movement' }, { label: 'Medium', value: 'medium', description: 'Balanced motion' }, { label: 'Dynamic', value: 'high', description: 'Strong, dramatic motion' }, ]; // Duration presets const DURATION_PRESETS = [ { label: '2s', value: 2 }, { label: '4s', value: 4 }, { label: '6s', value: 6 }, { label: '8s', value: 8 }, ]; // FPS presets const FPS_PRESETS = [ { label: '8 fps', value: 8 }, { label: '12 fps', value: 12 }, { label: '16 fps', value: 16 }, { label: '24 fps', value: 24 }, ]; // Quality presets const QUALITY_PRESETS = [ { id: 'low', label: 'Low', short: 'Fast', description: 'For 6-8GB VRAM. Fastest generation.' }, { id: 'medium', label: 'Medium', short: 'Balanced', description: 'For 10-12GB VRAM. Good quality/speed balance.' }, { id: 'high', label: 'High', short: 'Quality', description: 'For 16GB+ VRAM. Higher quality output.' }, { id: 'ultra', label: 'Ultra', short: 'Maximum', description: 'For 24GB+ VRAM. Best quality, longest clips.' }, ]; // Default aspect ratios (used as fallback) const DEFAULT_ASPECT_RATIOS = [ { id: '16:9', label: 'Widescreen', previewW: 42, previewH: 24 }, { id: '9:16', label: 'Vertical', previewW: 24, previewH: 42 }, { id: '1:1', label: 'Square', previewW: 24, previewH: 24 }, { id: '4:3', label: 'Classic', previewW: 32, previewH: 24 }, { id: '3:4', label: 'Portrait', previewW: 24, previewH: 32 }, ]; // Fallback default values (used when API is unavailable) // Matches LTX "high" preset - proven working configuration const FALLBACK_ADVANCED_PARAMS = { steps: 32, cfg: 4.0, denoise: 0.8, }; function isDim(x) { return x && typeof x.width === 'number' && typeof x.height === 'number'; } /** Returns the preset-derived (Auto) resolution for the current aspectRatio + qualityPreset */ function getAutoVideoDims(opts) { const { rawAspectRatioData, aspectRatio, qualityPreset } = opts; const ratioEntry = rawAspectRatioData?.find((ar) => ar.id === aspectRatio); // Prefer all_dimensions (keyed by tier), fall back to legacy dimensions const allDims = ratioEntry?.all_dimensions; if (allDims) { const d = allDims[qualityPreset]; if (isDim(d)) return d; const fallback = allDims['medium']; return isDim(fallback) ? fallback : null; } // Legacy path: dimensions was a flat {width, height} for the selected preset const flat = ratioEntry?.dimensions; if (isDim(flat)) return flat; return null; } /** Returns the override tier dims from availableResolutions */ function getOverrideVideoDims(opts) { const { availableResolutions, customResolution } = opts; if (customResolution === 'auto') return null; const hit = availableResolutions?.find((r) => r.id === customResolution); return hit ? { width: hit.width, height: hit.height } : null; } // ----------------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------------- async function postJson(baseUrl, path, body, apiKey) { const url = `${baseUrl.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(apiKey ? { 'x-api-key': apiKey } : {}), }, credentials: 'include', body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`HTTP ${res.status} ${res.statusText}${text ? `: ${text}` : ''}`); } return (await res.json()); } function uid() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; } function formatDuration(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; if (mins > 0) return `${mins}m ${secs}s`; return `${secs}s`; } // Check if URL points to an animated image (WEBP/GIF) vs video (MP4/WebM) // Animated WEBP/GIF must use tag, videos use