From cfa8123b67c1165e9f7bb753aacab3cfc2b8691f Mon Sep 17 00:00:00 2001 From: Max Buckley Date: Sat, 23 May 2026 15:44:31 +0200 Subject: [PATCH] Add ruff CI gate and fix deterministic lint issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces pyproject.toml + .github/workflows/ruff.yml that gate E701, E711, E712, F401, F541 on every PR and push to main. Fixes the existing findings for those rules: - Remove unused imports (sklearn.silhouette_score, numpy in several files, typing.Optional, get_one_face, gpu_cvt_color, sys, insightface.face_align) - Annotate the intentional tkinter_fix side-effect import with `# noqa: F401` - Split multi-statement `if x: y` one-liners onto separate lines - Replace `state == True` / `state == False` with truthiness checks - Drop `f` prefix from f-strings with no placeholders F841 (unused-variable), E402 (module-level-import-not-at-top), and F821 (undefined-name) are left out of the gate for now — they surface real findings (including a latent NameError in face_swapper.py) that require human review to fix safely. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ruff.yml | 17 ++++++++++ benchmark_pipeline.py | 13 +++++--- modules/cluster_analysis.py | 1 - modules/core.py | 3 +- modules/face_analyser.py | 1 - modules/gpu_processing.py | 2 +- modules/onnx_optimize.py | 2 +- modules/processors/frame/core.py | 4 +-- modules/processors/frame/face_enhancer.py | 2 +- .../processors/frame/face_enhancer_gpen256.py | 1 - .../processors/frame/face_enhancer_gpen512.py | 1 - modules/processors/frame/face_masking.py | 2 +- modules/processors/frame/face_swapper.py | 31 ++++++++++++------- modules/run.py | 4 +-- modules/video_capture.py | 1 - pyproject.toml | 9 ++++++ 16 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/ruff.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..b9250e0 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,17 @@ +name: ruff + +on: + pull_request: + push: + branches: [main] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: pip install ruff==0.15.7 + - run: ruff check . diff --git a/benchmark_pipeline.py b/benchmark_pipeline.py index b8f6235..6a1e076 100644 --- a/benchmark_pipeline.py +++ b/benchmark_pipeline.py @@ -14,7 +14,6 @@ if sys.platform == "win32": import insightface from insightface.app import FaceAnalysis -from insightface.utils import face_align from modules.processors.frame.face_swapper import _fast_paste_back from modules import platform_info @@ -81,10 +80,14 @@ def capture_thread(): try: capture_queue.put_nowait(frame) except queue.Full: - try: capture_queue.get_nowait() - except queue.Empty: pass - try: capture_queue.put_nowait(frame) - except queue.Full: pass + try: + capture_queue.get_nowait() + except queue.Empty: + pass + try: + capture_queue.put_nowait(frame) + except queue.Full: + pass cap_t = threading.Thread(target=capture_thread, daemon=True) cap_t.start() diff --git a/modules/cluster_analysis.py b/modules/cluster_analysis.py index 0e7db03..ea1c941 100644 --- a/modules/cluster_analysis.py +++ b/modules/cluster_analysis.py @@ -1,6 +1,5 @@ import numpy as np from sklearn.cluster import KMeans -from sklearn.metrics import silhouette_score from typing import Any diff --git a/modules/core.py b/modules/core.py index 6e50252..dfd1081 100644 --- a/modules/core.py +++ b/modules/core.py @@ -322,7 +322,8 @@ def start() -> None: def destroy(to_quit=True) -> None: if modules.globals.target_path: clean_temp(modules.globals.target_path) - if to_quit: quit() + if to_quit: + quit() def run() -> None: diff --git a/modules/face_analyser.py b/modules/face_analyser.py index ce51017..ccc438a 100644 --- a/modules/face_analyser.py +++ b/modules/face_analyser.py @@ -5,7 +5,6 @@ import insightface import threading import cv2 -import numpy as np import modules.globals from tqdm import tqdm from modules.typing import Frame diff --git a/modules/gpu_processing.py b/modules/gpu_processing.py index e398dd2..689843e 100644 --- a/modules/gpu_processing.py +++ b/modules/gpu_processing.py @@ -21,7 +21,7 @@ from __future__ import annotations import os import cv2 import numpy as np -from typing import Tuple, Optional +from typing import Tuple # --------------------------------------------------------------------------- # CUDA availability detection (evaluated once at import time) diff --git a/modules/onnx_optimize.py b/modules/onnx_optimize.py index a5ba0b0..6a29995 100644 --- a/modules/onnx_optimize.py +++ b/modules/onnx_optimize.py @@ -392,7 +392,7 @@ def _decompose_split(model) -> bool: # Collect all needed boundary constants for _, (a, b) in splits: - ensure_const(f"_sp_s0", [0]) + ensure_const("_sp_s0", [0]) ensure_const(f"_sp_s{a}", [a]) ensure_const(f"_sp_s{a + b}", [a + b]) diff --git a/modules/processors/frame/core.py b/modules/processors/frame/core.py index 77f2fe9..64330c6 100644 --- a/modules/processors/frame/core.py +++ b/modules/processors/frame/core.py @@ -60,7 +60,7 @@ def set_frame_processors_modules_from_ui(frame_processors: List[str]) -> None: current_processor_names = [proc.__name__.split('.')[-1] for proc in FRAME_PROCESSORS_MODULES] for frame_processor, state in modules.globals.fp_ui.items(): - if state == True and frame_processor not in current_processor_names: + if state and frame_processor not in current_processor_names: try: frame_processor_module = load_frame_processor_module(frame_processor) FRAME_PROCESSORS_MODULES.append(frame_processor_module) @@ -71,7 +71,7 @@ def set_frame_processors_modules_from_ui(frame_processors: List[str]) -> None: except Exception as e: print(f"Warning: Error loading frame processor {frame_processor} requested by UI state: {e}") - elif state == False and frame_processor in current_processor_names: + elif not state and frame_processor in current_processor_names: try: module_to_remove = next((mod for mod in FRAME_PROCESSORS_MODULES if mod.__name__.endswith(f'.{frame_processor}')), None) if module_to_remove: diff --git a/modules/processors/frame/face_enhancer.py b/modules/processors/frame/face_enhancer.py index b665e08..12e65aa 100644 --- a/modules/processors/frame/face_enhancer.py +++ b/modules/processors/frame/face_enhancer.py @@ -11,7 +11,7 @@ import onnxruntime import modules.globals import modules.processors.frame.core from modules.core import update_status -from modules.face_analyser import get_one_face, get_many_faces +from modules.face_analyser import get_many_faces from modules.typing import Frame, Face from modules.utilities import ( is_image, diff --git a/modules/processors/frame/face_enhancer_gpen256.py b/modules/processors/frame/face_enhancer_gpen256.py index 78d87ac..9814ebd 100644 --- a/modules/processors/frame/face_enhancer_gpen256.py +++ b/modules/processors/frame/face_enhancer_gpen256.py @@ -5,7 +5,6 @@ import os import threading import cv2 -import numpy as np import modules.globals import modules.processors.frame.core diff --git a/modules/processors/frame/face_enhancer_gpen512.py b/modules/processors/frame/face_enhancer_gpen512.py index 9a34e1d..b6b792a 100644 --- a/modules/processors/frame/face_enhancer_gpen512.py +++ b/modules/processors/frame/face_enhancer_gpen512.py @@ -5,7 +5,6 @@ import os import threading import cv2 -import numpy as np import modules.globals import modules.processors.frame.core diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py index 5265921..6e5811d 100644 --- a/modules/processors/frame/face_masking.py +++ b/modules/processors/frame/face_masking.py @@ -2,7 +2,7 @@ import cv2 import numpy as np from modules.typing import Face, Frame import modules.globals -from modules.gpu_processing import gpu_gaussian_blur, gpu_resize, gpu_cvt_color +from modules.gpu_processing import gpu_gaussian_blur, gpu_resize def apply_color_transfer(source, target): """ diff --git a/modules/processors/frame/face_swapper.py b/modules/processors/frame/face_swapper.py index 74997f4..109c359 100644 --- a/modules/processors/frame/face_swapper.py +++ b/modules/processors/frame/face_swapper.py @@ -16,7 +16,7 @@ from modules.utilities import ( is_video, ) from modules.cluster_analysis import find_closest_centroid -from modules.gpu_processing import gpu_gaussian_blur, gpu_sharpen, gpu_add_weighted, gpu_resize, gpu_cvt_color +from modules.gpu_processing import gpu_gaussian_blur, gpu_sharpen, gpu_add_weighted, gpu_resize import os from collections import deque import time @@ -680,7 +680,8 @@ def apply_post_processing(current_frame: Frame, swapped_face_bboxes: List[np.nda continue face_region = processed_frame[y1:y2, x1:x2] - if face_region.size == 0: continue + if face_region.size == 0: + continue # Apply sharpening (GPU-accelerated when CUDA OpenCV is available) try: @@ -815,9 +816,11 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: else: # Single face or specific mapping for map_data in source_target_map: source_info = map_data.get("source", {}) - if not source_info: continue # Skip if no source info + if not source_info: + continue # Skip if no source info source_face = source_info.get("face") - if not source_face: continue # Skip if no source defined for this map entry + if not source_face: + continue # Skip if no source defined for this map entry if is_image(modules.globals.target_path): target_info = map_data.get("target", {}) @@ -854,7 +857,8 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: if len(detected_faces) <= len(target_embeddings): # More targets defined than detected - match each detected face for detected_face in detected_faces: - if detected_face.normed_embedding is None: continue + if detected_face.normed_embedding is None: + continue closest_idx, _ = find_closest_centroid(target_embeddings, detected_face.normed_embedding) if 0 <= closest_idx < len(source_faces): source_target_pairs.append((source_faces[closest_idx], detected_face)) @@ -862,7 +866,8 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: # More faces detected than targets defined - match each target embedding to closest detected face detected_embeddings = [f.normed_embedding for f in detected_faces if f.normed_embedding is not None] detected_faces_with_embedding = [f for f in detected_faces if f.normed_embedding is not None] - if not detected_embeddings: return processed_frame # No embeddings to match + if not detected_embeddings: + return processed_frame # No embeddings to match for i, target_embedding in enumerate(target_embeddings): if 0 <= i < len(source_faces): # Ensure source face exists for this embedding @@ -936,7 +941,7 @@ def process_frames( # --- Stop processing entirely if in Simple Mode and source face is invalid --- if not use_v2 and source_face is None: - update_status(f"Halting video processing: Invalid or no face detected in source image for simple mode.", NAME) + update_status("Halting video processing: Invalid or no face detected in source image for simple mode.", NAME) if progress: # Ensure the progress bar completes if it was started remaining_updates = total_frames - progress.n if hasattr(progress, 'n') else total_frames @@ -955,11 +960,13 @@ def process_frames( temp_frame = cv2.imread(temp_frame_path) if temp_frame is None: print(f"{NAME}: Error: Could not read frame: {temp_frame_path}, skipping.") - if progress: progress.update(1) + if progress: + progress.update(1) continue # Skip this frame if read fails except Exception as read_e: print(f"{NAME}: Error reading frame {temp_frame_path}: {read_e}, skipping.") - if progress: progress.update(1) + if progress: + progress.update(1) continue # Select processing function and execute @@ -1496,7 +1503,8 @@ def apply_color_transfer(source, target): if len(source.shape) == 2: # Grayscale source = cv2.cvtColor(source, cv2.COLOR_GRAY2BGR) source = np.clip(source, 0, 255).astype(np.uint8) - if len(source.shape)!= 3 or source.shape[2]!= 3: raise ValueError("Conversion failed") + if len(source.shape) != 3 or source.shape[2] != 3: + raise ValueError("Conversion failed") except Exception: return source if len(target.shape) != 3 or target.shape[2] != 3 or target.dtype != np.uint8: @@ -1505,7 +1513,8 @@ def apply_color_transfer(source, target): if len(target.shape) == 2: # Grayscale target = cv2.cvtColor(target, cv2.COLOR_GRAY2BGR) target = np.clip(target, 0, 255).astype(np.uint8) - if len(target.shape)!= 3 or target.shape[2]!= 3: raise ValueError("Conversion failed") + if len(target.shape) != 3 or target.shape[2] != 3: + raise ValueError("Conversion failed") except Exception: return source # Return original source if target invalid diff --git a/modules/run.py b/modules/run.py index 1abdd11..2981e7e 100644 --- a/modules/run.py +++ b/modules/run.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -# Import the tkinter fix to patch the ScreenChanged error -import tkinter_fix +# Import the tkinter fix to patch the ScreenChanged error (module patches Tk on import) +import tkinter_fix # noqa: F401 import core diff --git a/modules/video_capture.py b/modules/video_capture.py index 65148ad..b374a1a 100644 --- a/modules/video_capture.py +++ b/modules/video_capture.py @@ -1,6 +1,5 @@ import cv2 import numpy as np -import sys import time from typing import Optional, Tuple, Callable import platform diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4dde0da --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +# Deterministic, low-risk rules enforced in CI. Other rules (F841, E402, F821) +# surface real findings but require human judgement to fix safely, so they are +# left out of the gate for now. Intentional side-effect imports should be +# annotated with `# noqa: F401`. +select = ["E701", "E711", "E712", "F401", "F541"]