mirror of
https://github.com/facefusion/facefusion.git
synced 2026-07-27 12:30:55 +02:00
annotation for new code
This commit is contained in:
@@ -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 =\
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('/')
|
||||
|
||||
@@ -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) ]
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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' ]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user