plan(P2): address review findings
Five review findings of the P2 phase, each fixed at its root: 1. (medium) The P2.6 guard could not see an undeclared third-party import: the closure's imports were filtered through a hard-coded 10-name map, so every module outside it was dropped silently (a module-level `import av` in a copy of the repo still gave "1 passed"). The map is now a *rename* map (cv2 -> opencv-python, PIL -> Pillow, yaml -> pyyaml); every other module is mapped to its own name, so an unknown module shows up as an undeclared dependency. The guard is split by import time: every third-party module imported at import time must be a declared distribution, every lazy (function-level) one must be allowlisted with a reason (dlib, gdown, mc, wandb - all unreachable from the node). 2. (low) The P2.1 (c) progression test was vacuous: it returned before its assertions and probed a second, top-level copy of the vendored tree (`sys.path.insert(modules/deps)`). It now imports the packaged architecture, asserts that FeedForward / AdaLayerNorm come from the vendored _diffusers_compat and that no top-level `basicsr` shadows an installed one. 3. (low) Two swallow-and-stub fallbacks remained in the vendored tree (facelib/detection, facelib/parsing - their stubs called an unimported `urlparse` and would have raised NameError). Both now import the vendored symbol directly; a guard forbids a fallback stub in the vendored tree. 4. (low) `basicsr/utils/file_client.py` still appended to `sys.path` (the optional `sys_path` of MemcachedBackend), which the plan's §3 rule forbids anywhere in the package. The mutation and the parameter are gone and guard (d) now covers the whole package (the vendored tree included). 5. (low) The unreachable body of `KEEPFaceProcessor.process_video` still called the deleted VideoReader / VideoWriter. The 141 dead lines are removed; an AST guard keeps the references out. Found while fixing the findings: the CI job did not install `requests`, which the vendored `basicsr/utils/download_util.py` imports at module level (P2.6 added it to requirements.txt but not to the workflow) - a clean CI runner died with "No module named 'requests'". The install list is now completed and a test derives it from requirements.txt. Gate: pytest -m "unit or mock_integration" -> 109 passed, 1 skipped, 4 xfailed; plan's local gate (+ headless) -> 109 passed, 3 skipped, 4 xfailed; tools/scan_compat.py -> finding_count 0 (exit 0); ruff check . -> All checks passed. A mutation check re-introduces six defects into a copy of the repository: the matching guard fails in 6/6 cases.
This commit is contained in:
+8
-143
@@ -17,6 +17,10 @@ from comfy.utils import ProgressBar, tiled_scale
|
||||
# the package calls it; P4.7 removes it), so it now fails loudly instead of
|
||||
# silently importing a stub. The image / image-sequence paths are unaffected.
|
||||
#
|
||||
# P2 review fix: the unreachable body of ``process_video`` (it still called
|
||||
# the deleted ``VideoReader`` / ``VideoWriter``) is gone - the method is now
|
||||
# the raise and nothing else.
|
||||
#
|
||||
# P2.7: the dummy image helpers are gone - a broken vendored dependency must
|
||||
# fail loudly instead of returning ``None`` images.
|
||||
from .deps.basicsr.utils.img_util import img2tensor, tensor2img
|
||||
@@ -161,8 +165,10 @@ class KEEPFaceProcessor:
|
||||
|
||||
The vendored ``basicsr.utils.video_util`` module executed a pip
|
||||
install at import time; it was deleted in P2.1 and this dead code
|
||||
path was disabled with it. Use ``KEEP_ProcessImageSequence`` for
|
||||
sequences (P4.7 removes this method entirely).
|
||||
path was disabled with it. The unreachable body that still used the
|
||||
deleted ``VideoReader`` / ``VideoWriter`` was removed in the P2
|
||||
review fix. Use ``KEEP_ProcessImageSequence`` for sequences (P4.7
|
||||
removes this method entirely).
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"process_video is not supported any more: the vendored "
|
||||
@@ -170,147 +176,6 @@ class KEEPFaceProcessor:
|
||||
"KEEP_ProcessImageSequence node for image sequences."
|
||||
)
|
||||
|
||||
vidreader = VideoReader(video_path)
|
||||
original_fps = vidreader.get_fps()
|
||||
num_input_frames = len(vidreader)
|
||||
|
||||
pbar_comfy = ProgressBar(num_input_frames * 5)
|
||||
|
||||
input_frames_bgr = []
|
||||
for _ in tqdm(range(num_input_frames), desc="Reading video frames"):
|
||||
img = vidreader.get_frame()
|
||||
if img is None: break
|
||||
input_frames_bgr.append(img)
|
||||
pbar_comfy.update(1)
|
||||
vidreader.close()
|
||||
|
||||
if not input_frames_bgr: return None
|
||||
|
||||
avg_landmarks_sequence = None
|
||||
if not has_aligned:
|
||||
raw_landmarks_list_video = []
|
||||
for i in tqdm(range(num_input_frames), desc="Detecting face landmarks"):
|
||||
img = input_frames_bgr[i]
|
||||
self.face_helper.clean_all()
|
||||
self.face_helper.read_image(img)
|
||||
num_det_faces = self.face_helper.get_face_landmarks_5(
|
||||
only_center_face=only_center_face, resize=640, eye_dist_threshold=5,
|
||||
only_keep_largest=True)
|
||||
if num_det_faces >= 1:
|
||||
raw_landmarks_list_video.append(self.face_helper.all_landmarks_5[0].reshape((10,)))
|
||||
else:
|
||||
raw_landmarks_list_video.append(np.array([np.nan] * 10))
|
||||
pbar_comfy.update(1)
|
||||
raw_landmarks_np_video = np.array(raw_landmarks_list_video)
|
||||
if np.all(np.isnan(raw_landmarks_np_video)): return None
|
||||
for i_lm_coord in range(raw_landmarks_np_video.shape[1]):
|
||||
raw_landmarks_np_video[:, i_lm_coord] = interpolate_sequence(raw_landmarks_np_video[:, i_lm_coord])
|
||||
avg_landmarks_sequence = gaussian_filter1d(raw_landmarks_np_video, sigma=5, axis=0).reshape(num_input_frames, 5, 2)
|
||||
else:
|
||||
pbar_comfy.update(num_input_frames)
|
||||
|
||||
all_cropped_face_tensors = []
|
||||
for i in tqdm(range(num_input_frames), desc="Cropping and aligning faces"):
|
||||
img_bgr = input_frames_bgr[i]
|
||||
self.face_helper.clean_all()
|
||||
self.face_helper.read_image(img_bgr)
|
||||
if not has_aligned:
|
||||
if avg_landmarks_sequence is None or i >= len(avg_landmarks_sequence) or np.isnan(avg_landmarks_sequence[i]).any():
|
||||
all_cropped_face_tensors.append(torch.zeros(3, 512, 512, dtype=torch.float32, device=self.device) * 0.5)
|
||||
continue
|
||||
self.face_helper.all_landmarks_5 = [avg_landmarks_sequence[i]]
|
||||
self.face_helper.align_warp_face()
|
||||
else:
|
||||
img_resized = cv2.resize(img_bgr, (512, 512), interpolation=cv2.INTER_LINEAR)
|
||||
self.face_helper.is_gray = is_gray(img_resized, threshold=10)
|
||||
self.face_helper.cropped_faces = [img_resized]
|
||||
if not self.face_helper.cropped_faces:
|
||||
all_cropped_face_tensors.append(torch.zeros(3, 512, 512, dtype=torch.float32, device=self.device) * 0.5)
|
||||
continue
|
||||
cropped_face_cv2 = self.face_helper.cropped_faces[0]
|
||||
cropped_face_t = img2tensor(cropped_face_cv2 / 255., bgr2rgb=True, float32=True)
|
||||
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
||||
all_cropped_face_tensors.append(cropped_face_t)
|
||||
pbar_comfy.update(1)
|
||||
|
||||
if not all_cropped_face_tensors: return None
|
||||
|
||||
batched_cropped_faces = torch.stack(all_cropped_face_tensors, dim=0).unsqueeze(0).to(self.device)
|
||||
temp_restored_face_tensors = []
|
||||
for start_idx in tqdm(range(0, num_input_frames, max_clip_length), desc="Restoring faces with KEEP"):
|
||||
end_idx = min(start_idx + max_clip_length, num_input_frames)
|
||||
current_clip_faces = batched_cropped_faces[:, start_idx:end_idx, ...]
|
||||
if current_clip_faces.shape[1] == 1:
|
||||
current_clip_faces_for_net = torch.cat([current_clip_faces, current_clip_faces], dim=1)
|
||||
clip_output_tensor = self.keep_net(current_clip_faces_for_net, need_upscale=False)
|
||||
temp_restored_face_tensors.append(clip_output_tensor[:, 0:1, ...])
|
||||
elif current_clip_faces.shape[1] > 1 :
|
||||
clip_output_tensor = self.keep_net(current_clip_faces, need_upscale=False)
|
||||
temp_restored_face_tensors.append(clip_output_tensor)
|
||||
pbar_comfy.update(end_idx - start_idx)
|
||||
|
||||
if not temp_restored_face_tensors: return None
|
||||
all_restored_face_tensors = torch.cat(temp_restored_face_tensors, dim=1).squeeze(0)
|
||||
all_restored_faces_cv2 = [tensor2img(t, rgb2bgr=True, min_max=(-1, 1)) for t in all_restored_face_tensors]
|
||||
del batched_cropped_faces, all_restored_face_tensors, temp_restored_face_tensors
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
final_fps = target_fps if target_fps is not None else original_fps
|
||||
h_orig, w_orig, _ = input_frames_bgr[0].shape
|
||||
output_video_height, output_video_width = int(h_orig * final_upscale_factor), int(w_orig * final_upscale_factor)
|
||||
|
||||
vidwriter = VideoWriter(output_video_path, output_video_height, output_video_width, final_fps)
|
||||
|
||||
for i in tqdm(range(num_input_frames), desc="Pasting faces and writing video"):
|
||||
original_frame_bgr = input_frames_bgr[i]
|
||||
if i >= len(all_restored_faces_cv2):
|
||||
final_frame_to_write = self._run_upscaler(self.bg_upscale_model, original_frame_bgr)
|
||||
if final_frame_to_write.shape[0] != output_video_height or final_frame_to_write.shape[1] != output_video_width:
|
||||
final_frame_to_write = cv2.resize(final_frame_to_write, (output_video_width, output_video_height))
|
||||
vidwriter.write_frame(final_frame_to_write)
|
||||
continue
|
||||
|
||||
restored_face_patch_cv2 = all_restored_faces_cv2[i]
|
||||
self.face_helper.clean_all()
|
||||
self.face_helper.read_image(original_frame_bgr)
|
||||
self.face_helper.add_restored_face(restored_face_patch_cv2.astype('uint8'))
|
||||
|
||||
if not has_aligned:
|
||||
if avg_landmarks_sequence is None or i >= len(avg_landmarks_sequence) or np.isnan(avg_landmarks_sequence[i]).any():
|
||||
final_frame_to_write = self._run_upscaler(self.bg_upscale_model, original_frame_bgr)
|
||||
if final_frame_to_write.shape[0] != output_video_height or final_frame_to_write.shape[1] != output_video_width:
|
||||
final_frame_to_write = cv2.resize(final_frame_to_write, (output_video_width, output_video_height))
|
||||
vidwriter.write_frame(final_frame_to_write)
|
||||
continue
|
||||
|
||||
self.face_helper.upscale_factor = final_upscale_factor
|
||||
self.face_helper.all_landmarks_5 = [avg_landmarks_sequence[i]]
|
||||
self.face_helper.align_warp_face()
|
||||
|
||||
bg_img_upscaled_by_model = self._run_upscaler(self.bg_upscale_model, original_frame_bgr)
|
||||
if bg_img_upscaled_by_model.shape[0] != output_video_height or bg_img_upscaled_by_model.shape[1] != output_video_width:
|
||||
bg_img_final = cv2.resize(bg_img_upscaled_by_model, (output_video_width, output_video_height), interpolation=cv2.INTER_LANCZOS4)
|
||||
else:
|
||||
bg_img_final = bg_img_upscaled_by_model
|
||||
|
||||
self.face_helper.get_inverse_affine(None)
|
||||
pasted_frame = self.face_helper.paste_faces_to_input_image(
|
||||
upsample_img=bg_img_final, draw_box=draw_box, face_upsampler=self.face_upscale_model)
|
||||
else:
|
||||
pasted_frame = self.face_helper.restored_faces[0]
|
||||
if self.face_upscale_model:
|
||||
pasted_frame = self._run_upscaler(self.face_upscale_model, pasted_frame)
|
||||
if len(pasted_frame.shape) == 2: pasted_frame = cv2.cvtColor(pasted_frame, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
if pasted_frame.shape[0] != output_video_height or pasted_frame.shape[1] != output_video_width:
|
||||
pasted_frame = cv2.resize(pasted_frame, (output_video_width, output_video_height), interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
vidwriter.write_frame(pasted_frame)
|
||||
pbar_comfy.update(1)
|
||||
|
||||
vidwriter.close()
|
||||
return output_video_path
|
||||
|
||||
@torch.no_grad()
|
||||
def process_image_sequence(self,
|
||||
image_sequence_tensor: torch.Tensor,
|
||||
|
||||
Reference in New Issue
Block a user