From b22f3e01022ef577a2efc396ba2e7bb5515dffff Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Wed, 23 Aug 2023 22:01:15 +0400 Subject: [PATCH 01/10] issue #61 Show the lost frames sequences, if present --- sinner/State.py | 20 +++++++++++++++++++- sinner/utilities.py | 18 ++++++++++++++++++ tests/test_utilities.py | 7 ++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/sinner/State.py b/sinner/State.py index f83f3778..916d4227 100644 --- a/sinner/State.py +++ b/sinner/State.py @@ -6,7 +6,7 @@ from sinner.Status import Status, Mood from sinner.handlers.frame.CV2VideoHandler import CV2VideoHandler from sinner.typing import Frame -from sinner.utilities import is_absolute_path +from sinner.utilities import is_absolute_path, format_sequences from sinner.validators.AttributeLoader import Rules @@ -24,6 +24,7 @@ class State(Status): final_check_state: bool = True final_check_empty: bool = True + final_check_integrity: bool = True def rules(self) -> Rules: return [ @@ -162,4 +163,21 @@ def final_check(self) -> bool: if zero_sized_files_count > 0: self.update_status(message=f"There is zero-sized files in {self.path} temp directory ({zero_sized_files_count} of {processed_frames_count}). Check for free disk space and access rights.", mood=Mood.BAD) result = False + + if self.final_check_integrity: + lost_frames = self.check_integrity() + if lost_frames: + self.update_status(message=f"There is lost frames in the processed sequence: {format_sequences(lost_frames)}", mood=Mood.BAD) + result = False + return result + + def check_integrity(self) -> List[int]: + result: List[int] = [] + for frame in range(self.frames_count): + f_name = self.get_frame_processed_name(frame) + if not os.path.exists(f_name): + result.append(frame) + return result + + diff --git a/sinner/utilities.py b/sinner/utilities.py index c83aeafe..7591fc90 100644 --- a/sinner/utilities.py +++ b/sinner/utilities.py @@ -207,3 +207,21 @@ def is_int(value: str) -> bool: return True except ValueError: return False + + +def format_sequences(sorted_list: List[int]) -> str: + def format_sequence(s_start: int, s_end: int) -> str: + return str(start) if s_start == s_end else f"{start}..{end}" + + sequences = [] + start = end = sorted_list[0] + + for num in sorted_list[1:]: + if num == end + 1: + end = num + else: + sequences.append(format_sequence(start, end)) + start = end = num + + sequences.append(format_sequence(start, end)) + return ", ".join(sequences) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 9989e5e8..ff6e1937 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,4 +1,4 @@ -from sinner.utilities import get_all_base_names +from sinner.utilities import get_all_base_names, format_sequences def test_get_all_base_names() -> None: @@ -36,3 +36,8 @@ class F(D): assert 'B' in get_all_base_names(F) assert 'E' not in get_all_base_names(F) assert 'C' not in get_all_base_names(F) + + +def test_find_sequences() -> None: + assert format_sequences([1, 2, 3, 4, 10, 20, 21, 22, 23]) == '1..4, 10, 20..23' + assert format_sequences([100, 3, 2, 3, 4, 5]) == '100, 3, 2..5' From b3b8b3879722d1eda6f6e59f960fbf67f600550f Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Wed, 23 Aug 2023 22:24:53 +0400 Subject: [PATCH 02/10] issue #61 Implement the code to reprocess lost frames --- sinner/State.py | 8 +++----- sinner/processors/frame/BaseFrameProcessor.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/sinner/State.py b/sinner/State.py index 916d4227..c6f6aae4 100644 --- a/sinner/State.py +++ b/sinner/State.py @@ -148,7 +148,7 @@ def zfill_length(self) -> int: self._zfill_length = len(str(self.frames_count)) return self._zfill_length - def final_check(self) -> bool: + def final_check(self) -> tuple[bool, List[int]]: result = True processed_frames_count = self.processed_frames_count if self.final_check_state and not self.is_finished: @@ -163,14 +163,14 @@ def final_check(self) -> bool: if zero_sized_files_count > 0: self.update_status(message=f"There is zero-sized files in {self.path} temp directory ({zero_sized_files_count} of {processed_frames_count}). Check for free disk space and access rights.", mood=Mood.BAD) result = False - + lost_frames = [] if self.final_check_integrity: lost_frames = self.check_integrity() if lost_frames: self.update_status(message=f"There is lost frames in the processed sequence: {format_sequences(lost_frames)}", mood=Mood.BAD) result = False - return result + return result, lost_frames def check_integrity(self) -> List[int]: result: List[int] = [] @@ -179,5 +179,3 @@ def check_integrity(self) -> List[int]: if not os.path.exists(f_name): result.append(frame) return result - - diff --git a/sinner/processors/frame/BaseFrameProcessor.py b/sinner/processors/frame/BaseFrameProcessor.py index c9900160..8e307698 100644 --- a/sinner/processors/frame/BaseFrameProcessor.py +++ b/sinner/processors/frame/BaseFrameProcessor.py @@ -110,7 +110,17 @@ def process(self, desc: str = 'Processing', set_progress: Callable[[int], None] initial=self.state.processed_frames_count, ) as progress: self.multi_process_frame(frames_iterator=self.handler, state=self.state, process_frames=self.process_frames, progress=progress) - if not self.state.final_check(): + _, lost_frames = self.state.final_check() + if lost_frames: + with tqdm( + total=len(lost_frames), + desc="Processing lost frames", unit='frame', + dynamic_ncols=True, + bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]', + ) as progress: + self.multi_process_frame(frames_iterator=lost_frames, state=self.state, process_frames=self.process_frames, progress=progress) + is_ok, _ = self.state.final_check() + if not is_ok: raise Exception("Something went wrong on processed frames check") @abstractmethod From 0a3354b4192c98ddd45e367305e347c3d91c298b Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Wed, 23 Aug 2023 22:27:08 +0400 Subject: [PATCH 03/10] issue #61 update tests --- tests/test_state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_state.py b/tests/test_state.py index 3cfa84f3..b52f32e4 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -103,14 +103,14 @@ def test_states() -> None: def test_final_check_ok(): state = State(parameters=Namespace(), target_path=target_mp4, temp_dir=tmp_dir, frames_count=TARGET_FC, processor_name='DummyProcessor') shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) - assert state.final_check() is True + assert state.final_check() is (True, []) def test_final_check_fail_state(): state = State(parameters=Namespace(), target_path=target_mp4, temp_dir=tmp_dir, frames_count=TARGET_FC, processor_name='DummyProcessor') shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) os.remove(os.path.join(state.path, '05.png')) - assert state.final_check() is False + assert state.final_check() is (False, []) def test_final_check_fail_zero_files(): @@ -118,4 +118,4 @@ def test_final_check_fail_zero_files(): shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) with open(os.path.join(state.path, '04.png'), 'r+') as file: file.truncate(0) - assert state.final_check() is False + assert state.final_check() is (False, []) From 0aea2ac4862d533ee1843d6ab10be3b2cf72a523 Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Wed, 23 Aug 2023 22:48:10 +0400 Subject: [PATCH 04/10] issue #61 Add the test case for a new feature --- tests/test_run.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_run.py b/tests/test_run.py index 641859d6..4424b2d6 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -8,7 +8,8 @@ from sinner.Parameters import Parameters from sinner.Core import Core -from sinner.utilities import limit_resources, suggest_max_memory, get_file_name +from sinner.processors.frame.DummyProcessor import DummyProcessor +from sinner.utilities import limit_resources, suggest_max_memory, get_file_name, get_app_dir, resolve_relative_path from sinner.validators.LoaderException import LoadingException from tests.constants import target_png, source_jpg, target_mp4, source_target_png_result, source_target_mp4_result, state_frames_dir, result_mp4, tmp_dir, result_png, TARGET_FC, images_dir, source_images_result @@ -140,3 +141,18 @@ def test_set_execution_provider(capsys) -> None: captured = capsys.readouterr() assert "Error Unknown Provider Type" not in captured.out assert os.path.exists(result_png) is True + + +def test_reprocess_lost_frames() -> None: + case_temp_dir = resolve_relative_path('temp/DummyProcessor/frames/source.jpg', get_app_dir()) + assert os.path.exists(case_temp_dir) is False + params = Parameters(f'--target-path="{state_frames_dir}" --source-path="{source_jpg}" --output-path="{result_mp4}" --execution-treads={threads_count}') + current_processor = DummyProcessor(params.parameters) + current_processor.process() + assert os.path.exists(case_temp_dir) is True + assert len(glob.glob(os.path.join(case_temp_dir, '*.png'))) == 10 + os.remove(os.path.join(case_temp_dir, '05.png')) + os.remove(os.path.join(case_temp_dir, '08.png')) + assert len(glob.glob(os.path.join(case_temp_dir, '*.png'))) == 8 + current_processor.process() + assert len(glob.glob(os.path.join(case_temp_dir, '*.png'))) == 10 From a76d0f2cfae0c1021833644387433fc94e96bb21 Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Wed, 23 Aug 2023 23:52:18 +0400 Subject: [PATCH 05/10] issue #61 tests fixed --- tests/test_state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_state.py b/tests/test_state.py index b52f32e4..0255f1ab 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -103,14 +103,14 @@ def test_states() -> None: def test_final_check_ok(): state = State(parameters=Namespace(), target_path=target_mp4, temp_dir=tmp_dir, frames_count=TARGET_FC, processor_name='DummyProcessor') shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) - assert state.final_check() is (True, []) + assert state.final_check() == (True, []) def test_final_check_fail_state(): state = State(parameters=Namespace(), target_path=target_mp4, temp_dir=tmp_dir, frames_count=TARGET_FC, processor_name='DummyProcessor') shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) os.remove(os.path.join(state.path, '05.png')) - assert state.final_check() is (False, []) + assert state.final_check() == (False, []) def test_final_check_fail_zero_files(): @@ -118,4 +118,4 @@ def test_final_check_fail_zero_files(): shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) with open(os.path.join(state.path, '04.png'), 'r+') as file: file.truncate(0) - assert state.final_check() is (False, []) + assert state.final_check() == (False, []) From 8d631d0e26c1cfdfda54b46d41481a0e3f5dec66 Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Thu, 24 Aug 2023 00:08:57 +0400 Subject: [PATCH 06/10] issue #61 test fixed --- tests/test_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_state.py b/tests/test_state.py index 0255f1ab..f1295db5 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -110,7 +110,7 @@ def test_final_check_fail_state(): state = State(parameters=Namespace(), target_path=target_mp4, temp_dir=tmp_dir, frames_count=TARGET_FC, processor_name='DummyProcessor') shutil.copytree(state_frames_dir, state.path, dirs_exist_ok=True) os.remove(os.path.join(state.path, '05.png')) - assert state.final_check() == (False, []) + assert state.final_check() == (False, [5]) def test_final_check_fail_zero_files(): From d37b46b14b87157a6c7057755b0913640f0762c9 Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Thu, 24 Aug 2023 09:01:42 +0400 Subject: [PATCH 07/10] issue #61 Do integrity check only if state is not finalized -> faster check, allow to pass the test --- sinner/State.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinner/State.py b/sinner/State.py index c6f6aae4..2291bcdd 100644 --- a/sinner/State.py +++ b/sinner/State.py @@ -164,7 +164,7 @@ def final_check(self) -> tuple[bool, List[int]]: self.update_status(message=f"There is zero-sized files in {self.path} temp directory ({zero_sized_files_count} of {processed_frames_count}). Check for free disk space and access rights.", mood=Mood.BAD) result = False lost_frames = [] - if self.final_check_integrity: + if self.final_check_integrity and not self.is_finished: lost_frames = self.check_integrity() if lost_frames: self.update_status(message=f"There is lost frames in the processed sequence: {format_sequences(lost_frames)}", mood=Mood.BAD) From b49e732ed967b88682851308871e5d4b933a23cd Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Thu, 24 Aug 2023 09:25:43 +0400 Subject: [PATCH 08/10] replace GFPGAN download url to the real one --- sinner/processors/frame/FaceEnhancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinner/processors/frame/FaceEnhancer.py b/sinner/processors/frame/FaceEnhancer.py index a773b897..1a93ad27 100644 --- a/sinner/processors/frame/FaceEnhancer.py +++ b/sinner/processors/frame/FaceEnhancer.py @@ -76,7 +76,7 @@ def face_enhancer(self) -> GFPGANer: def __init__(self, parameters: Namespace, target_path: str | None = None) -> None: download_directory_path = get_app_dir('models') - conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/GFPGANv1.4.pth']) + conditional_download(download_directory_path, ['https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth']) super().__init__(parameters, target_path) def enhance_face(self, temp_frame: Frame) -> Frame: From 93eb95edff7413f0e8af676acf6fc36ee047f18a Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Thu, 24 Aug 2023 09:31:19 +0400 Subject: [PATCH 09/10] remove credits line --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 20efc8f2..72a73f8b 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,6 @@ sinner uses the same ML libraries to perform its magic, but handles them in its ## Credits - [s0md3v](https://github.com/s0md3v/): the original author of roop -- [henryruhs](https://github.com/henryruhs): the significant contributor to roop - [ffmpeg](https://ffmpeg.org/): for making video related operations easy - [deepinsight](https://github.com/deepinsight): for their [insightface](https://github.com/deepinsight/insightface) project which provided a well-made library and models. - [ARC Lab, Tencent PCG](https://github.com/TencentARC): for their [GFPGAN](https://github.com/TencentARC/GFPGAN) project which provided a face restoration library and models. From 5aff841eb58a015599a19d3ef4a2dc8d35d53ae3 Mon Sep 17 00:00:00 2001 From: Pozitronik Date: Thu, 24 Aug 2023 09:33:43 +0400 Subject: [PATCH 10/10] replace inswapper download url to the one from releases --- sinner/processors/frame/FaceSwapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinner/processors/frame/FaceSwapper.py b/sinner/processors/frame/FaceSwapper.py index e20b8b5f..2542b4f0 100644 --- a/sinner/processors/frame/FaceSwapper.py +++ b/sinner/processors/frame/FaceSwapper.py @@ -101,7 +101,7 @@ def face_swapper(self) -> FaceSwapperType: def __init__(self, parameters: Namespace, target_path: str | None = None) -> None: download_directory_path = get_app_dir('models') - conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/inswapper_128.onnx']) + conditional_download(download_directory_path, ['https://github.com/pozitronik/sinner/releases/download/v200823/inswapper_128.onnx']) super().__init__(parameters, target_path) def process_frame(self, frame: Frame) -> Frame: