1ripon1's picture
Upload folder using huggingface_hub
7344bef verified
Raw
History Blame Contribute Delete
10.7 kB
from __future__ import annotations
import os
import subprocess
import tempfile
import gradio as gr
import torch
from PIL import Image
from shared.utils.hdr import iter_hdr_gbrpf32_frames, tonemap_hdr_tensor_to_uint8
from shared.utils.utils import get_video_info_details
from shared.utils.video_decode import decode_video_frames_ffmpeg, resolve_media_binary
from shared.utils.virtual_media import build_virtual_media_path, clear_virtual_media_source, get_virtual_video, store_virtual_video
PROCESS_FULL_VIDEO_VSOURCE = "process_full_video"
PROCESS_FULL_VIDEO_VFILE = "last_frames.mp4"
def frame_to_image(frame_tensor: torch.Tensor) -> Image.Image:
return Image.fromarray(frame_tensor.permute(1, 2, 0).cpu().numpy())
def clear_process_full_video_source() -> None:
clear_virtual_media_source(PROCESS_FULL_VIDEO_VSOURCE)
def build_process_full_video_source_path(*, hdr: bool = False) -> str:
extras = {"vsource": PROCESS_FULL_VIDEO_VSOURCE}
if hdr:
extras["hdr"] = "1"
return build_virtual_media_path(PROCESS_FULL_VIDEO_VFILE, extras=extras)
def set_process_full_video_overlap_buffer(overlap_tensor: torch.Tensor | None, fps_float: float, *, hdr: bool = False) -> None:
if overlap_tensor is None or not torch.is_tensor(overlap_tensor) or int(overlap_tensor.shape[1]) <= 0:
clear_virtual_media_source(PROCESS_FULL_VIDEO_VSOURCE)
return
store_virtual_video(PROCESS_FULL_VIDEO_VSOURCE, PROCESS_FULL_VIDEO_VFILE, overlap_tensor.contiguous(), fps_float, hdr=hdr)
def load_process_full_video_overlap_buffer(video_path: str, overlap_frames: int, actual_frame_count: int) -> torch.Tensor | None:
if overlap_frames <= 0 or actual_frame_count <= 0 or not os.path.isfile(video_path):
return None
if overlap_frames > actual_frame_count:
return None
frames = decode_video_frames_ffmpeg(build_virtual_media_path(video_path, start_frame=-overlap_frames, end_frame=-1, extras={"frame_count": actual_frame_count}), 0, overlap_frames, target_fps=None, bridge="torch")
return None if not torch.is_tensor(frames) or int(frames.shape[0]) <= 0 else frames.permute(3, 0, 1, 2).float().div_(127.5).sub_(1.0).contiguous()
def update_process_full_video_overlap_buffer(committed_tensor: torch.Tensor, overlap_frames: int, fps_float: float, *, hdr: bool = False) -> torch.Tensor | None:
if overlap_frames <= 0 or not torch.is_tensor(committed_tensor) or int(committed_tensor.shape[1]) <= 0:
clear_virtual_media_source(PROCESS_FULL_VIDEO_VSOURCE)
return None
overlap_tensor = committed_tensor.detach().cpu().to(torch.float32).contiguous()
if not hdr:
overlap_tensor = overlap_tensor.div_(127.5).sub_(1.0)
previous_overlap = get_virtual_video(PROCESS_FULL_VIDEO_VSOURCE, PROCESS_FULL_VIDEO_VFILE)
if previous_overlap is not None and int(previous_overlap.shape[1]) > 0:
overlap_tensor = torch.cat([previous_overlap, overlap_tensor], dim=1)
available_frames = int(overlap_tensor.shape[1])
kept_frames = overlap_frames if overlap_frames < available_frames else available_frames
overlap_tensor = overlap_tensor[:, -kept_frames:].contiguous()
set_process_full_video_overlap_buffer(overlap_tensor, fps_float, hdr=hdr)
return overlap_tensor
def load_process_full_video_hdr_overlap_buffer(output_path: str, overlap_frames: int, actual_frame_count: int) -> torch.Tensor | None:
if overlap_frames <= 0 or actual_frame_count <= 0:
return None
if overlap_frames > actual_frame_count:
return None
frames = decode_video_frames_ffmpeg(build_virtual_media_path(output_path, start_frame=-overlap_frames, end_frame=-1, extras={"frame_count": actual_frame_count}), 0, overlap_frames, target_fps=None, bridge="torch", hdr_linear=True)
return None if not torch.is_tensor(frames) or int(frames.shape[0]) <= 0 else frames.permute(3, 0, 1, 2).contiguous()
def _extract_exact_frame_image(video_path: str, frame_no: int) -> Image.Image:
ffmpeg_path = resolve_media_binary("ffmpeg")
if ffmpeg_path is None or not os.path.isfile(video_path) or int(frame_no) < 0:
raise gr.Error(f"Unable to decode frame {frame_no} from {video_path}")
with tempfile.TemporaryDirectory(prefix="wangp_tail_frame_") as temp_dir:
output_path = os.path.join(temp_dir, "frame.png")
command = [ffmpeg_path, "-v", "error", "-y", "-i", video_path, "-an", "-sn", "-vf", f"select=eq(n\\,{int(frame_no)})", "-frames:v", "1", output_path]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0 or not os.path.isfile(output_path):
raise gr.Error(f"Unable to decode frame {frame_no} from {video_path}")
with Image.open(output_path) as frame_image:
return frame_image.convert("RGB").copy()
def resolve_resume_last_frame(video_path: str, reported_frame_count: int) -> tuple[int, Image.Image | None, str]:
if reported_frame_count <= 0:
return 0, None, "existing output contains no decodable frame"
for backtrack in (0, 1, 2, 4, 8, 16, 32, 64, 128, 256):
frame_no = reported_frame_count - 1 - backtrack
if frame_no < 0:
continue
try:
frame_image = _extract_exact_frame_image(video_path, frame_no)
except gr.Error:
continue
actual_frame_count = frame_no + 1
message = "" if actual_frame_count == reported_frame_count else f"Adjusted continuation point to {actual_frame_count} decodable frame(s) from the existing output."
return actual_frame_count, frame_image, message
return 0, None, f"Unable to decode a valid tail frame from {video_path}"
def probe_existing_output_resolution(output_path: str) -> tuple[str, int, int]:
metadata = get_video_info_details(output_path)
width = int(metadata.get("display_width") or metadata.get("width") or 0)
height = int(metadata.get("display_height") or metadata.get("height") or 0)
if width <= 0 or height <= 0:
raise gr.Error(f"Unable to read the resolution of existing output: {output_path}")
return f"{width}x{height}", width, height
def get_video_tensor_resolution(video_tensor_uint8: torch.Tensor) -> tuple[int, int]:
if not torch.is_tensor(video_tensor_uint8) or video_tensor_uint8.ndim != 4:
raise gr.Error("WanGP API returned an invalid video tensor.")
return int(video_tensor_uint8.shape[3]), int(video_tensor_uint8.shape[2])
def load_video_tensor_from_file(video_path: str) -> torch.Tensor:
metadata = get_video_info_details(video_path)
frame_count = int(metadata.get("frame_count") or 0)
if frame_count <= 0:
raise gr.Error(f"Unable to read the frame count of generated chunk: {video_path}")
frames = decode_video_frames_ffmpeg(video_path, 0, frame_count, target_fps=None, bridge="torch")
if frames.shape[0] <= 0:
raise gr.Error(f"Unable to decode generated chunk: {video_path}")
return frames.permute(3, 0, 1, 2).contiguous()
def write_video_chunk(process, video_tensor_uint8: torch.Tensor, *, start_frame: int, frame_count: int) -> torch.Tensor:
if frame_count <= 0:
raise RuntimeError("No frames available to write.")
end_frame = start_frame + frame_count
batch_frames = 8
for batch_start in range(start_frame, end_frame, batch_frames):
batch_end = batch_start + batch_frames
if batch_end > end_frame:
batch_end = end_frame
batch = video_tensor_uint8[:, batch_start:batch_end].permute(1, 2, 3, 0).contiguous()
try:
process.stdin.write(batch.numpy().tobytes())
process.stdin.flush()
except BrokenPipeError as exc:
stderr = process.stderr.read().decode("utf-8", errors="ignore").strip() if process.stderr is not None and process.poll() is not None else ""
raise RuntimeError(stderr or "ffmpeg stopped receiving video frames while streaming a chunk") from exc
if process.poll() not in (None, 0):
stderr = process.stderr.read().decode("utf-8", errors="ignore").strip() if process.stderr is not None else ""
raise RuntimeError(stderr or "ffmpeg exited while streaming a chunk")
return video_tensor_uint8[:, start_frame + frame_count - 1]
def write_hdr_video_chunk(process, video_tensor_hdr: torch.Tensor, *, start_frame: int, frame_count: int) -> torch.Tensor:
if frame_count <= 0:
raise RuntimeError("No HDR frames available to write.")
chunk = video_tensor_hdr[:, start_frame:start_frame + frame_count].detach().cpu()
try:
for frame_bytes in iter_hdr_gbrpf32_frames(chunk):
process.stdin.write(frame_bytes)
process.stdin.flush()
except BrokenPipeError as exc:
stderr = process.stderr.read().decode("utf-8", errors="ignore").strip() if process.stderr is not None and process.poll() is not None else ""
raise RuntimeError(stderr or "ffmpeg stopped receiving HDR video frames while streaming a chunk") from exc
if process.poll() not in (None, 0):
stderr = process.stderr.read().decode("utf-8", errors="ignore").strip() if process.stderr is not None else ""
raise RuntimeError(stderr or "ffmpeg exited while streaming HDR video")
return tonemap_hdr_tensor_to_uint8(video_tensor_hdr[:, start_frame + frame_count - 1:start_frame + frame_count])[:, 0]
def compute_selected_frame_range(metadata: dict, start_seconds: float | None, end_seconds: float | None) -> tuple[int, int, float, int]:
fps_float = float(metadata.get("fps_float") or metadata.get("fps") or 0.0)
total_frames = int(metadata.get("frame_count") or 0)
if fps_float <= 0 or total_frames <= 0:
raise gr.Error("Unable to read the source video FPS or frame count.")
start_frame = int(round(float(start_seconds or 0.0) * fps_float))
if start_frame < 0:
start_frame = 0
if start_frame >= total_frames:
start_frame = total_frames - 1
if end_seconds in (None, ""):
end_frame_exclusive = total_frames
else:
end_frame_exclusive = int(round(float(end_seconds) * fps_float))
if end_frame_exclusive <= start_frame:
end_frame_exclusive = start_frame + 1
if end_frame_exclusive > total_frames:
end_frame_exclusive = total_frames
if end_frame_exclusive <= start_frame:
raise gr.Error("End must be greater than Start.")
return start_frame, end_frame_exclusive, fps_float, total_frames
def get_processing_fps(fps_float: float) -> float:
processing_fps = int(round(float(fps_float)))
return float(processing_fps if processing_fps > 0 else 1)