Compare commits

..
Author SHA1 Message Date
Henry RuhsandGitHub 2dd10e0168 Hotfix memory leak (#1213)
* fix potential memory leak

* siwtch to queue

* fix lint

* spacing

* cosmetics
2026-07-31 15:34:34 +02:00
b60ea40d26 3.8.0 (#1212)
* mark as next

* unify the dependency checks in pre_check and add ffprobe (#1181)

* drop keep_temp and the common options component (#1180)

Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* introduce ffprobe and ffprobe_builder (#1182)

* introduce ffprobe and ffprobe_builder

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* introduce ffprobe and ffprobe_builder

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* probe video metadata via ffprobe in vision (#1184)

* probe video metadata via ffprobe in vision

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* probe video metadata via ffprobe in vision

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* adopt the workflow task vocabulary from next major (#1185)

Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* introduce workflow-mode and workflow-strategy like next major (#1187)

Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* restrict hdr color transfer and tag the merge output as bt709 (#1188)

* restrict hdr color transfer and tag the merge output as bt709

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* restrict hdr color transfer and tag the merge output as bt709

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* full video migration

* compose the hdr fixture via the builder chain

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* compose the test fixtures via the builder and run_ffmpeg

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* compose every test fixture via the builder and run_ffmpeg (#1189)

* compose every test fixture via the builder and run_ffmpeg

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* use loops in tests for ffmpeg stuff

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* New Video Manager (#1191)

* tiny adjustment for tests

* address the review on the video manager

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* introduce the stream strategy for the video workflow (#1192)

* introduce the stream strategy for the video workflow

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* address the review on the stream strategy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* annotate the changes for review

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* annotate the new tests for review

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* match the temp pixel format help to the locale style

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* question the set_input_seek naming

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* question the reader and writer keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* drop the review annotations from the encoder mapping tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* drop the review annotations from the thread count tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* drop the review annotations from the ui files

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* capture the open review questions as annotations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

* drop the settled annotations from the types

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbcd6VWCiU4BQP1gywPr2a

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* switch to ffmpeg.style for audio.py

* remove todos that were never needed

* fix for ffmpeg7

* Add frame_store module (#1194)

* add frame_store module

* rename and change tests

* rename and update tests

* route window read through frame_store (#1196)

* route window read through frame_store

* update proper id

* restore todos

* restore todos

* go v4 style for workflow (#1197)

* go v4 style for workflow

* remove some todos

* route chunk read through frame_store (#1198)

* vision integration

* Deleted read_video_chunk + read_static_video_chunk

* margin decouple (#1199)

* fix windows CI fail (#1200)

* Cleanup Part1 (#1201)

* remove some todos, improve video manager, simplify ffmpeg commands and more

* do more

* remove thread count for filters

* Cleanup Part 2 (#1202)

* tons of renaming

* tons of renaming

* multi reader approach

* bring tests to an okay-ish state

* bring drain back

* improve read_video_frame speed

* rename method

* move variables

* seek video reader only when trim frame start is larger 0

* make stream the default

* Cleanup/part 3 (#1203)

* remove todo

* sort out workflow, to match upcoming v4

* remove look ahead

* remove core namespace again

* Revamp execution provider overrides/adjustments (#1206)

* Split provider hooks into override/adjust with cached CoreML base

Replace the single resolve_inference_providers processor hook with two:
override_inference_providers (full replacement) and adjust_inference_providers
(merge options onto the base providers built by create_inference_providers).
This lets CoreML processors inherit ModelCacheDirectory + SpecializationStrategy
from the base while layering ModelFormat/MLComputeUnits on top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTQCZiYjJyUX11bDpbRSiB

* fix caching for execution provider by having override and adjust ways

* fix caching for execution provider by having override and adjust ways

* fix lint

* use proper pytest fixtures

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix update preview bug (#1205)

* fix update preview bug

* fix update preview bug

* remove guard

* add is_vision_frame

* Restrict the preview frame slider and the reader seek to the last frame index (#1207)

* fix index bug

* fix rounding bug

* avoid tobytes copy (#1208)

* beautify tests

* hide ffmpeg warnings

* simplify process_stream_frame

* Use is vision frame everywhere (#1210)

* use is_vision_frame everywhere

* fix hash

* fix lint

* fix hash creation in face store

* that model does not exist

* update workflow ffmpeg

* guard workflow (#1211)

* bump version and dependencies

* Update preview

* switch workflow strategy to disk|memory

* update preview

* update preview

* fix wording

* last minute change workflow position

* adjust wording

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Harisreedhar <46858047+harisreedhar@users.noreply.github.com>
Co-authored-by: harisreedhar <h4harisreedhar.s.s@gmail.com>
2026-07-30 22:25:09 +02:00
22 changed files with 100 additions and 97 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up FFmpeg - name: Set up FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: AnimMouse/setup-ffmpeg@v1
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
+4 -4
View File
@@ -1,7 +1,3 @@
[workflow]
workflow_mode =
workflow_strategy =
[paths] [paths]
temp_path = temp_path =
jobs_path = jobs_path =
@@ -72,6 +68,10 @@ output_video_quality =
output_video_scale = output_video_scale =
output_video_fps = output_video_fps =
[workflow]
workflow_mode =
workflow_strategy =
[processors] [processors]
processors = processors =
age_modifier_model = age_modifier_model =
+2 -2
View File
@@ -9,8 +9,6 @@ from facefusion.vision import detect_video_fps
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None: def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
apply_state_item('command', args.get('command')) apply_state_item('command', args.get('command'))
apply_state_item('workflow_mode', args.get('workflow_mode'))
apply_state_item('workflow_strategy', args.get('workflow_strategy'))
apply_state_item('temp_path', args.get('temp_path')) apply_state_item('temp_path', args.get('temp_path'))
apply_state_item('jobs_path', args.get('jobs_path')) apply_state_item('jobs_path', args.get('jobs_path'))
apply_state_item('source_paths', args.get('source_paths')) apply_state_item('source_paths', args.get('source_paths'))
@@ -63,6 +61,8 @@ def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
output_video_fps = normalize_fps(args.get('output_video_fps')) or detect_video_fps(args.get('target_path')) output_video_fps = normalize_fps(args.get('output_video_fps')) or detect_video_fps(args.get('target_path'))
apply_state_item('output_video_fps', output_video_fps) apply_state_item('output_video_fps', output_video_fps)
apply_state_item('workflow_mode', args.get('workflow_mode'))
apply_state_item('workflow_strategy', args.get('workflow_strategy'))
available_processors = [ get_file_name(file_path) for file_path in resolve_file_paths('facefusion/processors/modules') ] available_processors = [ get_file_name(file_path) for file_path in resolve_file_paths('facefusion/processors/modules') ]
apply_state_item('processors', args.get('processors')) apply_state_item('processors', args.get('processors'))
+2 -2
View File
@@ -9,7 +9,7 @@ from facefusion.download import conditional_download_hashes, conditional_downloa
from facefusion.filesystem import resolve_relative_path from facefusion.filesystem import resolve_relative_path
from facefusion.thread_helper import conditional_thread_semaphore from facefusion.thread_helper import conditional_thread_semaphore
from facefusion.types import Detection, DownloadScope, DownloadSet, Fps, InferencePool, ModelSet, VisionFrame from facefusion.types import Detection, DownloadScope, DownloadSet, Fps, InferencePool, ModelSet, VisionFrame
from facefusion.vision import detect_video_fps, fit_contain_frame, read_image from facefusion.vision import detect_video_fps, fit_contain_frame, is_vision_frame, read_image
STREAM_COUNTER = 0 STREAM_COUNTER = 0
@@ -172,7 +172,7 @@ def analyse_video(video_path : str, trim_frame_start : int, trim_frame_end : int
vision_frame = video_manager.read_video_frame(video_reader) vision_frame = video_manager.read_video_frame(video_reader)
if frame_number % int(video_fps) == 0: if frame_number % int(video_fps) == 0:
if numpy.any(vision_frame): if is_vision_frame(vision_frame):
total += 1 total += 1
if analyse_frame(vision_frame): if analyse_frame(vision_frame):
+12 -9
View File
@@ -100,7 +100,7 @@ def pre_check() -> bool:
def common_pre_check() -> bool: def common_pre_check() -> bool:
content_analyser_content = inspect.getsource(content_analyser).encode() content_analyser_content = inspect.getsource(content_analyser).encode()
return hash_helper.create_hash(content_analyser_content) == '0922c180' return hash_helper.create_hash(content_analyser_content) == '3c6ce25e'
def processors_pre_check() -> bool: def processors_pre_check() -> bool:
@@ -315,16 +315,19 @@ def conditional_process() -> ErrorCode:
if state_manager.get_item('workflow_mode') == 'auto': if state_manager.get_item('workflow_mode') == 'auto':
state_manager.set_item('workflow_mode', detect_workflow_mode()) state_manager.set_item('workflow_mode', detect_workflow_mode())
for processor_module in get_processors_modules(state_manager.get_item('processors')): if state_manager.get_item('workflow_mode') == detect_workflow_mode():
if not processor_module.pre_process('output'): for processor_module in get_processors_modules(state_manager.get_item('processors')):
return 2 if not processor_module.pre_process('output'):
return 2
if state_manager.get_item('workflow_mode') == 'image-to-image': if state_manager.get_item('workflow_mode') == 'image-to-image':
return image_to_image.process(start_time) return image_to_image.process(start_time)
if state_manager.get_item('workflow_mode') == 'image-to-video': if state_manager.get_item('workflow_mode') == 'image-to-video':
return image_to_video.process(start_time) return image_to_video.process(start_time)
return 0 return 0
return 2
def detect_workflow_mode() -> WorkflowMode: def detect_workflow_mode() -> WorkflowMode:
+2 -1
View File
@@ -10,6 +10,7 @@ from facefusion.face_helper import apply_nms, average_points, convert_to_face_la
from facefusion.face_landmarker import detect_face_landmark, estimate_face_landmark_68_5 from facefusion.face_landmarker import detect_face_landmark, estimate_face_landmark_68_5
from facefusion.face_recognizer import calculate_face_embedding from facefusion.face_recognizer import calculate_face_embedding
from facefusion.types import BoundingBox, Face, FaceLandmark5, FaceLandmarkSet, FaceScoreSet, Score, VisionFrame from facefusion.types import BoundingBox, Face, FaceLandmark5, FaceLandmarkSet, FaceScoreSet, Score, VisionFrame
from facefusion.vision import is_vision_frame
def create_faces(vision_frame : VisionFrame, bounding_boxes : List[BoundingBox], face_scores : List[Score], face_landmarks_5 : List[FaceLandmark5]) -> List[Face]: def create_faces(vision_frame : VisionFrame, bounding_boxes : List[BoundingBox], face_scores : List[Score], face_landmarks_5 : List[FaceLandmark5]) -> List[Face]:
@@ -73,7 +74,7 @@ def get_many_faces(vision_frames : List[VisionFrame]) -> List[Face]:
many_faces : List[Face] = [] many_faces : List[Face] = []
for vision_frame in vision_frames: for vision_frame in vision_frames:
if numpy.any(vision_frame): if is_vision_frame(vision_frame):
all_bounding_boxes = [] all_bounding_boxes = []
all_face_scores = [] all_face_scores = []
all_face_landmarks_5 = [] all_face_landmarks_5 = []
+7 -8
View File
@@ -1,17 +1,16 @@
import threading import threading
from typing import List, Optional from typing import List, Optional
import numpy
from facefusion.hash_helper import create_hash from facefusion.hash_helper import create_hash
from facefusion.types import Face, FaceStore, VisionFrame from facefusion.types import Face, FaceStore, VisionFrame
from facefusion.vision import is_vision_frame
FACE_STORE : FaceStore = {} FACE_STORE : FaceStore = {}
def get_faces(vision_frame : VisionFrame) -> Optional[List[Face]]: def get_faces(vision_frame : VisionFrame) -> Optional[List[Face]]:
if numpy.any(vision_frame): if is_vision_frame(vision_frame):
vision_hash = create_hash(vision_frame.data) vision_hash = create_hash(vision_frame.tobytes())
if FACE_STORE.get(vision_hash): if FACE_STORE.get(vision_hash):
return FACE_STORE.get(vision_hash).get('faces') return FACE_STORE.get(vision_hash).get('faces')
@@ -20,8 +19,8 @@ def get_faces(vision_frame : VisionFrame) -> Optional[List[Face]]:
def set_faces(vision_frame : VisionFrame, faces : List[Face]) -> None: def set_faces(vision_frame : VisionFrame, faces : List[Face]) -> None:
if numpy.any(vision_frame): if is_vision_frame(vision_frame):
vision_hash = create_hash(vision_frame.data) vision_hash = create_hash(vision_frame.tobytes())
FACE_STORE.setdefault(vision_hash, FACE_STORE.setdefault(vision_hash,
{ {
'lock': threading.Lock() 'lock': threading.Lock()
@@ -29,8 +28,8 @@ def set_faces(vision_frame : VisionFrame, faces : List[Face]) -> None:
def resolve_lock(vision_frame : VisionFrame) -> threading.Lock: def resolve_lock(vision_frame : VisionFrame) -> threading.Lock:
if numpy.any(vision_frame): if is_vision_frame(vision_frame):
vision_hash = create_hash(vision_frame.data) vision_hash = create_hash(vision_frame.tobytes())
return FACE_STORE.setdefault(vision_hash, return FACE_STORE.setdefault(vision_hash,
{ {
'lock': threading.Lock() 'lock': threading.Lock()
+1 -1
View File
@@ -66,7 +66,7 @@ def run_ffmpeg(commands : List[Command]) -> subprocess.Popen[bytes]:
def open_ffmpeg(commands : List[Command]) -> subprocess.Popen[bytes]: def open_ffmpeg(commands : List[Command]) -> subprocess.Popen[bytes]:
commands = ffmpeg_builder.run(commands) commands = ffmpeg_builder.run(commands)
return subprocess.Popen(commands, stdin = subprocess.PIPE, stdout = subprocess.PIPE, pipesize = 1024 * 1024) return subprocess.Popen(commands, stdin = subprocess.PIPE, stderr = subprocess.DEVNULL, stdout = subprocess.PIPE)
def create_video_reader(video_path : str, frame_number : int, video_metadata : VideoReaderMetadata) -> subprocess.Popen[bytes]: def create_video_reader(video_path : str, frame_number : int, video_metadata : VideoReaderMetadata) -> subprocess.Popen[bytes]:
+2 -2
View File
@@ -28,8 +28,8 @@ if is_windows():
ONNXRUNTIME_SET['directml'] = ('onnxruntime-directml', '1.24.4') ONNXRUNTIME_SET['directml'] = ('onnxruntime-directml', '1.24.4')
ONNXRUNTIME_SET['qnn'] = ('onnxruntime-qnn', '1.24.4') ONNXRUNTIME_SET['qnn'] = ('onnxruntime-qnn', '1.24.4')
if is_linux(): if is_linux():
ONNXRUNTIME_SET['migraphx'] = ('onnxruntime-migraphx', '1.25.0') ONNXRUNTIME_SET['migraphx'] = ('onnxruntime-migraphx', '1.26.0')
ONNXRUNTIME_SET['rocm'] = ('onnxruntime-rocm', '1.22.2.post1') ONNXRUNTIME_SET['rocm'] = ('onnxruntime-rocm', '1.22.2.post3')
def cli() -> None: def cli() -> None:
+3 -4
View File
@@ -98,8 +98,6 @@ LOCALES : Locales =\
{ {
'install_dependency': 'choose the variant of {dependency} to install', 'install_dependency': 'choose the variant of {dependency} to install',
'skip_conda': 'skip the conda environment check', 'skip_conda': 'skip the conda environment check',
'workflow_mode': 'choose the workflow mode',
'workflow_strategy': 'choose the workflow strategy',
'config_path': 'choose the config file to override defaults', 'config_path': 'choose the config file to override defaults',
'temp_path': 'specify the directory for the temporary resources', 'temp_path': 'specify the directory for the temporary resources',
'jobs_path': 'specify the directory to store jobs', 'jobs_path': 'specify the directory to store jobs',
@@ -136,7 +134,7 @@ LOCALES : Locales =\
'voice_extractor_model': 'choose the model responsible for extracting the voices', 'voice_extractor_model': 'choose the model responsible for extracting the voices',
'trim_frame_start': 'specify the starting frame of the target video', 'trim_frame_start': 'specify the starting frame of the target video',
'trim_frame_end': 'specify the ending frame of the target video', 'trim_frame_end': 'specify the ending frame of the target video',
'temp_frame_format': 'specify the temporary resources format', 'temp_frame_format': 'specify the temporary frame format',
'temp_pixel_format': 'specify the temporary pixel format', 'temp_pixel_format': 'specify the temporary pixel format',
'target_frame_amount': 'specify the amount of target frames forwarded to the processor', 'target_frame_amount': 'specify the amount of target frames forwarded to the processor',
'output_image_quality': 'specify the image quality which translates to the image compression', 'output_image_quality': 'specify the image quality which translates to the image compression',
@@ -149,6 +147,8 @@ LOCALES : Locales =\
'output_video_quality': 'specify the video quality which translates to the video compression', 'output_video_quality': 'specify the video quality which translates to the video compression',
'output_video_scale': 'specify the video scale based on the target video', 'output_video_scale': 'specify the video scale based on the target video',
'output_video_fps': 'specify the video fps based on the target video', 'output_video_fps': 'specify the video fps based on the target video',
'workflow_mode': 'detect or enforce the workflow mode',
'workflow_strategy': 'process the temporary frames in memory or on disk',
'processors': 'load a single or multiple processors (choices: {choices}, ...)', 'processors': 'load a single or multiple processors (choices: {choices}, ...)',
'background-remover-model': 'choose the model responsible for removing the background', 'background-remover-model': 'choose the model responsible for removing the background',
'background-remover-color': 'apply red, green blue and alpha values of the background', 'background-remover-color': 'apply red, green blue and alpha values of the background',
@@ -204,7 +204,6 @@ LOCALES : Locales =\
'clear_button': 'CLEAR', 'clear_button': 'CLEAR',
'download_providers_checkbox_group': 'DOWNLOAD PROVIDERS', 'download_providers_checkbox_group': 'DOWNLOAD PROVIDERS',
'execution_providers_checkbox_group': 'EXECUTION PROVIDERS', 'execution_providers_checkbox_group': 'EXECUTION PROVIDERS',
'workflow_mode_dropdown': 'WORKFLOW MODE',
'workflow_strategy_dropdown': 'WORKFLOW STRATEGY', 'workflow_strategy_dropdown': 'WORKFLOW STRATEGY',
'execution_thread_count_slider': 'EXECUTION THREAD COUNT', 'execution_thread_count_slider': 'EXECUTION THREAD COUNT',
'face_detector_angles_checkbox_group': 'FACE DETECTOR ANGLES', 'face_detector_angles_checkbox_group': 'FACE DETECTOR ANGLES',
+1 -1
View File
@@ -4,7 +4,7 @@ METADATA =\
{ {
'name': 'FaceFusion', 'name': 'FaceFusion',
'description': 'Industry leading face manipulation platform', 'description': 'Industry leading face manipulation platform',
'version': 'NEXT', 'version': '3.8.0',
'license': 'OpenRAIL-AS', 'license': 'OpenRAIL-AS',
'author': 'Henry Ruhs', 'author': 'Henry Ruhs',
'url': 'https://facefusion.io' 'url': 'https://facefusion.io'
@@ -9,4 +9,4 @@ FrameEnhancerInputs = TypedDict('FrameEnhancerInputs',
'temp_vision_mask' : Mask 'temp_vision_mask' : Mask
}) })
FrameEnhancerModel = Literal['clear_reality_x4', 'face_dat_x4', 'lsdir_x4', 'nomos8k_sc_x4', 'real_esrgan_x2', 'real_esrgan_x2_fp16', 'real_esrgan_x4', 'real_esrgan_x4_fp16', 'real_esrgan_x8', 'real_esrgan_x8_fp16', 'real_hatgan_x4', 'real_web_photo_x4', 'realistic_rescaler_x4', 'remacri_x4', 'siax_x4', 'span_kendata_x4', 'swin2_sr_x4', 'tghq_face_x8', 'ultra_sharp_x4', 'ultra_sharp_2_x4'] FrameEnhancerModel = Literal['clear_reality_x4', 'face_dat_x4', 'nomos8k_sc_x4', 'real_esrgan_x2', 'real_esrgan_x2_fp16', 'real_esrgan_x4', 'real_esrgan_x4_fp16', 'real_esrgan_x8', 'real_esrgan_x8_fp16', 'real_hatgan_x4', 'real_web_photo_x4', 'realistic_rescaler_x4', 'remacri_x4', 'siax_x4', 'span_kendata_x4', 'swin2_sr_x4', 'tghq_face_x8', 'ultra_sharp_x4', 'ultra_sharp_2_x4']
+10 -10
View File
@@ -21,15 +21,6 @@ def create_help_formatter_large(prog : str) -> HelpFormatter:
return HelpFormatter(prog, max_help_position = 300) return HelpFormatter(prog, max_help_position = 300)
def create_workflow_program() -> ArgumentParser:
program = ArgumentParser(add_help = False)
group_workflow = program.add_argument_group('workflow')
group_workflow.add_argument('--workflow-mode', help = translator.get('help.workflow_mode'), default = config.get_str_value('workflow', 'workflow_mode', 'auto'), choices = facefusion.choices.workflow_modes)
group_workflow.add_argument('--workflow-strategy', help = translator.get('help.workflow_strategy'), default = config.get_str_value('workflow', 'workflow_strategy', 'stream'), choices = facefusion.choices.workflow_strategies)
job_store.register_step_keys([ 'workflow_mode', 'workflow_strategy' ])
return program
def create_config_path_program() -> ArgumentParser: def create_config_path_program() -> ArgumentParser:
program = ArgumentParser(add_help = False) program = ArgumentParser(add_help = False)
group_paths = program.add_argument_group('paths') group_paths = program.add_argument_group('paths')
@@ -209,6 +200,15 @@ def create_output_creation_program() -> ArgumentParser:
return program return program
def create_workflow_program() -> ArgumentParser:
program = ArgumentParser(add_help = False)
group_workflow = program.add_argument_group('workflow')
group_workflow.add_argument('--workflow-mode', help = translator.get('help.workflow_mode'), default = config.get_str_value('workflow', 'workflow_mode', 'auto'), choices = facefusion.choices.workflow_modes)
group_workflow.add_argument('--workflow-strategy', help = translator.get('help.workflow_strategy'), default = config.get_str_value('workflow', 'workflow_strategy', 'memory'), choices = facefusion.choices.workflow_strategies)
job_store.register_step_keys([ 'workflow_mode', 'workflow_strategy' ])
return program
def create_processors_program() -> ArgumentParser: def create_processors_program() -> ArgumentParser:
program = ArgumentParser(add_help = False) program = ArgumentParser(add_help = False)
available_processors = [ get_file_name(file_path) for file_path in resolve_file_paths('facefusion/processors/modules') ] available_processors = [ get_file_name(file_path) for file_path in resolve_file_paths('facefusion/processors/modules') ]
@@ -309,7 +309,7 @@ def create_step_index_program() -> ArgumentParser:
def collect_step_program() -> ArgumentParser: def collect_step_program() -> ArgumentParser:
return ArgumentParser(parents = [ create_workflow_program(), create_face_detector_program(), create_face_landmarker_program(), create_face_selector_program(), create_face_tracker_program(), create_face_masker_program(), create_voice_extractor_program(), create_frame_extraction_program(), create_frame_distribution_program(), create_output_creation_program(), create_processors_program() ], add_help = False) return ArgumentParser(parents = [ create_face_detector_program(), create_face_landmarker_program(), create_face_selector_program(), create_face_tracker_program(), create_face_masker_program(), create_voice_extractor_program(), create_frame_extraction_program(), create_frame_distribution_program(), create_output_creation_program(), create_workflow_program(), create_processors_program() ], add_help = False)
def collect_job_program() -> ArgumentParser: def collect_job_program() -> ArgumentParser:
+2 -3
View File
@@ -5,7 +5,6 @@ from concurrent.futures import ThreadPoolExecutor
from typing import Deque, Iterator, List from typing import Deque, Iterator, List
import cv2 import cv2
import numpy
from tqdm import tqdm from tqdm import tqdm
from facefusion import ffmpeg_builder, logger, state_manager, translator from facefusion import ffmpeg_builder, logger, state_manager, translator
@@ -15,7 +14,7 @@ from facefusion.ffmpeg import open_ffmpeg
from facefusion.filesystem import is_directory from facefusion.filesystem import is_directory
from facefusion.processors.core import get_processors_modules from facefusion.processors.core import get_processors_modules
from facefusion.types import Fps, StreamMode, VisionFrame from facefusion.types import Fps, StreamMode, VisionFrame
from facefusion.vision import extract_vision_mask, read_static_images from facefusion.vision import extract_vision_mask, is_vision_frame, read_static_images
def multi_process_capture(camera_capture : cv2.VideoCapture, camera_fps : Fps) -> Iterator[VisionFrame]: def multi_process_capture(camera_capture : cv2.VideoCapture, camera_fps : Fps) -> Iterator[VisionFrame]:
@@ -31,7 +30,7 @@ def multi_process_capture(camera_capture : cv2.VideoCapture, camera_fps : Fps) -
if analyse_stream(capture_vision_frame, camera_fps): if analyse_stream(capture_vision_frame, camera_fps):
camera_capture.release() camera_capture.release()
if numpy.any(capture_vision_frame): if is_vision_frame(capture_vision_frame):
future = executor.submit(process_stream_frame, source_vision_frames, capture_vision_frame) future = executor.submit(process_stream_frame, source_vision_frames, capture_vision_frame)
futures.append(future) futures.append(future)
+5 -5
View File
@@ -65,7 +65,7 @@ Locales : TypeAlias = Dict[Language, Dict[str, Any]]
LocalePoolSet : TypeAlias = Dict[str, Locales] LocalePoolSet : TypeAlias = Dict[str, Locales]
WorkflowMode = Literal['auto', 'image-to-image', 'image-to-video'] WorkflowMode = Literal['auto', 'image-to-image', 'image-to-video']
WorkflowStrategy = Literal['disk', 'stream'] WorkflowStrategy = Literal['disk', 'memory']
CameraCaptureSet : TypeAlias = Dict[str, cv2.VideoCapture] CameraCaptureSet : TypeAlias = Dict[str, cv2.VideoCapture]
CameraPoolSet = TypedDict('CameraPoolSet', CameraPoolSet = TypedDict('CameraPoolSet',
@@ -322,8 +322,6 @@ JobSet : TypeAlias = Dict[str, Job]
StateKey = Literal\ StateKey = Literal\
[ [
'command', 'command',
'workflow_mode',
'workflow_strategy',
'config_path', 'config_path',
'temp_path', 'temp_path',
'jobs_path', 'jobs_path',
@@ -378,6 +376,8 @@ StateKey = Literal\
'output_video_quality', 'output_video_quality',
'output_video_scale', 'output_video_scale',
'output_video_fps', 'output_video_fps',
'workflow_mode',
'workflow_strategy',
'processors', 'processors',
'open_browser', 'open_browser',
'ui_layouts', 'ui_layouts',
@@ -395,8 +395,6 @@ StateKey = Literal\
State = TypedDict('State', State = TypedDict('State',
{ {
'command' : str, 'command' : str,
'workflow_mode' : WorkflowMode,
'workflow_strategy' : WorkflowStrategy,
'config_path' : str, 'config_path' : str,
'temp_path' : str, 'temp_path' : str,
'jobs_path' : str, 'jobs_path' : str,
@@ -451,6 +449,8 @@ State = TypedDict('State',
'output_video_quality' : int, 'output_video_quality' : int,
'output_video_scale' : Scale, 'output_video_scale' : Scale,
'output_video_fps' : float, 'output_video_fps' : float,
'workflow_mode' : WorkflowMode,
'workflow_strategy' : WorkflowStrategy,
'processors' : List[str], 'processors' : List[str],
'open_browser' : bool, 'open_browser' : bool,
'ui_layouts' : List[str], 'ui_layouts' : List[str],
+2 -2
View File
@@ -18,7 +18,7 @@ from facefusion.types import AudioFrame, Face, Mask, VisionFrame
from facefusion.uis import choices as uis_choices from facefusion.uis import choices as uis_choices
from facefusion.uis.core import get_ui_component, get_ui_components, register_ui_component from facefusion.uis.core import get_ui_component, get_ui_components, register_ui_component
from facefusion.uis.types import ComponentOptions, PreviewMode from facefusion.uis.types import ComponentOptions, PreviewMode
from facefusion.vision import detect_frame_orientation, extract_vision_mask, fit_cover_frame, merge_vision_mask, obscure_frame, read_static_image, read_static_images, read_video_frame, restrict_frame, select_video_frames, unpack_resolution from facefusion.vision import detect_frame_orientation, extract_vision_mask, fit_cover_frame, is_vision_frame, merge_vision_mask, obscure_frame, read_static_image, read_static_images, read_video_frame, restrict_frame, select_video_frames, unpack_resolution
PREVIEW_IMAGE : Optional[gradio.Image] = None PREVIEW_IMAGE : Optional[gradio.Image] = None
@@ -277,7 +277,7 @@ def create_face_by_face(reference_vision_frame : VisionFrame, source_vision_fram
target_crop_vision_frame = extract_crop_frame(target_vision_frame, target_face) target_crop_vision_frame = extract_crop_frame(target_vision_frame, target_face)
output_crop_vision_frame = extract_crop_frame(temp_vision_frame, target_face) output_crop_vision_frame = extract_crop_frame(temp_vision_frame, target_face)
if numpy.any(target_crop_vision_frame) and numpy.any(output_crop_vision_frame): if is_vision_frame(target_crop_vision_frame) and is_vision_frame(output_crop_vision_frame):
target_crop_dimension = min(target_crop_vision_frame.shape[:2]) target_crop_dimension = min(target_crop_vision_frame.shape[:2])
target_crop_vision_frame = fit_cover_frame(target_crop_vision_frame, (target_crop_dimension, target_crop_dimension)) target_crop_vision_frame = fit_cover_frame(target_crop_vision_frame, (target_crop_dimension, target_crop_dimension))
output_crop_vision_frame = fit_cover_frame(output_crop_vision_frame, (target_crop_dimension, target_crop_dimension)) output_crop_vision_frame = fit_cover_frame(output_crop_vision_frame, (target_crop_dimension, target_crop_dimension))
+1 -13
View File
@@ -4,21 +4,14 @@ import gradio
import facefusion.choices import facefusion.choices
from facefusion import state_manager, translator from facefusion import state_manager, translator
from facefusion.types import WorkflowMode, WorkflowStrategy from facefusion.types import WorkflowStrategy
WORKFLOW_MODE_DROPDOWN : Optional[gradio.Dropdown] = None
WORKFLOW_STRATEGY_DROPDOWN : Optional[gradio.Dropdown] = None WORKFLOW_STRATEGY_DROPDOWN : Optional[gradio.Dropdown] = None
def render() -> None: def render() -> None:
global WORKFLOW_MODE_DROPDOWN
global WORKFLOW_STRATEGY_DROPDOWN global WORKFLOW_STRATEGY_DROPDOWN
WORKFLOW_MODE_DROPDOWN = gradio.Dropdown(
label = translator.get('uis.workflow_mode_dropdown'),
choices = facefusion.choices.workflow_modes,
value = state_manager.get_item('workflow_mode')
)
WORKFLOW_STRATEGY_DROPDOWN = gradio.Dropdown( WORKFLOW_STRATEGY_DROPDOWN = gradio.Dropdown(
label = translator.get('uis.workflow_strategy_dropdown'), label = translator.get('uis.workflow_strategy_dropdown'),
choices = facefusion.choices.workflow_strategies, choices = facefusion.choices.workflow_strategies,
@@ -27,13 +20,8 @@ def render() -> None:
def listen() -> None: def listen() -> None:
WORKFLOW_MODE_DROPDOWN.change(update_workflow_mode, inputs = WORKFLOW_MODE_DROPDOWN)
WORKFLOW_STRATEGY_DROPDOWN.change(update_workflow_strategy, inputs = WORKFLOW_STRATEGY_DROPDOWN) WORKFLOW_STRATEGY_DROPDOWN.change(update_workflow_strategy, inputs = WORKFLOW_STRATEGY_DROPDOWN)
def update_workflow_mode(workflow_mode : WorkflowMode) -> None:
state_manager.set_item('workflow_mode', workflow_mode)
def update_workflow_strategy(workflow_strategy : WorkflowStrategy) -> None: def update_workflow_strategy(workflow_strategy : WorkflowStrategy) -> None:
state_manager.set_item('workflow_strategy', workflow_strategy) state_manager.set_item('workflow_strategy', workflow_strategy)
+3 -3
View File
@@ -3,7 +3,7 @@ from functools import partial
from facefusion import process_manager, state_manager from facefusion import process_manager, state_manager
from facefusion.types import ErrorCode from facefusion.types import ErrorCode
from facefusion.workflows.core import clear, setup from facefusion.workflows.core import clear, setup
from facefusion.workflows.to_video import analyse_video, extract_frames, finalize_video, merge_frames, process_disk_frames, process_stream_frames, restore_audio from facefusion.workflows.to_video import analyse_video, extract_frames, finalize_video, merge_frames, process_disk_frames, process_memory_frames, restore_audio
def process(start_time : float) -> ErrorCode: def process(start_time : float) -> ErrorCode:
@@ -22,8 +22,8 @@ def process(start_time : float) -> ErrorCode:
merge_frames merge_frames
]) ])
if state_manager.get_item('workflow_strategy') == 'stream': if state_manager.get_item('workflow_strategy') == 'memory':
tasks.append(process_stream_frames) tasks.append(process_memory_frames)
tasks.extend( tasks.extend(
[ [
+24 -14
View File
@@ -1,5 +1,6 @@
from concurrent.futures import ThreadPoolExecutor, as_completed from collections import deque
from typing import Tuple from concurrent.futures import Future, ThreadPoolExecutor
from typing import Deque
import cv2 import cv2
import numpy import numpy
@@ -58,18 +59,23 @@ def process_disk_frames() -> ErrorCode:
read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number'))
with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor:
futures = [] futures : Deque[Future[bool]] = deque()
for frame_number, temp_frame_path in temp_frame_set.items(): for frame_number, temp_frame_path in temp_frame_set.items():
future = executor.submit(process_disk_frame, temp_frame_path, frame_number) future = executor.submit(process_disk_frame, temp_frame_path, frame_number)
futures.append(future) futures.append(future)
for future in as_completed(futures): while futures:
future = futures.popleft()
if is_process_stopping(): if is_process_stopping():
for pending_future in futures: for pending_future in futures:
pending_future.cancel() pending_future.cancel()
if not future.cancelled(): futures.clear()
else:
future.result() future.result()
progress.update() progress.update()
@@ -84,7 +90,7 @@ def process_disk_frames() -> ErrorCode:
return 0 return 0
def process_stream_frame(frame_number : int, temp_video_resolution : Resolution) -> Tuple[int, VisionFrame]: def process_memory_frame(frame_number : int, temp_video_resolution : Resolution) -> VisionFrame:
target_vision_frames = select_video_frames(state_manager.get_item('target_path'), frame_number, state_manager.get_item('target_frame_amount')) target_vision_frames = select_video_frames(state_manager.get_item('target_path'), frame_number, state_manager.get_item('target_frame_amount'))
target_vision_frame = get_middle(target_vision_frames) target_vision_frame = get_middle(target_vision_frames)
temp_vision_frame = target_vision_frame.copy() temp_vision_frame = target_vision_frame.copy()
@@ -100,10 +106,10 @@ def process_stream_frame(frame_number : int, temp_video_resolution : Resolution)
if state_manager.get_item('temp_pixel_format') == 'bgr24': if state_manager.get_item('temp_pixel_format') == 'bgr24':
temp_vision_frame = temp_vision_frame[:, :, :3] temp_vision_frame = temp_vision_frame[:, :, :3]
return frame_number, numpy.ascontiguousarray(temp_vision_frame) return numpy.ascontiguousarray(temp_vision_frame)
def process_stream_frames() -> ErrorCode: def process_memory_frames() -> ErrorCode:
trim_frame_start, trim_frame_end = restrict_trim_frame(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end')) trim_frame_start, trim_frame_end = restrict_trim_frame(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end'))
output_video_resolution = scale_resolution(detect_video_resolution(state_manager.get_item('target_path')), state_manager.get_item('output_video_scale')) output_video_resolution = scale_resolution(detect_video_resolution(state_manager.get_item('target_path')), state_manager.get_item('output_video_scale'))
temp_video_resolution = restrict_video_resolution(state_manager.get_item('target_path'), output_video_resolution) temp_video_resolution = restrict_video_resolution(state_manager.get_item('target_path'), output_video_resolution)
@@ -119,20 +125,24 @@ def process_stream_frames() -> ErrorCode:
read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number'))
with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor:
futures = [] futures : Deque[Future[VisionFrame]] = deque()
for frame_number in temp_frame_range: for frame_number in temp_frame_range:
future = executor.submit(process_stream_frame, frame_number, temp_video_resolution) future = executor.submit(process_memory_frame, frame_number, temp_video_resolution)
futures.append(future) futures.append(future)
for future in futures: while futures:
future = futures.popleft()
if is_process_stopping(): if is_process_stopping():
for pending_future in futures: for pending_future in futures:
pending_future.cancel() pending_future.cancel()
if not future.cancelled(): futures.clear()
_, temp_vision_frame = future.result()
video_manager.write_video_frame(video_writer, temp_vision_frame) else:
video_manager.write_video_frame(video_writer, future.result())
progress.update() progress.update()
if not video_manager.close_video_writer(video_writer): if not video_manager.close_video_writer(video_writer):
+5 -5
View File
@@ -1,8 +1,8 @@
gradio-rangeslider==0.0.8 gradio-rangeslider==0.0.8
gradio==5.50.0 gradio==5.50.0
numpy==2.2.1 numpy==2.4.6
onnx==1.21.0 onnx==1.22.0
onnxruntime==1.26.0 onnxruntime==1.26.0
opencv-python==4.13.0.92 opencv-python-headless==5.0.0.93
tqdm==4.67.3 tqdm==4.70.0
scipy==1.17.1 scipy==1.18.0
+10 -6
View File
@@ -50,34 +50,35 @@ def test_get_reader() -> None:
assert video_metadata.get('resolution') == (426, 226) assert video_metadata.get('resolution') == (426, 226)
assert video_metadata.get('fps') == 25.0 assert video_metadata.get('fps') == 25.0
assert video_metadata.get('frame_total') == 270 assert video_metadata.get('frame_total') == 270
assert get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame') is video_reader assert get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame') is video_reader
assert not get_reader(get_test_example_file('target-240p-25fps.mp4'), 'select_video_frames').get('id') == video_reader.get('id') assert not get_reader(get_test_example_file('target-240p-25fps.mp4'), 'select_video_frames').get('id') == video_reader.get('id')
def test_conditional_seek_video_reader() -> None: def test_conditional_seek_video_reader() -> None:
video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame') video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame')
sequential_frames = {} video_frames = {}
for frame_number in range(30): for frame_number in range(30):
sequential_frames[frame_number] = read_video_frame(video_reader) video_frames[frame_number] = read_video_frame(video_reader)
for frame_number in [ 5, 17, 29 ]: for frame_number in [ 5, 17, 29 ]:
conditional_seek_video_reader(video_reader, frame_number) conditional_seek_video_reader(video_reader, frame_number)
assert numpy.array_equal(read_video_frame(video_reader), sequential_frames.get(frame_number)) is True assert numpy.array_equal(read_video_frame(video_reader), video_frames.get(frame_number)) is True
def test_seek_video_reader() -> None: def test_seek_video_reader() -> None:
video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame') video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'read_video_frame')
sequential_frames = {} video_frames = {}
for frame_number in range(30): for frame_number in range(30):
sequential_frames[frame_number] = read_video_frame(video_reader) video_frames[frame_number] = read_video_frame(video_reader)
for frame_number in [ 5, 17, 29 ]: for frame_number in [ 5, 17, 29 ]:
seek_video_reader(video_reader, frame_number) seek_video_reader(video_reader, frame_number)
assert numpy.array_equal(read_video_frame(video_reader), sequential_frames.get(frame_number)) is True assert numpy.array_equal(read_video_frame(video_reader), video_frames.get(frame_number)) is True
def test_drain_video_reader() -> None: def test_drain_video_reader() -> None:
@@ -133,6 +134,7 @@ def test_collect_video_frames() -> None:
def test_close_video_reader() -> None: def test_close_video_reader() -> None:
video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'select_video_frames') video_reader = get_reader(get_test_example_file('target-240p-25fps.mp4'), 'select_video_frames')
read_video_frames(video_reader, 0, 4) read_video_frames(video_reader, 0, 4)
close_video_reader(video_reader) close_video_reader(video_reader)
@@ -176,6 +178,7 @@ def test_close_video_writer() -> None:
create_temp_directory(target_path) create_temp_directory(target_path)
video_reader = get_reader(target_path, 'read_video_frame') video_reader = get_reader(target_path, 'read_video_frame')
video_writer = get_writer(target_path, 30.0, (426, 226), (426, 226), 30.0) video_writer = get_writer(target_path, 30.0, (426, 226), (426, 226), 30.0)
write_video_frame(video_writer, read_video_frame(video_reader)) write_video_frame(video_writer, read_video_frame(video_reader))
assert close_video_writer(video_writer) is True assert close_video_writer(video_writer) is True
@@ -186,6 +189,7 @@ def test_clear_video_pool() -> None:
create_temp_directory(target_path) create_temp_directory(target_path)
video_reader = get_reader(target_path, 'select_video_frames') video_reader = get_reader(target_path, 'select_video_frames')
video_writer = get_writer(target_path, 25.0, (426, 226), (426, 226), 25.0) video_writer = get_writer(target_path, 25.0, (426, 226), (426, 226), 25.0)
read_video_frames(video_reader, 0, 4) read_video_frames(video_reader, 0, 4)
write_video_frame(video_writer, read_video_frame(video_reader)) write_video_frame(video_writer, read_video_frame(video_reader))
clear_video_pool() clear_video_pool()