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:
WildAi
2026-10-23 18:29:04 +03:00
parent b98a6179f4
commit 4c44a8e275
8 changed files with 418 additions and 242 deletions
+8 -143
View File
@@ -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,