Compare commits

...

7 Commits

Author SHA1 Message Date
Dopan
3b69413d61
Merge pull request #1845 from maxwbuckley/ruff-code-health
Some checks failed
ruff / ruff (push) Has been cancelled
Add ruff CI gate and fix deterministic lint issues
2026-06-01 00:50:59 +08:00
Kenneth Estanislao
07e2e960c8
Update Quick Start version from v2.7 RC1 to v2.7 RC2 2026-05-24 18:55:35 +08:00
Max Buckley
ba27b75265 Use astral-sh/ruff-action for inline PR annotations
Swap the manual pip install + ruff check steps for astral-sh/ruff-action@v4.0.0.
Same pinned ruff 0.15.7, but with --output-format=github so violations appear
as inline annotations on the PR diff instead of a flat log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:50:44 +02:00
Max Buckley
cfa8123b67 Add ruff CI gate and fix deterministic lint issues
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) <noreply@anthropic.com>
2026-05-23 15:44:31 +02:00
Dopan
08b2dd2526
Merge pull request #1844 from hklcf/fix/bugfix-batch
lgtm
2026-05-23 16:54:41 +08:00
hklcf
886e64b320 Fix: resolve 5 confirmed bugs (imwrite_unicode, macOS memory, face_analyser None crash, silent sys.exit, core memory calc) 2026-05-23 10:37:20 +08:00
Kenneth Estanislao
aa6f2cbade
Update version from v2.7 beta to v2.7 RC1 in README 2026-05-21 05:11:41 +08:00
18 changed files with 74 additions and 38 deletions

16
.github/workflows/ruff.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: ruff
on:
pull_request:
push:
branches: [main]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v4.0.0
with:
version: "0.15.7"
args: "check --output-format=github"

View File

@ -30,7 +30,7 @@ By using this software, you agree to these terms and commit to using it in a man
Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions.
## Exclusive v2.7 beta Quick Start - Pre-built (Windows/Mac Silicon/CPU)
## Exclusive v2.7 RC2 Quick Start - Pre-built (Windows/Mac Silicon/CPU)
<a href="https://deeplivecam.net/index.php/quickstart"> <img src="media/Download.png" width="285" height="77" />

View File

@ -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()

View File

@ -11,8 +11,8 @@ def imwrite_unicode(path, img, params=None):
root, ext = os.path.splitext(path)
if not ext:
ext = ".png"
result, encoded_img = cv2.imencode(ext, img, params if params else [])
result, encoded_img = cv2.imencode(f".{ext}", img, params if params is not None else [])
encoded_img.tofile(path)
return True
return False
result, encoded_img = cv2.imencode(ext, img, params if params is not None else [])
if not result:
return False
encoded_img.tofile(path)
return True

View File

@ -1,6 +1,5 @@
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from typing import Any

View File

@ -171,8 +171,6 @@ def limit_resources() -> None:
# limit memory usage
if modules.globals.max_memory:
memory = modules.globals.max_memory * 1024 ** 3
if platform.system().lower() == 'darwin':
memory = modules.globals.max_memory * 1024 ** 6
if platform.system().lower() == 'windows':
import ctypes
kernel32 = ctypes.windll.kernel32
@ -324,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:

View File

@ -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
@ -257,6 +256,8 @@ def get_unique_faces_from_target_image() -> Any:
modules.globals.source_target_map = []
target_frame = cv2.imread(modules.globals.target_path)
many_faces = get_many_faces(target_frame)
if many_faces is None:
return None
i = 0
for face in many_faces:
@ -291,6 +292,8 @@ def get_unique_faces_from_target_video() -> Any:
for temp_frame_path in tqdm(temp_frame_paths, desc="Extracting face embeddings from frames"):
temp_frame = cv2.imread(temp_frame_path)
many_faces = get_many_faces(temp_frame)
if many_faces is None:
continue
for face in many_faces:
face_embeddings.append(face.normed_embedding)

View File

@ -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)

View File

@ -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])

View File

@ -37,6 +37,7 @@ def load_frame_processor_module(frame_processor: str) -> Any:
frame_processor_module = importlib.import_module(f'modules.processors.frame.{frame_processor}')
for method_name in FRAME_PROCESSORS_INTERFACE:
if not hasattr(frame_processor_module, method_name):
print(f"Frame processor {frame_processor} is missing required method {method_name}")
sys.exit()
except ImportError:
print(f"Frame processor {frame_processor} not found")
@ -59,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)
@ -70,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:

View File

@ -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,

View File

@ -5,7 +5,6 @@ import os
import threading
import cv2
import numpy as np
import modules.globals
import modules.processors.frame.core

View File

@ -5,7 +5,6 @@ import os
import threading
import cv2
import numpy as np
import modules.globals
import modules.processors.frame.core

View File

@ -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):
"""

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,5 @@
import cv2
import numpy as np
import sys
import time
from typing import Optional, Tuple, Callable
import platform

9
pyproject.toml Normal file
View File

@ -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"]