Skip to content

Commit c754e76

Browse files
laurigatesclaude
andcommitted
refactor(face-swapper): extract pair-building helpers, deduplicate constants
- Extract _build_pairs_from_file_map and _build_pairs_live from process_frame_v2, reducing nesting from 7 levels to 1 - Remove local DETECTION_INTERVAL; use modules.globals.DETECTION_INTERVAL - Both helpers snapshot maps under MAP_LOCK before iterating Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 71cb0f7 commit c754e76

1 file changed

Lines changed: 90 additions & 93 deletions

File tree

modules/processors/frame/face_swapper.py

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
FRAME_CACHE = deque(maxlen=3) # Cache for frame reuse
3434
FACE_DETECTION_CACHE = {} # Cache face detections
3535
LAST_DETECTION_TIME = 0
36-
DETECTION_INTERVAL = 0.033 # ~30 FPS detection rate for live mode
3736
FRAME_SKIP_COUNTER = 0
3837
ADAPTIVE_QUALITY = True
3938
# --- END: Mac M1-M5 Optimizations ---
@@ -273,7 +272,7 @@ def get_faces_optimized(frame: Frame, use_cache: bool = True) -> Optional[List[F
273272
time_since_last = current_time - LAST_DETECTION_TIME
274273

275274
# Skip detection if too soon (adaptive frame skipping)
276-
if time_since_last < DETECTION_INTERVAL and FACE_DETECTION_CACHE:
275+
if time_since_last < modules.globals.DETECTION_INTERVAL and FACE_DETECTION_CACHE:
277276
return FACE_DETECTION_CACHE.get('faces')
278277

279278
# Perform detection
@@ -412,6 +411,90 @@ def process_frame(source_face: Face, temp_frame: Frame) -> Frame:
412411
return final_frame
413412

414413

414+
def _build_pairs_from_file_map(temp_frame_path: str) -> list:
415+
"""Build (source_face, target_face) pairs from source_target_map for image/video files."""
416+
pairs = []
417+
with modules.globals.MAP_LOCK:
418+
source_target_map = list(getattr(modules.globals, "source_target_map", []))
419+
if not source_target_map:
420+
return pairs
421+
422+
if modules.globals.many_faces:
423+
source_face = default_source_face()
424+
if not source_face:
425+
return pairs
426+
for map_data in source_target_map:
427+
if is_image(modules.globals.target_path):
428+
target_face = map_data.get("target", {}).get("face")
429+
if target_face:
430+
pairs.append((source_face, target_face))
431+
elif is_video(modules.globals.target_path):
432+
for frame_data in map_data.get("target_faces_in_frame", []):
433+
if frame_data and frame_data.get("location") == temp_frame_path:
434+
for target_face in frame_data.get("faces", []):
435+
pairs.append((source_face, target_face))
436+
else:
437+
for map_data in source_target_map:
438+
source_face = map_data.get("source", {}).get("face")
439+
if not source_face:
440+
continue
441+
if is_image(modules.globals.target_path):
442+
target_face = map_data.get("target", {}).get("face")
443+
if target_face:
444+
pairs.append((source_face, target_face))
445+
elif is_video(modules.globals.target_path):
446+
for frame_data in map_data.get("target_faces_in_frame", []):
447+
if frame_data and frame_data.get("location") == temp_frame_path:
448+
for target_face in frame_data.get("faces", []):
449+
pairs.append((source_face, target_face))
450+
return pairs
451+
452+
453+
def _build_pairs_live(processed_frame: Frame) -> list:
454+
"""Build (source_face, target_face) pairs for live/webcam mode."""
455+
pairs = []
456+
detected_faces = get_many_faces(processed_frame)
457+
if not detected_faces:
458+
return pairs
459+
460+
with modules.globals.MAP_LOCK:
461+
simple_map = dict(getattr(modules.globals, "simple_map", None) or {})
462+
463+
if modules.globals.many_faces:
464+
source_face = default_source_face()
465+
if source_face:
466+
for target_face in detected_faces:
467+
pairs.append((source_face, target_face))
468+
elif simple_map:
469+
source_faces = simple_map.get("source_faces", [])
470+
target_embeddings = simple_map.get("target_embeddings", [])
471+
if source_faces and target_embeddings and len(source_faces) == len(target_embeddings):
472+
if len(detected_faces) <= len(target_embeddings):
473+
for detected_face in detected_faces:
474+
if detected_face.normed_embedding is None:
475+
continue
476+
closest_idx, _ = find_closest_centroid(target_embeddings, detected_face.normed_embedding)
477+
if 0 <= closest_idx < len(source_faces):
478+
pairs.append((source_faces[closest_idx], detected_face))
479+
else:
480+
detected_embeddings = [f.normed_embedding for f in detected_faces if f.normed_embedding is not None]
481+
detected_faces_with_embedding = [f for f in detected_faces if f.normed_embedding is not None]
482+
if not detected_embeddings:
483+
return pairs
484+
for i, target_embedding in enumerate(target_embeddings):
485+
if 0 <= i < len(source_faces):
486+
closest_idx, _ = find_closest_centroid(detected_embeddings, target_embedding)
487+
if 0 <= closest_idx < len(detected_faces_with_embedding):
488+
pairs.append((source_faces[i], detected_faces_with_embedding[closest_idx]))
489+
else:
490+
source_face = default_source_face()
491+
# Reuse already-detected faces instead of running detection again
492+
target_face = min(detected_faces, key=lambda x: x.bbox[0])
493+
if source_face and target_face:
494+
pairs.append((source_face, target_face))
495+
return pairs
496+
497+
415498
def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame:
416499
"""Handles complex mapping scenarios (map_faces=True) and live streams."""
417500
if getattr(modules.globals, "opacity", 1.0) == 0:
@@ -425,100 +508,14 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame:
425508
swapped_face_bboxes = [] # Keep track of where swaps happened
426509

427510
# Determine source/target pairs based on mode
428-
source_target_pairs = []
429-
430-
# Ensure maps exist before accessing them
431-
source_target_map = getattr(modules.globals, "source_target_map", None)
432-
simple_map = getattr(modules.globals, "simple_map", None)
433-
434-
# Check if target is a file path (image or video) or live stream
435-
is_file_target = modules.globals.target_path and (is_image(modules.globals.target_path) or is_video(modules.globals.target_path))
511+
is_file_target = modules.globals.target_path and (
512+
is_image(modules.globals.target_path) or is_video(modules.globals.target_path)
513+
)
436514

437515
if is_file_target:
438-
# Processing specific image or video file with pre-analyzed maps
439-
if source_target_map:
440-
if modules.globals.many_faces:
441-
source_face = default_source_face() # Use default source for all targets
442-
if source_face:
443-
for map_data in source_target_map:
444-
if is_image(modules.globals.target_path):
445-
target_info = map_data.get("target", {})
446-
if target_info: # Check if target info exists
447-
target_face = target_info.get("face")
448-
if target_face:
449-
source_target_pairs.append((source_face, target_face))
450-
elif is_video(modules.globals.target_path):
451-
# Find faces for the current frame_path in video map
452-
target_frames_data = map_data.get("target_faces_in_frame", [])
453-
if target_frames_data: # Check if frame data exists
454-
target_frames = [f for f in target_frames_data if f and f.get("location") == temp_frame_path]
455-
for frame_data in target_frames:
456-
faces_in_frame = frame_data.get("faces", [])
457-
if faces_in_frame: # Check if faces exist
458-
for target_face in faces_in_frame:
459-
source_target_pairs.append((source_face, target_face))
460-
else: # Single face or specific mapping
461-
for map_data in source_target_map:
462-
source_info = map_data.get("source", {})
463-
if not source_info: continue # Skip if no source info
464-
source_face = source_info.get("face")
465-
if not source_face: continue # Skip if no source defined for this map entry
466-
467-
if is_image(modules.globals.target_path):
468-
target_info = map_data.get("target", {})
469-
if target_info:
470-
target_face = target_info.get("face")
471-
if target_face:
472-
source_target_pairs.append((source_face, target_face))
473-
elif is_video(modules.globals.target_path):
474-
target_frames_data = map_data.get("target_faces_in_frame", [])
475-
if target_frames_data:
476-
target_frames = [f for f in target_frames_data if f and f.get("location") == temp_frame_path]
477-
for frame_data in target_frames:
478-
faces_in_frame = frame_data.get("faces", [])
479-
if faces_in_frame:
480-
for target_face in faces_in_frame:
481-
source_target_pairs.append((source_face, target_face))
482-
516+
source_target_pairs = _build_pairs_from_file_map(temp_frame_path)
483517
else:
484-
# Live stream or webcam processing (analyze faces on the fly)
485-
detected_faces = get_many_faces(processed_frame)
486-
if detected_faces:
487-
if modules.globals.many_faces:
488-
source_face = default_source_face() # Use default source for all detected targets
489-
if source_face:
490-
for target_face in detected_faces:
491-
source_target_pairs.append((source_face, target_face))
492-
elif simple_map:
493-
# Use simple_map (source_faces <-> target_embeddings)
494-
source_faces = simple_map.get("source_faces", [])
495-
target_embeddings = simple_map.get("target_embeddings", [])
496-
497-
if source_faces and target_embeddings and len(source_faces) == len(target_embeddings):
498-
# Match detected faces to the closest target embedding
499-
if len(detected_faces) <= len(target_embeddings):
500-
# More targets defined than detected - match each detected face
501-
for detected_face in detected_faces:
502-
if detected_face.normed_embedding is None: continue
503-
closest_idx, _ = find_closest_centroid(target_embeddings, detected_face.normed_embedding)
504-
if 0 <= closest_idx < len(source_faces):
505-
source_target_pairs.append((source_faces[closest_idx], detected_face))
506-
else:
507-
# More faces detected than targets defined - match each target embedding to closest detected face
508-
detected_embeddings = [f.normed_embedding for f in detected_faces if f.normed_embedding is not None]
509-
detected_faces_with_embedding = [f for f in detected_faces if f.normed_embedding is not None]
510-
if not detected_embeddings: return processed_frame # No embeddings to match
511-
512-
for i, target_embedding in enumerate(target_embeddings):
513-
if 0 <= i < len(source_faces): # Ensure source face exists for this embedding
514-
closest_idx, _ = find_closest_centroid(detected_embeddings, target_embedding)
515-
if 0 <= closest_idx < len(detected_faces_with_embedding):
516-
source_target_pairs.append((source_faces[i], detected_faces_with_embedding[closest_idx]))
517-
else: # Fallback: if no map, use default source for the single detected face (if any)
518-
source_face = default_source_face()
519-
target_face = get_one_face(processed_frame, detected_faces) # Use faces already detected
520-
if source_face and target_face:
521-
source_target_pairs.append((source_face, target_face))
518+
source_target_pairs = _build_pairs_live(processed_frame)
522519

523520

524521
# Perform swaps based on the collected pairs

0 commit comments

Comments
 (0)