This commit is contained in:
kevin314
2026-07-15 23:20:22 +00:00
committed by Will Lin
parent b6eafbea50
commit 63991d2017
6 changed files with 106 additions and 80 deletions
+2 -2
View File
@@ -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,
)
+6 -3
View File
@@ -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"
+12 -5
View File
@@ -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>
);
}