Skip to content

Commit 987f6b3

Browse files
authored
fix: extract frames for map faces fallback (#1824)
Verified this fix. Confirmed the bug by reverting just the `modules/core.py` hunk and re-running the new regression test — with the old code, `process_video`/`create_video` run against a temp directory that was never populated when `map_faces=True`, since `create_temp`/`extract_frames` were skipped for that case. That means map-faces video runs were silently broken (empty or failed output). The fix removes the `map_faces` guard so extraction always runs before the disk-based fallback, which is correct for both cases that reach this branch (map_faces=True, and non-map-faces pipe failures). `create_temp` is idempotent (mkdir exist_ok=True), so the double-call for the non-map-faces path is harmless.
1 parent 97a4480 commit 987f6b3

2 files changed

Lines changed: 140 additions & 4 deletions

File tree

modules/core.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,9 @@ def start() -> None:
276276
update_status('Falling back to disk-based processing...')
277277

278278
extraction_start = time.time()
279-
if not modules.globals.map_faces:
280-
create_temp(modules.globals.target_path)
281-
update_status('Extracting frames...')
282-
extract_frames(modules.globals.target_path)
279+
create_temp(modules.globals.target_path)
280+
update_status('Extracting frames...')
281+
extract_frames(modules.globals.target_path)
283282
extraction_time = time.time() - extraction_start
284283

285284
temp_frame_paths = get_temp_frame_paths(modules.globals.target_path)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import importlib
2+
import sys
3+
import types
4+
import unittest
5+
from contextlib import contextmanager
6+
from unittest.mock import patch
7+
8+
9+
@contextmanager
10+
def _patched_core_import_stubs(calls, pipe_result=False):
11+
class Processor:
12+
NAME = "test_processor"
13+
14+
def pre_start(self):
15+
return True
16+
17+
def pre_check(self):
18+
return True
19+
20+
def process_image(self, *_args, **_kwargs):
21+
raise AssertionError("image path should not be used")
22+
23+
def process_video(self, source_path, frame_paths):
24+
calls.append(("process_video", source_path, tuple(frame_paths)))
25+
26+
stubs = {
27+
"cv2": types.SimpleNamespace(
28+
IMREAD_COLOR=1,
29+
imdecode=lambda *_args, **_kwargs: None,
30+
imencode=lambda *_args, **_kwargs: (
31+
True,
32+
types.SimpleNamespace(tofile=lambda *_a, **_k: None),
33+
),
34+
),
35+
"numpy": types.SimpleNamespace(uint8=object, fromfile=lambda *_args, **_kwargs: b""),
36+
"torch": types.SimpleNamespace(
37+
cuda=types.SimpleNamespace(empty_cache=lambda: None)
38+
),
39+
"onnxruntime": types.SimpleNamespace(
40+
get_available_providers=lambda: ["CPUExecutionProvider"]
41+
),
42+
"tensorflow": types.SimpleNamespace(),
43+
"modules.metadata": types.SimpleNamespace(name="Deep-Live-Cam", version="test"),
44+
"modules.ui": types.SimpleNamespace(
45+
check_and_ignore_nsfw=lambda *_args, **_kwargs: False,
46+
update_status=lambda *_args, **_kwargs: None,
47+
init=lambda *_args, **_kwargs: types.SimpleNamespace(mainloop=lambda: None),
48+
),
49+
"modules.processors.frame.core": types.SimpleNamespace(
50+
get_frame_processors_modules=lambda _names: [Processor()],
51+
process_video_in_memory=lambda *_args, **_kwargs: calls.append(("pipe",))
52+
or pipe_result,
53+
),
54+
"modules.utilities": types.SimpleNamespace(
55+
has_image_extension=lambda _path: False,
56+
is_image=lambda _path: False,
57+
is_video=lambda _path: True,
58+
detect_fps=lambda _path: 24.0,
59+
create_video=lambda target_path, fps: calls.append(
60+
("create_video", target_path, fps)
61+
)
62+
or True,
63+
extract_frames=lambda target_path: calls.append(
64+
("extract_frames", target_path)
65+
),
66+
get_temp_frame_paths=lambda target_path: [f"{target_path}/0001.png"],
67+
restore_audio=lambda *_args, **_kwargs: calls.append(("restore_audio",)),
68+
create_temp=lambda target_path: calls.append(("create_temp", target_path)),
69+
move_temp=lambda target_path, output_path: calls.append(
70+
("move_temp", target_path, output_path)
71+
),
72+
clean_temp=lambda target_path: calls.append(("clean_temp", target_path)),
73+
normalize_output_path=lambda _source, _target, output: output,
74+
),
75+
}
76+
with patch.dict(sys.modules, stubs, clear=False):
77+
sys.modules.pop("modules.core", None)
78+
yield importlib.import_module("modules.core")
79+
sys.modules.pop("modules.core", None)
80+
81+
82+
def _configure_video_run(core, *, map_faces):
83+
core.modules.globals.source_path = "source.jpg"
84+
core.modules.globals.target_path = "target.mp4"
85+
core.modules.globals.output_path = "output.mp4"
86+
core.modules.globals.frame_processors = ["face_swapper"]
87+
core.modules.globals.headless = True
88+
core.modules.globals.keep_fps = False
89+
core.modules.globals.keep_audio = False
90+
core.modules.globals.keep_frames = False
91+
core.modules.globals.map_faces = map_faces
92+
core.modules.globals.nsfw_filter = False
93+
core.modules.globals.execution_threads = 1
94+
core.modules.globals.execution_providers = ["CPUExecutionProvider"]
95+
core.modules.globals.max_memory = None
96+
97+
98+
class MapFacesFallbackTests(unittest.TestCase):
99+
def test_map_faces_disk_fallback_extracts_frames_before_processing(self):
100+
calls = []
101+
with _patched_core_import_stubs(calls, pipe_result=False) as core:
102+
_configure_video_run(core, map_faces=True)
103+
104+
with patch.object(core.os.path, "isfile", return_value=True):
105+
core.start()
106+
107+
self.assertNotIn(("pipe",), calls)
108+
self.assertIn(("create_temp", "target.mp4"), calls)
109+
self.assertIn(("extract_frames", "target.mp4"), calls)
110+
self.assertIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
111+
self.assertIn(("create_video", "target.mp4", 30.0), calls)
112+
self.assertIn(("move_temp", "target.mp4", "output.mp4"), calls)
113+
114+
step_indices = {}
115+
for index, call in enumerate(calls):
116+
step_indices.setdefault(call[0], index)
117+
118+
self.assertLess(step_indices["create_temp"], step_indices["extract_frames"])
119+
self.assertLess(step_indices["extract_frames"], step_indices["process_video"])
120+
self.assertLess(step_indices["process_video"], step_indices["create_video"])
121+
self.assertLess(step_indices["create_video"], step_indices["move_temp"])
122+
123+
def test_non_map_faces_pipe_success_does_not_extract_frames(self):
124+
calls = []
125+
with _patched_core_import_stubs(calls, pipe_result=True) as core:
126+
_configure_video_run(core, map_faces=False)
127+
128+
with patch.object(core.os.path, "isfile", return_value=True):
129+
core.start()
130+
131+
self.assertIn(("pipe",), calls)
132+
self.assertNotIn(("extract_frames", "target.mp4"), calls)
133+
self.assertNotIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
134+
135+
136+
if __name__ == "__main__":
137+
unittest.main()

0 commit comments

Comments
 (0)