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 <noreply@anthropic.com>
This commit is contained in:
henryruhs
2026-07-22 09:49:33 +02:00
parent d30f2e2f8e
commit 1ed1c39269
3 changed files with 55 additions and 12 deletions
+3 -1
View File
@@ -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',
+43 -5
View File
@@ -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()
+9 -6
View File
@@ -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)