mirror of
https://github.com/facefusion/facefusion.git
synced 2026-07-27 12:30:55 +02:00
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:
+3
-1
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user