diff --git a/facefusion/choices.py b/facefusion/choices.py index 44fa598c..35de62a6 100755 --- a/facefusion/choices.py +++ b/facefusion/choices.py @@ -2,7 +2,7 @@ import logging from typing import List, Sequence, get_args from facefusion.common_helper import create_float_range, create_int_range -from facefusion.types import Angle, ApiSecurityStrategy, AudioEncoder, AudioFormat, AudioSet, BenchmarkMode, BenchmarkResolution, BenchmarkSet, DownloadProvider, DownloadProviderSet, DownloadScope, ExecutionProvider, ExecutionProviderSet, FaceAlignerModel, FaceDetectorModel, FaceDetectorSet, FaceMaskArea, FaceMaskAreaSet, FaceMaskRegion, FaceMaskRegionSet, FaceMaskType, FaceOccluderModel, FaceParserModel, FaceSelectorGender, FaceSelectorMode, FaceSelectorOrder, FaceSelectorRace, Gender, ImageEncoder, ImageFormat, ImageSet, JobStatus, LogLevel, LogLevelSet, Race, Score, TempFrameFormat, TempPixelFormat, VideoEncoder, VideoFormat, VideoMemoryStrategy, VideoPreset, VideoSet, VoiceExtractorModel, WorkflowMode, WorkflowStrategy +from facefusion.types import Angle, ApiSecurityStrategy, AudioEncoder, AudioFormat, AudioSet, BenchmarkMode, BenchmarkResolution, BenchmarkSet, DownloadProvider, DownloadProviderSet, DownloadScope, ExecutionProvider, ExecutionProviderSet, FaceAlignerModel, FaceDetectorModel, FaceDetectorSet, FaceMaskArea, FaceMaskAreaSet, FaceMaskRegion, FaceMaskRegionSet, FaceMaskType, FaceOccluderModel, FaceParserModel, FaceSelectorGender, FaceSelectorMode, FaceSelectorOrder, FaceSelectorRace, Gender, ImageEncoder, ImageFormat, ImageSet, JobStatus, LogLevel, LogLevelSet, ProgressActionSet, Race, Score, TempFrameFormat, TempPixelFormat, VideoEncoder, VideoFormat, VideoMemoryStrategy, VideoPreset, VideoSet, VoiceExtractorModel, WorkflowMode, WorkflowStrategy face_detector_set : FaceDetectorSet =\ { @@ -151,6 +151,15 @@ log_level_set : LogLevelSet =\ } log_levels : List[LogLevel] = list(get_args(LogLevel)) +progress_action_set : ProgressActionSet =\ +{ + 'color_active': '\033[94m', + 'color_neutral': '\033[90m', + 'cursor_start': '\033[G', + 'erase_line': '\033[K', + 'reset': '\033[0m' +} + job_statuses : List[JobStatus] = list(get_args(JobStatus)) benchmark_cycle_count_range : Sequence[int] = create_int_range(1, 10, 1) diff --git a/facefusion/cli_progress.py b/facefusion/cli_progress.py new file mode 100644 index 00000000..050997e0 --- /dev/null +++ b/facefusion/cli_progress.py @@ -0,0 +1,127 @@ +import os +import shutil +import sys +import time +from contextlib import contextmanager +from functools import partial +from types import SimpleNamespace +from typing import Iterator, Sized + +from facefusion import choices +from facefusion.types import ProgressUnit + + +@contextmanager +def create(unit : ProgressUnit = 'frame', current : int = 0, total : int = 0) -> Iterator[SimpleNamespace]: + progress = SimpleNamespace( + unit = unit, + current = current, + total = total, + time_start = time.monotonic(), + time_update = 0.0 + ) + progress.count = partial(count, progress) + progress.set_title = partial(set_title, progress) + progress.set_description = partial(set_description, progress) + progress.update = partial(update, progress) + progress.seek = partial(seek, progress) + + yield progress + + progress.current = progress.total + render(progress) + + sys.stdout.flush() + sys.stdout.write(os.linesep) + + +def set_title(progress : SimpleNamespace, title : str) -> None: + progress.title = title + + +def set_description(progress : SimpleNamespace, description : str) -> None: + progress.description = description + + +def count(progress : SimpleNamespace, collection : Sized) -> None: + progress.total = len(collection) + + +def update(progress : SimpleNamespace) -> None: + seek(progress, progress.current + 1) + + +def seek(progress : SimpleNamespace, current : int) -> None: + progress.current = current + time_current = time.monotonic() + + if time_current - progress.time_update > 0.5: + progress.time_update = time_current + render(progress) + + +def render(progress : SimpleNamespace) -> None: + title = getattr(progress, 'title', '') + description = getattr(progress, 'description', '') + status = str(progress.current) + + if progress.unit == 'percent': + status = resolve_percent(progress) + + if progress.unit == 'frame': + status = resolve_frame(progress) + + if progress.unit == 'download': + status = resolve_download(progress) + + progress_width = shutil.get_terminal_size().columns - len(title) - len(status) - 2 + + if description: + progress_width -= len(description) + 3 + + if progress.total > 0: + progress_fill = progress.current * progress_width // progress.total + else: + progress_fill = 0 + + progress_bar = choices.progress_action_set.get('color_active') + '=' * progress_fill + choices.progress_action_set.get('color_neutral') + '=' * (progress_width - progress_fill) + choices.progress_action_set.get('reset') + progress_parts = [ title, progress_bar, status ] + + if description: + progress_parts.append('|') + progress_parts.append(description) + + sys.stdout.write(choices.progress_action_set.get('cursor_start') + ' '.join(progress_parts) + choices.progress_action_set.get('erase_line')) + sys.stdout.flush() + + +def resolve_percent(progress : SimpleNamespace) -> str: + if progress.total > 0: + percent = progress.current * 100 // progress.total + return str(percent) + ' %' + + return '0 %' + + +def resolve_frame(progress : SimpleNamespace) -> str: + time_current = time.monotonic() + + if time_current - progress.time_start > 0: + rate = progress.current / (time_current - progress.time_start) + return str(round(rate, 1)) + 'frame/s' + + return '0.0frame/s' + + +def resolve_download(progress : SimpleNamespace) -> str: + time_current = time.monotonic() + + if time_current - progress.time_start > 0: + rate = progress.current / (time_current - progress.time_start) + + if rate > 1024 * 1024: + return str(round(rate / (1024 * 1024), 1)) + 'mb/s' + + return str(round(rate / 1024, 1)) + 'kb/s' + + return '0.0kb/s' diff --git a/facefusion/content_analyser.py b/facefusion/content_analyser.py index 6caf43fe..f7b7b38f 100644 --- a/facefusion/content_analyser.py +++ b/facefusion/content_analyser.py @@ -2,9 +2,8 @@ from functools import lru_cache from typing import Tuple import numpy -from tqdm import tqdm -from facefusion import inference_manager, state_manager, translator, video_manager +from facefusion import cli_progress, inference_manager, translator, video_manager from facefusion.download import conditional_download_hashes, conditional_download_sources, resolve_download_url from facefusion.filesystem import resolve_relative_path from facefusion.thread_helper import conditional_thread_semaphore @@ -166,7 +165,9 @@ def analyse_video(video_path : str, trim_frame_start : int, trim_frame_end : int if trim_frame_start > 0: video_manager.seek_video_reader(video_reader, trim_frame_start) - with tqdm(total = len(frame_range), desc = translator.get('analysing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + with cli_progress.create(unit = 'frame') as progress: + progress.set_title(translator.get('analysing')) + progress.count(frame_range) for frame_number in frame_range: vision_frame = video_manager.read_video_frame(video_reader) @@ -181,7 +182,7 @@ def analyse_video(video_path : str, trim_frame_start : int, trim_frame_end : int if counter > 0 and total > 0: rate = counter / total * 100 - progress.set_postfix(rate = rate) + progress.set_description('rate = ' + str(rate)) progress.update() return bool(rate > 10.0) diff --git a/facefusion/core.py b/facefusion/core.py index 087b07fb..fa6524ce 100755 --- a/facefusion/core.py +++ b/facefusion/core.py @@ -101,8 +101,7 @@ def pre_check() -> bool: def common_pre_check() -> bool: content_analyser_content = inspect.getsource(content_analyser).encode() - - return hash_helper.create_hash(content_analyser_content) == '3c6ce25e' + return hash_helper.create_hash(content_analyser_content) == '61e33ef3' def processors_pre_check() -> bool: diff --git a/facefusion/download.py b/facefusion/download.py index 5f4c7152..49ccecad 100644 --- a/facefusion/download.py +++ b/facefusion/download.py @@ -4,10 +4,8 @@ from functools import lru_cache from typing import List, Optional, Tuple from urllib.parse import urlparse -from tqdm import tqdm - import facefusion.choices -from facefusion import curl_builder, logger, process_manager, state_manager, translator +from facefusion import cli_progress, curl_builder, logger, process_manager, state_manager, translator from facefusion.filesystem import get_file_name, get_file_size, is_file, remove_file from facefusion.hash_helper import validate_hash from facefusion.types import Buffer, Command, DownloadProvider, DownloadSet @@ -26,7 +24,10 @@ def conditional_download(download_directory_path : str, urls : List[str]) -> Non download_size = get_static_download_size(url) if initial_size < download_size: - with tqdm(total = download_size, initial = initial_size, desc = translator.get('downloading'), unit = 'B', unit_scale = True, unit_divisor = 1024, ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + with cli_progress.create(unit = 'download', total = download_size) as progress: + progress.set_title(translator.get('downloading')) + progress.set_description('file_name = ' + download_file_name) + commands = curl_builder.chain( curl_builder.download(url, download_file_path), curl_builder.set_timeout(5), @@ -34,12 +35,11 @@ def conditional_download(download_directory_path : str, urls : List[str]) -> Non ) open_curl(commands) current_size = initial_size - progress.set_postfix(download_providers = state_manager.get_item('download_providers'), file_name = download_file_name) while current_size < download_size: if is_file(download_file_path): current_size = get_file_size(download_file_path) - progress.update(current_size - progress.n) + progress.seek(current_size) @lru_cache(maxsize = 64) diff --git a/facefusion/ffmpeg.py b/facefusion/ffmpeg.py index 0b97c6da..18d09f3a 100644 --- a/facefusion/ffmpeg.py +++ b/facefusion/ffmpeg.py @@ -1,13 +1,11 @@ import os import subprocess import tempfile -from functools import lru_cache, partial +from functools import lru_cache from typing import List, Optional, cast -from tqdm import tqdm - import facefusion.choices -from facefusion import ffmpeg_builder, ffprobe, logger, process_manager, state_manager, translator, vision +from facefusion import cli_progress, ffmpeg_builder, ffprobe, logger, process_manager, state_manager, translator, vision from facefusion.filesystem import get_file_format, remove_file from facefusion.temp_helper import get_temp_file_path, get_temp_frames_pattern from facefusion.types import ApiSecurityStrategy, AudioEncoder, Buffer, Command, EncoderSet, Fps, Resolution, SampleRate, UpdateProgress, VideoEncoder, VideoFormat, VideoReaderMetadata @@ -40,10 +38,6 @@ def run_ffmpeg_with_progress(commands : List[Command], update_progress : UpdateP return process -def update_progress(progress : tqdm, frame_number : int) -> None: - progress.update(frame_number - progress.n) - - def run_ffmpeg_with_pipe(commands : List[Command], file_content : Buffer) -> subprocess.Popen[Buffer]: commands = ffmpeg_builder.run(commands) process = subprocess.Popen(commands, stdin = subprocess.PIPE, stderr = subprocess.PIPE, stdout = subprocess.PIPE) @@ -184,8 +178,9 @@ def extract_frames(target_path : str, output_path : str, temp_video_resolution : ffmpeg_builder.set_output(temp_frames_pattern) ) - with tqdm(total = extract_frame_total, desc = translator.get('extracting'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: - process = run_ffmpeg_with_progress(commands, partial(update_progress, progress)) + with cli_progress.create(total = extract_frame_total) as progress: + progress.set_title(translator.get('extracting')) + process = run_ffmpeg_with_progress(commands, progress.seek) return process.returncode == 0 @@ -202,8 +197,9 @@ def spawn_frames(target_path : str, output_path : str, temp_video_resolution : R ffmpeg_builder.set_output(temp_frames_pattern) ) - with tqdm(total = spawn_frame_total, desc = translator.get('spawning'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: - process = run_ffmpeg_with_progress(commands, partial(update_progress, progress)) + with cli_progress.create(total = spawn_frame_total) as progress: + progress.set_title(translator.get('spawning')) + process = run_ffmpeg_with_progress(commands, progress.seek) return process.returncode == 0 @@ -328,8 +324,9 @@ def merge_video(target_path : str, output_path : str, temp_video_fps : Fps, outp ffmpeg_builder.force_output(temp_video_path) ) - with tqdm(total = merge_frame_total, desc = translator.get('merging'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: - process = run_ffmpeg_with_progress(commands, partial(update_progress, progress)) + with cli_progress.create(total = merge_frame_total) as progress: + progress.set_title(translator.get('merging')) + process = run_ffmpeg_with_progress(commands, progress.seek) return process.returncode == 0 diff --git a/facefusion/streamer.py b/facefusion/streamer.py index ff58c03c..fba3921c 100644 --- a/facefusion/streamer.py +++ b/facefusion/streamer.py @@ -5,9 +5,8 @@ from concurrent.futures import ThreadPoolExecutor from typing import Deque, Iterator, List import cv2 -from tqdm import tqdm -from facefusion import ffmpeg_builder, logger, state_manager, translator +from facefusion import cli_progress, ffmpeg_builder, logger, state_manager, translator from facefusion.audio import create_empty_audio_frame from facefusion.content_analyser import analyse_stream from facefusion.ffmpeg import open_ffmpeg @@ -21,7 +20,9 @@ def multi_process_capture(camera_capture : cv2.VideoCapture, camera_fps : Fps) - capture_deque : Deque[VisionFrame] = deque() source_vision_frames = read_static_images(state_manager.get_item('source_paths')) - with tqdm(desc = translator.get('streaming'), unit = 'frame', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + with cli_progress.create() as progress: + progress.set_title(translator.get('streaming')) + with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: futures = [] diff --git a/facefusion/types.py b/facefusion/types.py index db6ae03c..62ec68da 100755 --- a/facefusion/types.py +++ b/facefusion/types.py @@ -177,6 +177,10 @@ ErrorCode = Literal[0, 1, 2, 3, 4] LogLevel = Literal['error', 'warn', 'info', 'debug'] LogLevelSet : TypeAlias = Dict[LogLevel, int] +ProgressAction = Literal['color_active', 'color_neutral', 'cursor_start', 'erase_line', 'reset'] +ProgressActionSet : TypeAlias = Dict[ProgressAction, str] +ProgressUnit = Literal['percent', 'frame', 'download'] + TableHeader : TypeAlias = str TableContent : TypeAlias = Any diff --git a/facefusion/workflows/core.py b/facefusion/workflows/core.py index 54d995c9..2a114b24 100644 --- a/facefusion/workflows/core.py +++ b/facefusion/workflows/core.py @@ -3,9 +3,8 @@ from concurrent.futures import Future, ThreadPoolExecutor from typing import Deque, List import numpy -from tqdm import tqdm -from facefusion import logger, process_manager, state_manager, translator +from facefusion import cli_progress, logger, process_manager, state_manager, translator from facefusion.audio import create_empty_audio_frame, get_audio_frame, get_voice_frame from facefusion.common_helper import get_first from facefusion.filesystem import filter_audio_paths, get_file_extension, has_audio, has_image, has_video @@ -150,8 +149,9 @@ def process_frames() -> ErrorCode: temp_frame_set = resolve_temp_frame_set(state_manager.get_temp_path(), state_manager.get_item('output_path'), state_manager.get_item('temp_frame_format')) if temp_frame_set: - with tqdm(total = len(temp_frame_set), desc = translator.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: - progress.set_postfix(execution_providers = state_manager.get_item('execution_providers')) + with cli_progress.create(unit = 'frame') as progress: + progress.set_title(translator.get('processing')) + progress.count(temp_frame_set) with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: futures : Deque[Future[bool]] = deque() diff --git a/facefusion/workflows/to_video.py b/facefusion/workflows/to_video.py index deadd513..ae4ced3b 100644 --- a/facefusion/workflows/to_video.py +++ b/facefusion/workflows/to_video.py @@ -4,9 +4,8 @@ from typing import Deque import cv2 import numpy -from tqdm import tqdm -from facefusion import content_analyser, ffmpeg, logger, process_manager, state_manager, translator, video_manager +from facefusion import cli_progress, content_analyser, ffmpeg, logger, process_manager, state_manager, translator, video_manager from facefusion.common_helper import get_first, get_middle from facefusion.filesystem import filter_audio_paths, is_video from facefusion.media_helper import restrict_trim_frame @@ -75,8 +74,9 @@ def process_memory_frames() -> ErrorCode: if temp_frame_range: video_writer = video_manager.get_writer(state_manager.get_item('output_path'), temp_video_fps, output_video_resolution, output_video_resolution, state_manager.get_item('output_video_fps')) - with tqdm(total = len(temp_frame_range), desc = translator.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: - progress.set_postfix(execution_providers = state_manager.get_item('execution_providers')) + with cli_progress.create(unit = 'frame') as progress: + progress.set_title(translator.get('processing')) + progress.count(temp_frame_range) read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) diff --git a/requirements.txt b/requirements.txt index e72c0933..0f19c728 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ onnxruntime==1.28.0 opencv-python-headless==5.0.0.93 psutil==7.2.2 python-multipart==0.0.28 -tqdm==4.70.0 scipy==1.18.0 starlette==0.52.1 uvicorn==0.41.0