Automated PR - 2026-03-30

This commit is contained in:
github-actions[bot]
2026-03-30 17:59:34 +00:00
parent ae855f8538
commit f4d0c1ec0e
48 changed files with 8429 additions and 6644 deletions
@@ -12,7 +12,7 @@ from PIL import Image
from torch._prims_common import DeviceLikeType
from tqdm import tqdm
from ltx_core.types import Audio
from ltx_core.types import Audio, VideoPixelShape
from ltx_pipelines.utils.constants import DEFAULT_IMAGE_CRF
logger = logging.getLogger(__name__)
@@ -79,7 +79,7 @@ def normalize_latent(latent: torch.Tensor, device: torch.device, dtype: torch.dt
return (latent / 127.5 - 1.0).to(device=device, dtype=dtype)
def load_image_conditioning(
def load_image_and_preprocess(
image_path: str,
height: int,
width: int,
@@ -99,14 +99,23 @@ def load_image_conditioning(
return image
def load_video_conditioning(
video_path: str, height: int, width: int, frame_cap: int, dtype: torch.dtype, device: torch.device
def video_preprocess(
frames: Generator[torch.Tensor],
height: int,
width: int,
dtype: torch.dtype,
device: torch.device,
) -> torch.Tensor:
"""Preprocesses a video frame generator for conditioning.
Args:
frames: Generator of video frames as tensors of shape (1, H, W, C), dtype uint8.
height: Target height in pixels.
width: Target width in pixels.
dtype: Target dtype for the output tensor.
device: Target device for the output tensor.
Returns:
Tensor of shape (1, C, F, height, width) with values in [-1, 1].
"""
Loads a video from a path and preprocesses it for conditioning.
Note: The video is resized to the nearest multiple of 2 for compatibility with video codecs.
"""
frames = decode_video_from_file(path=video_path, frame_cap=frame_cap, device=device)
result = None
for f in frames:
frame = resize_and_center_crop(f.to(torch.float32), height, width)
@@ -257,9 +266,23 @@ def _audio_frame_to_float(frame: av.AudioFrame) -> np.ndarray:
return arr
def get_videostream_metadata(path: str) -> tuple[float, int, int, int]:
"""Read video stream metadata: (fps, num_frames, width, height).
def get_videostream_fps(path: str) -> float:
"""Read video stream FPS."""
container = av.open(path)
try:
video_stream = next(s for s in container.streams if s.type == "video")
return float(video_stream.average_rate)
finally:
container.close()
def get_videostream_metadata(path: str) -> VideoPixelShape:
"""Read video stream metadata as a VideoPixelShape with batch=1.
If frame count is missing in the container, decodes the stream to count frames.
Args:
path: Path to the video file.
Returns:
VideoPixelShape with batch=1, frames, height, width, and fps populated from the stream.
"""
container = av.open(path)
try:
@@ -270,7 +293,7 @@ def get_videostream_metadata(path: str) -> tuple[float, int, int, int]:
num_frames = sum(1 for _ in container.decode(video_stream))
width = video_stream.codec_context.width
height = video_stream.codec_context.height
return fps, num_frames, width, height
return VideoPixelShape(batch=1, frames=num_frames, height=height, width=width, fps=fps)
finally:
container.close()
@@ -338,16 +361,85 @@ def decode_audio_from_file(
return Audio(waveform=waveform, sampling_rate=sample_rate)
def decode_video_from_file(path: str, frame_cap: int, device: DeviceLikeType) -> Generator[torch.Tensor]:
def decode_video_by_frame(
path: str,
device: DeviceLikeType,
starting_frame: int = 0,
frame_cap: int | None = None,
) -> Generator[torch.Tensor]:
"""Decodes video from a file by sequential frame index, without relying on pts.
Args:
path: Path to the video file.
device: Device to place the resulting tensors on.
starting_frame: Number of leading frames to skip (default 0).
frame_cap: Maximum number of frames to yield. If None, no frame limit (default None).
Yields:
Frames as tensors of shape (1, H, W, C), dtype uint8.
"""
container = av.open(path)
try:
video_stream = next(s for s in container.streams if s.type == "video")
for frame in container.decode(video_stream):
for index, frame in enumerate(container.decode(video_stream)):
if index < starting_frame:
continue
tensor = torch.tensor(frame.to_rgb().to_ndarray(), dtype=torch.uint8, device=device).unsqueeze(0)
yield tensor
frame_cap = frame_cap - 1
if frame_cap == 0:
if frame_cap is not None:
frame_cap -= 1
if frame_cap == 0:
break
finally:
container.close()
def decode_video_from_file(
path: str,
device: DeviceLikeType,
start_time: float = 0.0,
max_duration: float | None = None,
) -> Generator[torch.Tensor]:
"""Decodes video from a file using presentation timestamps for time-based trimming.
If a frame with no pts is encountered, falls back to :func:`decode_video_by_frame`
using FPS-derived frame indices.
Args:
path: Path to the video file.
device: Device to place the resulting tensors on.
start_time: Start time in seconds (default 0.0).
max_duration: Maximum duration in seconds to decode. If None, reads to end of
stream (default None).
Yields:
Frames as tensors of shape (1, H, W, C), dtype uint8.
"""
container = av.open(path)
try:
video_stream = next(s for s in container.streams if s.type == "video")
time_base = float(video_stream.time_base)
if start_time > 0:
container.seek(int(start_time / time_base), stream=video_stream)
end_time = start_time + max_duration if max_duration is not None else None
for frame in container.decode(video_stream):
# PyAV may leave pts unset when the demuxer does not expose per-frame
# timestamps (e.g. some raw/elementary streams, stripped or missing
# metadata, or certain remux paths). Without pts we cannot map frames to
# wall-clock time, so we fall back to sequential frame indices using the
# stream's average frame rate.
if frame.pts is None:
fps = float(video_stream.average_rate)
starting_frame = round(start_time * fps)
frame_cap = round(max_duration * fps) if max_duration is not None else None
yield from decode_video_by_frame(
path=path, device=device, starting_frame=starting_frame, frame_cap=frame_cap
)
return
frame_time = frame.pts * time_base
if frame_time < start_time:
continue
if end_time is not None and frame_time >= end_time:
break
yield torch.tensor(frame.to_rgb().to_ndarray(), dtype=torch.uint8, device=device).unsqueeze(0)
finally:
container.close()