From 1ed1c39269b3c5d73a0cf6ea1a37b04839d17e88 Mon Sep 17 00:00:00 2001 From: henryruhs Date: Wed, 22 Jul 2026 09:49:33 +0200 Subject: [PATCH] Replace chunk cache with a sliding frame buffer in the video reader select_video_frames routed the target window through read_static_video_chunk (lru_cache of chunk-aligned 5-frame blocks), decoding ~2 chunks / 10 frames to serve a 5-frame window. With the persistent sequential ffmpeg reader this indirection is unnecessary. Keep a small sliding frame buffer on the reader instance and read the window straight from it: sequential processing decodes ~1 new frame per output frame (matching 3.6.1), no chunk-alignment waste. A margin absorbs the thread out-of-order access without pipe restarts; a backward or large-forward miss restarts the pipe at the target frame. The old chunk functions stay in place (now unused by the window path) so processor cache_clear calls keep working. AV1 ~20 -> ~21 fps, H.264 ~18 -> ~19 fps, and lower RAM (~12 -> ~9.7 GB), output pixel-identical to 3.6.1. Co-Authored-By: Claude Opus 4.8 --- facefusion/types.py | 4 +++- facefusion/video_manager.py | 48 +++++++++++++++++++++++++++++++++---- facefusion/vision.py | 15 +++++++----- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/facefusion/types.py b/facefusion/types.py index c119ede1..b4e02e2b 100755 --- a/facefusion/types.py +++ b/facefusion/types.py @@ -96,6 +96,7 @@ Duration : TypeAlias = float Buffer : TypeAlias = bytes BitRate : TypeAlias = int +VideoReaderBuffer : TypeAlias = Dict[int, VisionFrame] VideoReader = TypedDict('VideoReader', { 'process' : subprocess.Popen, @@ -104,7 +105,8 @@ VideoReader = TypedDict('VideoReader', 'height' : int, 'fps' : Fps, 'frame_total' : int, - 'position' : int + 'position' : int, + 'frame_buffer' : VideoReaderBuffer }) VideoReaderSet : TypeAlias = Dict[str, VideoReader] VideoPoolSet = TypedDict('VideoPoolSet', diff --git a/facefusion/video_manager.py b/facefusion/video_manager.py index c42ffe5b..59534b29 100644 --- a/facefusion/video_manager.py +++ b/facefusion/video_manager.py @@ -6,7 +6,7 @@ import numpy from facefusion import ffmpeg_builder from facefusion.ffprobe import extract_video_metadata -from facefusion.types import Fps, VideoPoolSet, VideoReader, VisionFrame +from facefusion.types import Fps, VideoPoolSet, VideoReader, VideoReaderBuffer, VisionFrame VIDEO_POOL_SET : VideoPoolSet =\ { @@ -14,6 +14,7 @@ VIDEO_POOL_SET : VideoPoolSet =\ 'writer': {}, 'reader': {} } +VIDEO_READER_BUFFER_MARGIN = 16 def get_video_capture(video_path : str) -> cv2.VideoCapture: @@ -58,7 +59,8 @@ def get_video_reader(video_path : str) -> VideoReader: 'height': height, 'fps': video_metadata.get('fps'), 'frame_total': video_metadata.get('frame_total'), - 'position': 0 + 'position': 0, + 'frame_buffer': {} } return VIDEO_POOL_SET.get('reader').get(video_path) @@ -66,9 +68,14 @@ def get_video_reader(video_path : str) -> VideoReader: def conditional_set_video_reader_position(video_reader : VideoReader, frame_position : int) -> None: if not video_reader.get('position') == frame_position: - video_reader.get('process').terminate() - video_reader['process'] = create_video_reader_process(video_reader.get('video_path'), frame_position, video_reader.get('fps')) - video_reader['position'] = frame_position + restart_video_reader(video_reader, frame_position) + + +def restart_video_reader(video_reader : VideoReader, frame_position : int) -> None: + video_reader.get('process').terminate() + video_reader['process'] = create_video_reader_process(video_reader.get('video_path'), frame_position, video_reader.get('fps')) + video_reader['position'] = frame_position + video_reader['frame_buffer'].clear() def read_video_reader_frame(video_reader : VideoReader) -> Tuple[bool, Optional[VisionFrame]]: @@ -81,6 +88,37 @@ def read_video_reader_frame(video_reader : VideoReader) -> Tuple[bool, Optional[ return False, None +def evict_video_reader_buffer(video_reader : VideoReader, frame_start : int) -> None: + frame_buffer = video_reader.get('frame_buffer') + + for frame_number in [ key for key in frame_buffer if key < frame_start - VIDEO_READER_BUFFER_MARGIN ]: + del frame_buffer[frame_number] + + +def read_video_reader_window(video_reader : VideoReader, frame_start : int, frame_end : int) -> VideoReaderBuffer: + frame_buffer = video_reader.get('frame_buffer') + read_start = max(frame_start, 0) + read_end = frame_end + + if video_reader.get('frame_total') > 0: + read_end = min(read_end, video_reader.get('frame_total') - 1) + + if read_start not in frame_buffer and (read_start < video_reader.get('position') or read_start > video_reader.get('position') + VIDEO_READER_BUFFER_MARGIN): + restart_video_reader(video_reader, read_start) + + reading = video_reader.get('position') <= read_end + + while reading: + has_vision_frame, vision_frame = read_video_reader_frame(video_reader) + + if has_vision_frame: + frame_buffer[video_reader.get('position') - 1] = vision_frame + reading = has_vision_frame and video_reader.get('position') <= read_end + + evict_video_reader_buffer(video_reader, read_start) + return frame_buffer + + def get_video_writer(video_path : str) -> cv2.VideoWriter: if video_path not in VIDEO_POOL_SET.get('writer'): video_writer = cv2.VideoWriter() diff --git a/facefusion/vision.py b/facefusion/vision.py index 157afbeb..dfe32648 100644 --- a/facefusion/vision.py +++ b/facefusion/vision.py @@ -10,7 +10,7 @@ from facefusion.common_helper import is_windows from facefusion.filesystem import get_file_extension, is_image, is_video from facefusion.thread_helper import thread_lock, thread_semaphore from facefusion.types import ColorMode, Duration, Fps, Mask, Orientation, Resolution, Scale, VisionFrame -from facefusion.video_manager import conditional_set_video_frame_position, conditional_set_video_reader_position, get_video_capture, get_video_reader, read_video_reader_frame +from facefusion.video_manager import conditional_set_video_frame_position, conditional_set_video_reader_position, get_video_capture, get_video_reader, read_video_reader_frame, read_video_reader_window def read_static_images(image_paths : List[str], color_mode : ColorMode = 'rgb') -> List[VisionFrame]: @@ -125,16 +125,19 @@ def read_video_chunk(video_path : str, chunk_number : int, chunk_size : int) -> def select_video_frames(video_path : str, frame_number : int = 0, frame_offset : int = 2) -> List[VisionFrame]: vision_frames = [] - chunk_size = frame_offset * 2 + 1 + frame_start = frame_number - frame_offset + frame_end = frame_number + frame_offset if is_video(video_path): with thread_lock(): - for current_number in range(frame_number - frame_offset, frame_number + frame_offset + 1): - video_frame_chunk = read_static_video_chunk(video_path, current_number // chunk_size, chunk_size) + video_reader = get_video_reader(video_path) + frame_buffer = read_video_reader_window(video_reader, frame_start, frame_end) + + for current_number in range(frame_start, frame_end + 1): vision_frame = create_empty_vision_frame() - if current_number in video_frame_chunk: - vision_frame = video_frame_chunk.get(current_number) + if current_number in frame_buffer: + vision_frame = frame_buffer.get(current_number) vision_frames.append(vision_frame)