annotation for new code

This commit is contained in:
henryruhs
2026-07-22 21:33:29 +02:00
parent 94139118ed
commit 3b7a73aa06
18 changed files with 66 additions and 11 deletions
+1
View File
@@ -82,6 +82,7 @@ video_formats : List[VideoFormat] = list(get_args(VideoFormat))
temp_frame_formats : List[TempFrameFormat] = list(get_args(TempFrameFormat))
output_audio_encoders : List[AudioEncoder] = list(get_args(AudioEncoder))
#todo: needs review - [config] [critical: low] workflow mode choices for disk and stream
workflow_modes : List[WorkflowMode] = list(get_args(WorkflowMode))
output_video_encoders : List[VideoEncoder] = list(get_args(VideoEncoder))
output_encoder_set : EncoderSet =\
+1
View File
@@ -154,6 +154,7 @@ def analyse_image(image_path : str) -> bool:
return analyse_frame(vision_frame)
#todo: needs review - [sampling] [critical: high] pre sampled frames replace per frame reads, sample_index must stay aligned with the fps modulo
@lru_cache()
def analyse_video(video_path : str, trim_frame_start : int, trim_frame_end : int) -> bool:
video_fps = detect_video_fps(video_path)
+2 -1
View File
@@ -100,10 +100,11 @@ def pre_check() -> bool:
return True
#todo: needs review - [security] [critical: high] hash guard updated for the sampling change in content_analyser, stale hash silently exits the app
def common_pre_check() -> bool:
content_analyser_content = inspect.getsource(content_analyser).encode()
return hash_helper.create_hash(content_analyser_content) == 'dfca29ec'
return hash_helper.create_hash(content_analyser_content) == 'f9a5317c'
def processors_pre_check() -> bool:
+2
View File
@@ -108,6 +108,7 @@ def get_available_encoder_set() -> EncoderSet:
return available_encoder_set
#todo: needs review - [color] [critical: high] hdr sources are tonemapped to bt709 during extraction, gated by the probed color_transfer
def extract_frames(target_path : str, temp_video_resolution : Resolution, temp_video_fps : Fps, trim_frame_start : int, trim_frame_end : int) -> bool:
color_transfer = extract_video_metadata(target_path).get('color_transfer')
extract_frame_total = predict_video_frame_total(target_path, temp_video_fps, trim_frame_start, trim_frame_end)
@@ -242,6 +243,7 @@ def merge_video(target_path : str, temp_video_fps : Fps, output_video_resolution
ffmpeg_builder.set_video_tag(output_video_encoder, temp_video_format),
ffmpeg_builder.set_video_quality(output_video_encoder, output_video_quality),
ffmpeg_builder.set_video_preset(output_video_encoder, output_video_preset),
#todo: needs review - [color] [critical: high] merge output always converted and tagged bt709
ffmpeg_builder.concat(
ffmpeg_builder.set_video_fps(output_video_fps),
ffmpeg_builder.keep_video_alpha(output_video_encoder),
+8
View File
@@ -83,10 +83,12 @@ def unsafe_concat() -> List[Command]:
return [ '-f', 'concat', '-safe', '0' ]
#todo: needs review - [seeking] [critical: low] seek based start for the pipe reader
def set_input_seek(seek_time : float) -> List[Command]:
return [ '-ss', str(seek_time) ]
#todo: needs review - [decoding] [critical: low] raw output format for pipe io
def set_output_format(output_format : str) -> List[Command]:
return [ '-f', output_format ]
@@ -107,10 +109,12 @@ def set_frame_quality(frame_quality : int) -> List[Command]:
return [ '-q:v', str(frame_quality) ]
#todo: needs review - [sampling] [critical: medium] select expression decodes every frame of the range to pick samples
def select_frame_samples(frame_start : int, frame_end : int, frame_stride : int) -> List[Command]:
return [ '-vf', 'select=between(n\\,' + str(frame_start) + '\\,' + str(frame_end - 1) + ')*not(mod(n\\,' + str(frame_stride) + '))' ]
#todo: needs review - [sampling] [critical: low] caps the sampler output frames
def set_frame_total(frame_total : int) -> List[Command]:
return [ '-frames:v', str(frame_total) ]
@@ -129,12 +133,14 @@ def prevent_frame_drop() -> List[Command]:
return [ '-vsync', '0' ]
#todo: needs review - [color] [critical: high] gated hdr to sdr tonemap, relies on ffmpeg 8 swscale intent support
def tonemap_video(color_transfer : ColorTransfer) -> List[Command]:
if color_transfer in [ 'smpte2084', 'arib-std-b67' ]:
return [ '-vf', 'scale=out_primaries=bt709:out_transfer=bt709:intent=perceptual' ]
return []
#todo: needs review - [color] [critical: high] converts and tags encoder output, prevents player colorimetry guessing
def convert_colorspace(color_space : str) -> List[Command]:
return [ '-vf', 'scale=out_color_matrix=' + color_space + ':out_range=tv:out_primaries=' + color_space + ':out_transfer=' + color_space ]
@@ -209,6 +215,7 @@ def set_audio_volume(audio_volume : int) -> List[Command]:
return [ '-filter:a', 'volume=' + str(audio_volume / 100) ]
#todo: needs review - [encoding] [critical: low] explicit ffmpeg thread cap
def set_thread_count(thread_count : int) -> List[Command]:
return [ '-threads', str(thread_count) ]
@@ -221,6 +228,7 @@ def copy_video_encoder() -> List[Command]:
return set_video_encoder('copy')
#todo: needs review - [encoding] [critical: medium] moved from ffmpeg, adds rawvideo to libx264 remap for mkv and mp4
def fix_video_encoder(video_format : VideoFormat, video_encoder : VideoEncoder) -> VideoEncoder:
if video_format in [ 'm4v', 'mpeg', 'mxf', 'wmv' ]:
return 'libx264'
+7
View File
@@ -6,11 +6,13 @@ from facefusion import ffprobe_builder
from facefusion.types import Buffer, Command, Fps, VideoMetadata
#todo: no review needed - [probing] copy of v4, return type uses the Buffer alias
def run_ffprobe(commands : List[Command]) -> subprocess.Popen[Buffer]:
commands = ffprobe_builder.run(commands)
return subprocess.Popen(commands, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
#todo: needs review - [probing] [critical: low] extracted from v4 probe_entries, parsing logic unchanged
def parse_entries(output : Buffer) -> Dict[str, str]:
media_entries = {}
@@ -25,6 +27,7 @@ def parse_entries(output : Buffer) -> Dict[str, str]:
return media_entries
#todo: no review needed - [probing] copy of v4 with parsing moved to parse_entries
def probe_entries(media_path : str, entries : List[str]) -> Dict[str, str]:
commands = ffprobe_builder.chain(
ffprobe_builder.show_entries(entries),
@@ -36,6 +39,7 @@ def probe_entries(media_path : str, entries : List[str]) -> Dict[str, str]:
return parse_entries(output)
#todo: needs review - [probing] [critical: low] new against v4, probes the first video stream
def probe_video_entries(video_path : str, entries : List[str]) -> Dict[str, str]:
commands = ffprobe_builder.chain(
ffprobe_builder.select_video_stream(),
@@ -48,6 +52,7 @@ def probe_video_entries(video_path : str, entries : List[str]) -> Dict[str, str]
return parse_entries(output)
#todo: needs review - [probing] [critical: low] new against v4, container level fallback for duration and bit_rate
def probe_format_entries(video_path : str, entries : List[str]) -> Dict[str, str]:
commands = ffprobe_builder.chain(
ffprobe_builder.show_format_entries(entries),
@@ -59,6 +64,7 @@ def probe_format_entries(video_path : str, entries : List[str]) -> Dict[str, str
return parse_entries(output)
#todo: needs review - [probing] [critical: high] extends v4 with lru_cache, na fallbacks and color_transfer, cache serves stale metadata when files are overwritten
@lru_cache(maxsize = 128)
def extract_video_metadata(video_path : str) -> VideoMetadata:
video_entries = probe_video_entries(video_path, [ 'duration', 'width', 'height', 'r_frame_rate', 'bit_rate', 'color_transfer' ])
@@ -90,6 +96,7 @@ def extract_video_metadata(video_path : str) -> VideoMetadata:
return video_metadata
#todo: no review needed - [probing] 100% copy of v4
def extract_video_fps(frame_rate : str) -> Fps:
if frame_rate and '/' in frame_rate:
numerator, denominator = frame_rate.split('/')
+3
View File
@@ -5,6 +5,7 @@ from typing import List
from facefusion.types import Command
#todo: no review needed - [probing] 100% copy of v4 ffprobe_builder except select_video_stream and show_format_entries
def run(commands : List[Command]) -> List[Command]:
return [ shutil.which('ffprobe'), '-loglevel', 'error' ] + commands
@@ -13,6 +14,7 @@ def chain(*commands : List[Command]) -> List[Command]:
return list(itertools.chain(*commands))
#todo: needs review - [probing] [critical: low] new against v4, selects the first video stream
def select_video_stream() -> List[Command]:
return [ '-select_streams', 'v:0' ]
@@ -21,6 +23,7 @@ def show_entries(entries : List[str]) -> List[Command]:
return [ '-show_entries', 'stream=' + ','.join(entries) ]
#todo: needs review - [probing] [critical: low] new against v4, container format entries
def show_format_entries(entries : List[str]) -> List[Command]:
return [ '-show_entries', 'format=' + ','.join(entries) ]
+2
View File
@@ -136,6 +136,7 @@ LOCALES : Locales =\
'trim_frame_start': 'specify the starting frame of the target video',
'trim_frame_end': 'specify the ending frame of the target video',
'temp_frame_format': 'specify the temporary resources format',
#todo: needs review - [ui] [critical: low] workflow_mode help text replaces keep_temp
'workflow_mode': 'specify how frames flow through processing (disk extracts and merges temp frames, stream pipes frames through ffmpeg)',
'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',
@@ -203,6 +204,7 @@ LOCALES : Locales =\
'clear_button': 'CLEAR',
'download_providers_checkbox_group': 'DOWNLOAD PROVIDERS',
'execution_providers_checkbox_group': 'EXECUTION PROVIDERS',
#todo: needs review - [ui] [critical: low] workflow mode dropdown label replaces common options
'workflow_mode_dropdown': 'WORKFLOW MODE',
'execution_thread_count_slider': 'EXECUTION THREAD COUNT',
'face_detector_angles_checkbox_group': 'FACE DETECTOR ANGLES',
+1
View File
@@ -4,6 +4,7 @@ METADATA =\
{
'name': 'FaceFusion',
'description': 'Industry leading face manipulation platform',
#todo: needs review - [config] [critical: low] version bump to 3.8.0
'version': '3.8.0',
'license': 'OpenRAIL-AS',
'author': 'Henry Ruhs',
+1
View File
@@ -169,6 +169,7 @@ def create_frame_extraction_program() -> ArgumentParser:
group_frame_extraction.add_argument('--trim-frame-start', help = translator.get('help.trim_frame_start'), type = int, default = facefusion.config.get_int_value('frame_extraction', 'trim_frame_start'))
group_frame_extraction.add_argument('--trim-frame-end', help = translator.get('help.trim_frame_end'), type = int, default = facefusion.config.get_int_value('frame_extraction', 'trim_frame_end'))
group_frame_extraction.add_argument('--temp-frame-format', help = translator.get('help.temp_frame_format'), default = config.get_str_value('frame_extraction', 'temp_frame_format', 'png'), choices = facefusion.choices.temp_frame_formats)
#todo: needs review - [workflow] [critical: medium] keep_temp cli option replaced by workflow_mode, breaks existing configs and jobs
group_frame_extraction.add_argument('--workflow-mode', help = translator.get('help.workflow_mode'), default = config.get_str_value('frame_extraction', 'workflow_mode', 'disk'), choices = facefusion.choices.workflow_modes)
job_store.register_step_keys([ 'trim_frame_start', 'trim_frame_end', 'temp_frame_format', 'workflow_mode' ])
return program
+1
View File
@@ -42,6 +42,7 @@ def create_temp_directory(file_path : str) -> bool:
return create_directory(temp_directory_path)
#todo: needs review - [workflow] [critical: medium] keep_temp support removed, the temp directory is always deleted
def clear_temp_directory(file_path : str) -> bool:
temp_directory_path = get_temp_directory_path(file_path)
return remove_directory(temp_directory_path)
+4
View File
@@ -64,6 +64,7 @@ Language = Literal['en']
Locales : TypeAlias = Dict[Language, Dict[str, Any]]
LocalePoolSet : TypeAlias = Dict[str, Locales]
#todo: needs review - [types] [critical: low] writer pool holds ffmpeg processes instead of cv2 writers
VideoWriterSet : TypeAlias = Dict[str, subprocess.Popen[bytes]]
CameraCaptureSet : TypeAlias = Dict[str, cv2.VideoCapture]
CameraPoolSet = TypedDict('CameraPoolSet',
@@ -95,6 +96,7 @@ Duration : TypeAlias = float
Buffer : TypeAlias = bytes
BitRate : TypeAlias = int
#todo: needs review - [types] [critical: low] new buffer, bitrate and reader state types for the pipe based video io
VideoReaderBuffer : TypeAlias = Dict[int, VisionFrame]
VideoReader = TypedDict('VideoReader',
{
@@ -118,6 +120,7 @@ Padding : TypeAlias = Tuple[int, int, int, int]
Margin : TypeAlias = Tuple[int, int, int, int]
Orientation = Literal['landscape', 'portrait']
Resolution : TypeAlias = Tuple[int, int]
#todo: needs review - [types] [critical: low] probed video metadata including color_transfer
ColorTransfer : TypeAlias = str
VideoMetadata = TypedDict('VideoMetadata',
{
@@ -169,6 +172,7 @@ AudioFormat = Literal['flac', 'm4a', 'mp3', 'ogg', 'opus', 'wav']
ImageFormat = Literal['bmp', 'jpeg', 'png', 'tiff', 'webp']
VideoFormat = Literal['avi', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mxf', 'webm', 'wmv']
TempFrameFormat = Literal['bmp', 'jpeg', 'png', 'tiff']
#todo: needs review - [workflow] [critical: medium] new workflow mode replaces keep_temp in state and jobs
WorkflowMode = Literal['disk', 'stream']
AudioTypeSet : TypeAlias = Dict[AudioFormat, str]
ImageTypeSet : TypeAlias = Dict[ImageFormat, str]
+1
View File
@@ -5,6 +5,7 @@ from facefusion.uis.types import JobManagerAction, JobRunnerAction, PreviewMode
job_manager_actions : List[JobManagerAction] = [ 'job-create', 'job-submit', 'job-delete', 'job-add-step', 'job-remix-step', 'job-insert-step', 'job-remove-step' ]
job_runner_actions : List[JobRunnerAction] = [ 'job-run', 'job-run-all', 'job-retry', 'job-retry-all' ]
#todo: needs review - [ui] [critical: low] common_options removed together with keep_temp
preview_modes : List[PreviewMode] = [ 'default', 'frame-by-frame', 'face-by-face' ]
+1
View File
@@ -6,6 +6,7 @@ import facefusion.choices
from facefusion import state_manager, translator
from facefusion.types import WorkflowMode
#todo: needs review - [ui] [critical: low] new workflow mode dropdown component replacing common_options
WORKFLOW_MODE_DROPDOWN : Optional[gradio.Dropdown] = None
+2
View File
@@ -40,6 +40,7 @@ def render() -> gradio.Blocks:
lip_syncer_options.render()
with gradio.Blocks():
voice_extractor.render()
#todo: needs review - [ui] [critical: low] workflow component added, common_options removed
with gradio.Blocks():
workflow.render()
with gradio.Blocks():
@@ -99,6 +100,7 @@ def listen() -> None:
frame_colorizer_options.listen()
frame_enhancer_options.listen()
lip_syncer_options.listen()
#todo: needs review - [ui] [critical: low] workflow listener added, common_options listener removed
workflow.listen()
execution.listen()
execution_thread_count.listen()
+12 -9
View File
@@ -18,7 +18,7 @@ VIDEO_POOL_SET : VideoPoolSet =\
}
#todo: needs review - [process] [critical: medium] new ffmpeg raw pipe reader with seek based start, stderr devnull hides decode errors
#todo: needs review - [decoding] [critical: medium] new ffmpeg raw pipe reader with seek based start, stderr devnull hides decode errors
def create_video_reader_process(video_path : str, frame_position : int, video_fps : Fps) -> subprocess.Popen[bytes]:
color_transfer = extract_video_metadata(video_path).get('color_transfer')
commands = ffmpeg_builder.chain(
@@ -34,7 +34,7 @@ def create_video_reader_process(video_path : str, frame_position : int, video_fp
return subprocess.Popen(commands, stdout = subprocess.PIPE, stderr = subprocess.DEVNULL)
#todo: needs review - [perf] [critical: medium] select filter decodes the entire range to sample frames, runs at lowered process priority
#todo: needs review - [sampling] [critical: medium] select filter decodes the entire range to sample frames, runs at lowered process priority
def create_video_sampler_process(video_path : str, frame_start : int, frame_end : int, frame_stride : int) -> subprocess.Popen[bytes]:
color_transfer = extract_video_metadata(video_path).get('color_transfer')
sample_total = len([ frame_number for frame_number in range(frame_start, frame_end) if frame_number % frame_stride == 0 ])
@@ -59,12 +59,12 @@ def create_video_sampler_process(video_path : str, frame_start : int, frame_end
return subprocess.Popen(commands, stdout = subprocess.PIPE, stderr = subprocess.DEVNULL, preexec_fn = demote_process_priority)
#todo: needs review - [process] [critical: low] posix only nice fallback for the sampler priority
#todo: needs review - [sampling] [critical: low] posix only nice fallback for the sampler priority
def demote_process_priority() -> None:
os.nice(10)
#todo: needs review - [memory] [critical: medium] pooled reader per path is created without lock and only reaped via clear_video_pool
#todo: needs review - [lifecycle] [critical: medium] pooled reader per path is created without lock and only reaped via clear_video_pool
def get_video_reader(video_path : str) -> VideoReader:
if video_path not in VIDEO_POOL_SET.get('reader'):
video_metadata = extract_video_metadata(video_path)
@@ -85,7 +85,7 @@ def get_video_reader(video_path : str) -> VideoReader:
return VIDEO_POOL_SET.get('reader').get(video_path)
#todo: needs review - [perf] [critical: high] forward skip up to 128 frames by draining the pipe, everything else restarts the ffmpeg process
#todo: needs review - [seeking] [critical: high] forward skip up to 128 frames by draining the pipe, everything else restarts the ffmpeg process
def conditional_set_video_reader_position(video_reader : VideoReader, frame_position : int) -> None:
skip_margin = 128
skipping = video_reader.get('position') < frame_position and frame_position - video_reader.get('position') <= skip_margin
@@ -98,7 +98,7 @@ def conditional_set_video_reader_position(video_reader : VideoReader, frame_posi
restart_video_reader(video_reader, frame_position)
#todo: needs review - [process] [critical: high] kill and respawn per out of order seek, frequent restarts stack up on random access
#todo: needs review - [seeking] [critical: high] kill and respawn per out of order seek, frequent restarts stack up on random access
def restart_video_reader(video_reader : VideoReader, frame_position : int) -> None:
video_reader.get('process').kill()
video_reader.get('process').wait()
@@ -107,7 +107,7 @@ def restart_video_reader(video_reader : VideoReader, frame_position : int) -> No
video_reader['frame_buffer'].clear()
#todo: needs review - [correctness] [critical: high] partial pipe read returns none and desyncs position from the actual stream
#todo: needs review - [decoding] [critical: high] partial pipe read returns none and desyncs position from the actual stream
def read_video_reader_frame(video_reader : VideoReader) -> Tuple[bool, Optional[VisionFrame]]:
frame_size = video_reader.get('width') * video_reader.get('height') * 3
frame_buffer = video_reader.get('process').stdout.read(frame_size)
@@ -152,7 +152,7 @@ def read_video_reader_window(video_reader : VideoReader, frame_start : int, fram
return frame_buffer
#todo: needs review - [correctness] [critical: low] rounds odd dimensions to even for yuv420p compatibility
#todo: needs review - [encoding] [critical: low] rounds odd dimensions to even for yuv420p compatibility
def pack_video_resolution(resolution : Resolution) -> str:
width, height = resolution
return str(round(width / 2) * 2) + 'x' + str(round(height / 2) * 2)
@@ -190,7 +190,7 @@ def create_video_writer_process(target_path : str, temp_video_fps : Fps, temp_vi
return subprocess.Popen(commands, stdin = subprocess.PIPE, stdout = subprocess.DEVNULL)
#todo: needs review - [process] [critical: low] pooled writer keyed by target_path
#todo: needs review - [lifecycle] [critical: low] pooled writer keyed by target_path
def get_video_writer(target_path : str, temp_video_fps : Fps, temp_video_resolution : Resolution, output_video_resolution : Resolution, output_video_fps : Fps) -> subprocess.Popen[bytes]:
if target_path not in VIDEO_POOL_SET.get('writer'):
VIDEO_POOL_SET['writer'][target_path] = create_video_writer_process(target_path, temp_video_fps, temp_video_resolution, output_video_resolution, output_video_fps)
@@ -198,16 +198,19 @@ def get_video_writer(target_path : str, temp_video_fps : Fps, temp_video_resolut
return VIDEO_POOL_SET.get('writer').get(target_path)
#todo: needs review - [encoding] [critical: medium] blocking stdin write without backpressure or broken pipe handling
def write_video_writer_frame(video_writer : subprocess.Popen[bytes], vision_frame : VisionFrame) -> None:
video_writer.stdin.write(vision_frame.tobytes())
#todo: needs review - [encoding] [critical: medium] encoder failures only surface via returncode at close time
def close_video_writer(video_writer : subprocess.Popen[bytes]) -> bool:
video_writer.stdin.close()
video_writer.wait()
return video_writer.returncode == 0
#todo: needs review - [lifecycle] [critical: medium] terminates writers mid encode, readers are terminated without reaping stdout
def clear_video_pool() -> None:
for video_writer in VIDEO_POOL_SET.get('writer').values():
video_writer.terminate()
+7
View File
@@ -76,6 +76,7 @@ def read_static_video_frame(video_path : str, frame_number : int = 0) -> Optiona
return read_video_frame(video_path, frame_number)
#todo: needs review - [decoding] [critical: medium] cv2 capture replaced by pooled ffmpeg reader, position clamped to the frame_total estimate
def read_video_frame(video_path : str, frame_number : int = 0) -> Optional[VisionFrame]:
if is_video(video_path):
video_reader = get_video_reader(video_path)
@@ -93,6 +94,7 @@ def read_video_frame(video_path : str, frame_number : int = 0) -> Optional[Visio
return None
#todo: needs review - [memory] [critical: high] all sampled frames are held in ram, long high resolution videos can exhaust memory
def sample_video_frames(video_path : str, frame_start : int, frame_end : int, frame_stride : int) -> List[VisionFrame]:
vision_frames = []
@@ -119,6 +121,7 @@ def read_static_video_chunk(video_path : str, chunk_number : int, chunk_size : i
return read_video_chunk(video_path, chunk_number, chunk_size)
#todo: needs review - [decoding] [critical: medium] chunk end clamped by the ffprobe frame_total estimate instead of the exact cv2 count
def read_video_chunk(video_path : str, chunk_number : int, chunk_size : int) -> Dict[int, VisionFrame]:
video_frame_chunk = {}
@@ -144,6 +147,7 @@ def read_video_chunk(video_path : str, chunk_number : int, chunk_size : int) ->
return video_frame_chunk
#todo: needs review - [decoding] [critical: high] window read replaces the chunk cache, out of range frames fall back to empty vision frames
def select_video_frames(video_path : str, frame_number : int = 0, frame_offset : int = 2) -> List[VisionFrame]:
vision_frames = []
frame_start = frame_number - frame_offset
@@ -165,6 +169,7 @@ def select_video_frames(video_path : str, frame_number : int = 0, frame_offset :
return vision_frames
#todo: needs review - [probing] [critical: medium] frame total is duration times fps estimate, diverges from the real count on vfr sources
def count_video_frame_total(video_path : str) -> int:
if is_video(video_path):
return extract_video_metadata(video_path).get('frame_total')
@@ -180,6 +185,7 @@ def predict_video_frame_total(video_path : str, fps : Fps, trim_frame_start : in
return 0
#todo: needs review - [probing] [critical: low] fps now from cached ffprobe metadata
def detect_video_fps(video_path : str) -> Optional[float]:
if is_video(video_path):
return extract_video_metadata(video_path).get('fps')
@@ -228,6 +234,7 @@ def restrict_trim_frame(video_path : str, trim_frame_start : Optional[int], trim
return 0, video_frame_total
#todo: needs review - [probing] [critical: low] resolution now from cached ffprobe metadata
def detect_video_resolution(video_path : str) -> Optional[Resolution]:
if is_video(video_path):
return extract_video_metadata(video_path).get('resolution')
+10 -1
View File
@@ -20,6 +20,7 @@ from facefusion.vision import conditional_merge_vision_mask, detect_video_resolu
from facefusion.workflows.core import is_process_stopping
#todo: needs review - [workflow] [critical: medium] task list branches on workflow_mode, stream skips extract and merge entirely
def process(start_time : float) -> ErrorCode:
tasks =\
[
@@ -54,6 +55,7 @@ def process(start_time : float) -> ErrorCode:
return 0
#todo: needs review - [workflow] [critical: low] analysis moved out of setup into enforce_analysis
def setup() -> ErrorCode:
if clear_temp_directory(state_manager.get_item('target_path')):
logger.debug(translator.get('clearing_temp'), __name__)
@@ -64,6 +66,7 @@ def setup() -> ErrorCode:
return 0
#todo: needs review - [security] [critical: high] nsfw enforcement gates both workflows and clears temp on hit, must stay ahead of frame processing
def enforce_analysis() -> 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'))
@@ -90,6 +93,7 @@ def extract_frames() -> ErrorCode:
return 0
#todo: needs review - [streaming] [critical: high] ordered submit and pop keeps writes in order, stopping path drains remaining futures without cancel
def process_stream_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'))
output_video_resolution = scale_resolution(detect_video_resolution(state_manager.get_item('target_path')), state_manager.get_item('output_video_scale'))
@@ -107,7 +111,7 @@ def process_stream_frames() -> ErrorCode:
with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor:
futures = []
# TODO: review - this has to be a dedicated method
#todo: needs review - [memory] [critical: high] frame look ahead bound by hardcoded 3gb budget, extract into a dedicated method
width, height = temp_video_resolution
frame_memory_budget = 3 * 1024 ** 3
frame_memory_usage = width * height * 3 * 6
@@ -189,6 +193,7 @@ def restore_audio() -> ErrorCode:
return 0
#todo: needs review - [workflow] [critical: medium] shared processing path for disk and stream, receives vision frames instead of paths
def process_target_frame(frame_number : int, target_vision_frames : List[VisionFrame], temp_vision_frame : VisionFrame) -> VisionFrame:
trim_frame_start, _ = restrict_trim_frame(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end'))
reference_vision_frame = read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number'))
@@ -220,6 +225,7 @@ def process_target_frame(frame_number : int, target_vision_frames : List[VisionF
return conditional_merge_vision_mask(temp_vision_frame, temp_vision_mask)
#todo: needs review - [correctness] [critical: high] missing neighbor frames resolve to none paths and rely on read_static_image returning none
def resolve_disk_target_frames(frame_number : int, frame_amount : int, temp_frame_set : FrameSet) -> List[VisionFrame]:
target_vision_frames = []
@@ -229,6 +235,7 @@ def resolve_disk_target_frames(frame_number : int, frame_amount : int, temp_fram
return target_vision_frames
#todo: needs review - [workflow] [critical: low] disk variant reads temp frame and neighbors from disk and writes back in place
def process_disk_frame(temp_frame_path : str, frame_number : int, temp_frame_set : FrameSet) -> bool:
target_vision_frames = resolve_disk_target_frames(frame_number, state_manager.get_item('target_frame_amount'), temp_frame_set)
temp_vision_frame = read_static_image(temp_frame_path, 'rgba')
@@ -236,6 +243,7 @@ def process_disk_frame(temp_frame_path : str, frame_number : int, temp_frame_set
return write_image(temp_frame_path, temp_vision_frame)
#todo: needs review - [workflow] [critical: medium] renamed from process_video, warms the reference frame before the executor
def process_disk_frames() -> ErrorCode:
temp_frame_set = resolve_temp_frame_set(state_manager.get_item('target_path'))
@@ -271,6 +279,7 @@ def process_disk_frames() -> ErrorCode:
return 0
#todo: needs review - [streaming] [critical: high] middle frame resized to temp resolution before processing, returns contiguous bgr copy
def process_stream_frame(frame_number : int, temp_video_resolution : Resolution) -> Tuple[int, VisionFrame]:
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)