Test
This commit is contained in:
@@ -83,7 +83,7 @@ SESSION_TIMEOUT_SECONDS = 1800
|
||||
NUM_FRAMES = 121
|
||||
FRAME_HEIGHT = 1088
|
||||
FRAME_WIDTH = 1920
|
||||
NUM_INFERENCE_STEPS = 5
|
||||
NUM_INFERENCE_STEPS = 8
|
||||
JPEG_QUALITY = 100
|
||||
BATCH_SIZE = 3
|
||||
|
||||
@@ -376,4 +376,4 @@ STARTUP_WARMUP_PROMPT = os.getenv(
|
||||
("A cinematic drone shot over coastal cliffs at sunrise, "
|
||||
"golden light, gentle ocean waves, ultra detailed"),
|
||||
).strip()
|
||||
STARTUP_WARMUP_TIMEOUT_SECONDS = max(1, _env_int("FASTVIDEO_STARTUP_WARMUP_TIMEOUT_SECONDS", 2400))
|
||||
STARTUP_WARMUP_TIMEOUT_SECONDS = max(1, _env_int("FASTVIDEO_STARTUP_WARMUP_TIMEOUT_SECONDS", 24000))
|
||||
|
||||
@@ -265,6 +265,7 @@ class VideoGenerationWorker:
|
||||
EngineConfig,
|
||||
GeneratorConfig,
|
||||
OffloadConfig,
|
||||
ParallelismConfig,
|
||||
PipelineSelection,
|
||||
QuantizationConfig,
|
||||
)
|
||||
@@ -316,7 +317,7 @@ class VideoGenerationWorker:
|
||||
preset_overrides={
|
||||
"refine": {
|
||||
"enabled": True,
|
||||
"num_inference_steps": 2,
|
||||
"num_inference_steps": 3,
|
||||
"guidance_scale": 1.0,
|
||||
"add_noise": True,
|
||||
},
|
||||
@@ -489,7 +490,7 @@ class VideoGenerationWorker:
|
||||
num_inference_steps=NUM_INFERENCE_STEPS,
|
||||
guidance_scale=1.0,
|
||||
seed=10,
|
||||
ltx2_image_crf=0.0,
|
||||
ltx2_image_crf=(33.0 if image_path and segment_idx == 1 else 0.0),
|
||||
image_path=image_path if segment_idx == 1 else None,
|
||||
return_continuation_state=False,
|
||||
)
|
||||
|
||||
@@ -13,11 +13,12 @@ GXX="$CONDA_PREFIX/bin/aarch64-conda-linux-gnu-g++"
|
||||
|
||||
export CUDA_HOME="$CONDA_PREFIX"
|
||||
export FASTVIDEO_ENABLE_STARTUP_WARMUP=true
|
||||
export DREAMVERSE_MAX_AUTOTUNE=false
|
||||
export FASTVIDEO_ENABLE_PROMPT_SAFETY=true
|
||||
export DREAMVERSE_MAX_AUTOTUNE=true
|
||||
export LTX2_USE_DISTILLED_SIGMAS=0
|
||||
export DREAMVERSE_SESSION_TIMEOUT_SECONDS=1800
|
||||
export CEREBRAS_API_KEY="${CEREBRAS_API_KEY:-}" # set this in your env or ~/.env
|
||||
export FASTVIDEO_PROMPT_CEREBRAS_MODEL="ifm-k2-think-v2"
|
||||
export FASTVIDEO_PROMPT_CEREBRAS_MODEL="gpt-oss-120b"
|
||||
export TORCHINDUCTOR_CACHE_DIR="$HOME/.cache/torchinductor"
|
||||
export TRITON_CACHE_DIR="$HOME/.triton/cache"
|
||||
export TORCH_CUDA_ARCH_LIST="10.0a"
|
||||
@@ -35,7 +36,9 @@ export LIBRARY_PATH="$CONDA_PREFIX/lib/cuda-compat:$CONDA_PREFIX/lib/stubs"
|
||||
# Only libcudart.so.12 at runtime — prevents cuDNN from seeing .so.13
|
||||
export LD_LIBRARY_PATH="$CUDA_RT_DIR"
|
||||
|
||||
CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0}"
|
||||
CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0,1,2,3}"
|
||||
export FASTVIDEO_GPU_COUNT="${FASTVIDEO_GPU_COUNT:-all}"
|
||||
export DREAMVERSE_SP_SIZE="${DREAMVERSE_SP_SIZE:-4}"
|
||||
PORT="${DREAMVERSE_PORT:-8009}"
|
||||
|
||||
FFMPEG_ENV="$(dirname "$0")/ffmpeg-env.sh"
|
||||
|
||||
@@ -9,7 +9,7 @@ import SessionTimeoutModal from "@/components/SessionTimeoutModal";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import Header from "@/components/Header";
|
||||
import VideoPlayer from "@/components/VideoPlayer";
|
||||
import Workspace from "@/components/Workspace";
|
||||
import Workspace, { SceneHistoryList } from "@/components/Workspace";
|
||||
import { saveProject, saveProjectMetadata, listProjects, loadProjectClips, deleteProject, pruneOldProjects, type StoredProject, type StoredClip } from "@/lib/projectStorage";
|
||||
import { isInfrastructureError } from "@/lib/ws/reducer";
|
||||
import { useStore } from "@/hooks/useStore";
|
||||
@@ -80,7 +80,7 @@ function yieldToEventLoop(): Promise<void> {
|
||||
|
||||
const HERO_WAVE_LIGHT = ["#2A4A98", "#4878E5", "#6FA0F2", "#B0BCC8", "#E8D99E", "#D8C844", "#C2A620"];
|
||||
const HERO_WAVE_DARK = ["#143468", "#1E58B8", "#3892F0", "#80B8E8", "#B8D0EA", "#E2D498", "#DABB50"];
|
||||
const HERO_TEXT = "Direct scenes in seconds";
|
||||
const HERO_TEXT = "Direct scenes in seconds with";
|
||||
|
||||
function HeroTagline() {
|
||||
const ref = useRef<HTMLHeadingElement>(null);
|
||||
@@ -166,6 +166,8 @@ function HeroTagline() {
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
<span data-char className="transition-[color,filter] duration-150">{" "}</span>
|
||||
<img src="/logo.svg" alt="FastVideo" className="inline-block h-[1.1em] w-auto align-middle" />
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -467,9 +469,8 @@ export default function Page() {
|
||||
const hasEdits = useMemo(
|
||||
() => Boolean(sessionStarted) && (
|
||||
(promptEvents as Record<string, any>[]).some((e) => typeof e?.text === "string" && e.text.trim() && String(e?.source || "").trim() === "user_rewrite")
|
||||
|| (Boolean(manualContinuationMode) && steeringScenes.length > 0)
|
||||
),
|
||||
[sessionStarted, promptEvents, manualContinuationMode, steeringScenes],
|
||||
[sessionStarted, promptEvents],
|
||||
);
|
||||
|
||||
// Viewing mode: track which clip is selected (defaults to last clip)
|
||||
@@ -2711,6 +2712,7 @@ export default function Page() {
|
||||
/>
|
||||
<Header timeLeft={headerTimeLeft} formatTime={formatTime} onToggleSidebar={() => setSidebarOpen((prev) => !prev)} />
|
||||
|
||||
<div className={cn("flex flex-1 min-h-0 flex-col", sessionStarted && "pb-16 sm:pb-28")}>
|
||||
<div className="relative flex flex-1 min-h-0 flex-col justify-center px-4 pb-2 sm:px-6 sm:pb-12">
|
||||
{isViewingMode && (
|
||||
<>
|
||||
@@ -2806,7 +2808,6 @@ export default function Page() {
|
||||
<Workspace
|
||||
promptEvents={promptEvents as any[]}
|
||||
manualMode={manualContinuationMode as boolean}
|
||||
sceneHistory={steeringScenes as any[]}
|
||||
currentThumbnail={currentThumbnail}
|
||||
originalLabel={pendingInitialPromptRef.current || (selectedPreset as Record<string, any>)?.label || ""}
|
||||
sessionStarted={sessionStarted as boolean}
|
||||
@@ -2877,6 +2878,12 @@ export default function Page() {
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
{manualContinuationMode && (
|
||||
<div className="px-4 sm:px-6">
|
||||
<SceneHistoryList sceneHistory={steeringScenes as any[]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,8 +174,8 @@ export default function VideoPlayer({
|
||||
};
|
||||
}, [generatingNext, sessionStarted]);
|
||||
|
||||
// Drive a ~4.5s progress bar during generation so the wait has a visible ETA.
|
||||
const GEN_DURATION_MS = 4500;
|
||||
// Drive a 12s progress bar during generation so the wait has a visible ETA.
|
||||
const GEN_DURATION_MS = 12000;
|
||||
const [genProgress, setGenProgress] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!generatingNext) {
|
||||
|
||||
@@ -8,6 +8,85 @@ import { Check, Clapperboard, Lightbulb, Pencil } from "lucide-react";
|
||||
export const WORKSPACE_ORIGINAL_SELECTION_KEY = "original";
|
||||
export const WORKSPACE_CURRENT_SELECTION_KEY = "current";
|
||||
|
||||
export function SceneHistoryList({ sceneHistory = [] }: { sceneHistory?: Record<string, any>[] }) {
|
||||
const bottomSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const topSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const [showTopFade, setShowTopFade] = useState(false);
|
||||
|
||||
const scenes = useMemo(
|
||||
() => (sceneHistory || []).filter((s) => normalizeText(s?.prompt)),
|
||||
[sceneHistory],
|
||||
);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
bottomSentinelRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
|
||||
}, 60);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (scenes.length > 0) scrollToBottom();
|
||||
}, [scenes.length, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const sentinel = bottomSentinelRef.current;
|
||||
if (!sentinel || typeof ResizeObserver === "undefined") return;
|
||||
let container: HTMLElement | null = sentinel.parentElement;
|
||||
while (container) {
|
||||
const oy = getComputedStyle(container).overflowY;
|
||||
if (oy === "auto" || oy === "scroll") break;
|
||||
container = container.parentElement;
|
||||
}
|
||||
if (!container) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const nearBottom = container!.scrollHeight - container!.scrollTop - container!.clientHeight < 96;
|
||||
if (nearBottom) scrollToBottom();
|
||||
});
|
||||
ro.observe(container);
|
||||
return () => ro.disconnect();
|
||||
}, [scenes.length, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = topSentinelRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(([entry]) => setShowTopFade(!entry.isIntersecting), { threshold: 0.1 });
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [scenes.length]);
|
||||
|
||||
if (scenes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative z-10 flex flex-col mx-auto w-full max-w-2xl max-h-32 overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none sticky top-0 z-20 -mb-12 h-12 bg-linear-to-b from-background to-transparent transition-opacity duration-200",
|
||||
showTopFade ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div ref={topSentinelRef} className="h-0 w-0" aria-hidden="true" />
|
||||
<div className="flex flex-col gap-2 pt-12 pb-4">
|
||||
{scenes.map((scene, index) => (
|
||||
<div
|
||||
key={scene.id || index}
|
||||
className="flex items-start gap-3 rounded-xl p-3 transition-colors duration-200 hover:bg-slate-200/50 hover:dark:bg-slate-800/30"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Badge variant="secondary" className="horizontal gap-2 items-center w-fit">
|
||||
<Clapperboard className="size-3 opacity-70" />
|
||||
{`Scene ${index + 1}`}
|
||||
</Badge>
|
||||
<p className="line-clamp-2 text-sm leading-5 text-muted-foreground">{scene.prompt}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div ref={bottomSentinelRef} className="h-0 w-0" aria-hidden="true" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceProps {
|
||||
promptEvents?: Record<string, any>[];
|
||||
currentThumbnail?: string | null;
|
||||
@@ -20,7 +99,6 @@ interface WorkspaceProps {
|
||||
selectedEntryKey?: string;
|
||||
originalClipId?: string;
|
||||
manualMode?: boolean;
|
||||
sceneHistory?: Record<string, any>[];
|
||||
}
|
||||
|
||||
function normalizeText(value: any): string {
|
||||
@@ -220,7 +298,7 @@ function ChromaGradient({ sessionStarted = false }: { sessionStarted?: boolean }
|
||||
);
|
||||
}
|
||||
|
||||
export default function Workspace({ promptEvents = [], currentThumbnail = null, originalLabel = "", sessionStarted = false, onSelectOriginal, onSelectEvent, onSelectCurrent, selectedClipId, selectedEntryKey: selectedEntryKeyProp, originalClipId = "", manualMode = false, sceneHistory = [] }: WorkspaceProps) {
|
||||
export default function Workspace({ promptEvents = [], currentThumbnail = null, originalLabel = "", sessionStarted = false, onSelectOriginal, onSelectEvent, onSelectCurrent, selectedClipId, selectedEntryKey: selectedEntryKeyProp, originalClipId = "", manualMode = false }: WorkspaceProps) {
|
||||
const bottomSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const topSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const [showTopFade, setShowTopFade] = useState(false);
|
||||
@@ -243,13 +321,6 @@ export default function Workspace({ promptEvents = [], currentThumbnail = null,
|
||||
return WORKSPACE_CURRENT_SELECTION_KEY;
|
||||
}, [selectedEntryKeyProp, selectedClipId, originalClipId, conversationEvents]);
|
||||
|
||||
// Steering mode: an elegant per-segment list of each scene's prompt (oldest first,
|
||||
// already resolved upstream to prefer the user's own prompt).
|
||||
const scenes = useMemo(
|
||||
() => (sceneHistory || []).filter((s) => normalizeText(s?.prompt)),
|
||||
[sceneHistory],
|
||||
);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
bottomSentinelRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
|
||||
@@ -260,74 +331,18 @@ export default function Workspace({ promptEvents = [], currentThumbnail = null,
|
||||
if (conversationEvents.length > 0) scrollToBottom();
|
||||
}, [conversationEvents, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (manualMode && scenes.length > 0) scrollToBottom();
|
||||
}, [manualMode, scenes.length, scrollToBottom]);
|
||||
|
||||
// The scene list lives in a flex-1 area that shrinks when the ChatBar grows (e.g. the
|
||||
// "Segment complete" banner appears). Without re-scrolling, the latest scene gets clipped
|
||||
// behind the ChatBar. Re-pin to the bottom whenever the scroll container resizes — unless
|
||||
// the user has scrolled up to read earlier scenes.
|
||||
useEffect(() => {
|
||||
if (!manualMode) return;
|
||||
const sentinel = bottomSentinelRef.current;
|
||||
if (!sentinel || typeof ResizeObserver === "undefined") return;
|
||||
let container: HTMLElement | null = sentinel.parentElement;
|
||||
while (container) {
|
||||
const oy = getComputedStyle(container).overflowY;
|
||||
if (oy === "auto" || oy === "scroll") break;
|
||||
container = container.parentElement;
|
||||
}
|
||||
if (!container) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const nearBottom = container!.scrollHeight - container!.scrollTop - container!.clientHeight < 96;
|
||||
if (nearBottom) scrollToBottom();
|
||||
});
|
||||
ro.observe(container);
|
||||
return () => ro.disconnect();
|
||||
}, [manualMode, scenes.length, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = topSentinelRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(([entry]) => setShowTopFade(!entry.isIntersecting), { threshold: 0.1 });
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [conversationEvents.length, manualMode, scenes.length]);
|
||||
}, [conversationEvents.length]);
|
||||
|
||||
if (manualMode) {
|
||||
return (
|
||||
<div className="mt-auto flex flex-col">
|
||||
<ChromaGradient sessionStarted={sessionStarted} />
|
||||
{scenes.length >= 1 && (
|
||||
<section className="relative z-10 flex flex-col h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none sticky top-0 z-20 -mb-12 h-12 bg-linear-to-b from-background to-transparent transition-opacity duration-200",
|
||||
showTopFade ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div ref={topSentinelRef} className="h-0 w-0" aria-hidden="true" />
|
||||
<div className="flex flex-col gap-2 pt-4 pb-4">
|
||||
{scenes.map((scene, index) => (
|
||||
<div
|
||||
key={scene.id || index}
|
||||
className="flex items-start gap-3 rounded-xl p-3 transition-colors duration-200 hover:bg-slate-200/50 hover:dark:bg-slate-800/30"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Badge variant="secondary" className="horizontal gap-2 items-center w-fit">
|
||||
<Clapperboard className="size-3 opacity-70" />
|
||||
{`Scene ${index + 1}`}
|
||||
</Badge>
|
||||
<p className="line-clamp-2 text-sm leading-5 text-muted-foreground">{scene.prompt}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div ref={bottomSentinelRef} className="h-0 w-0" aria-hidden="true" />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user