From c706a69abfe9bae1322adbd344dfdaae37cdf564 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期三, 30 七月 2025 22:16:55 +0800
Subject: [PATCH] add face swaper

---
 auto_media_publisher/modifiers/face_swap/__init__.py                  |    0 
 auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py       |   28 +
 auto_media_publisher/modifiers/face_swap/face_enhancer.py             |   49 +++
 auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py          |  166 +++++++++++
 auto_media_publisher/modifiers/face_swap/typing.py                    |    7 
 auto_media_publisher/utils/media_input_output.py                      |   93 ++++++
 auto_media_publisher/modifiers/face_swap/face_analyser.py             |   53 +++
 auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py        |  238 +++++++++++++++
 auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py |  241 ++++++++++++++++
 9 files changed, 875 insertions(+), 0 deletions(-)

diff --git a/auto_media_publisher/modifiers/face_swap/__init__.py b/auto_media_publisher/modifiers/face_swap/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/__init__.py
diff --git a/auto_media_publisher/modifiers/face_swap/face_analyser.py b/auto_media_publisher/modifiers/face_swap/face_analyser.py
new file mode 100644
index 0000000..1c4538b
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_analyser.py
@@ -0,0 +1,53 @@
+
+import torch
+from typing import Optional, List
+import numpy
+from insightface.app import FaceAnalysis
+from insightface.app.common import Face
+
+from gfpgan.utils import GFPGANer
+from .typing import Frame # 浣犲凡鏈夊畾涔�
+
+SIMILAR_FACE_DISTANCE: float = 0.45     # < 0.4 鍚屼竴浜� 0.4 -0.6 澶ф鏄悓涓�浜� >0.6 涓嶆槸鍚屼竴涓�
+
+
+class FaceAnalyser:
+    def __init__(self):
+        self.device = self._select_device()
+        self._analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider' if self.device == 'cuda' else 'CPUExecutionProvider'])
+        self._analyser.prepare(ctx_id=0, det_size=(640, 640))
+
+    def _select_device(self):
+        if torch.cuda.is_available():
+            return 'cuda'
+        elif torch.backends.mps.is_available():
+            return 'mps'
+        else:
+            return 'cpu'
+        
+    def get_many_faces(self,frame: Frame) -> Optional[List[Face]]:
+        try:
+            return self._analyser.get(frame)
+        except Exception as e:
+            print(f"{e}")
+            return None
+
+    def get_one_face(self,frame: Frame, position: int = 0) -> Optional[Face]:
+        many_faces = self.get_many_faces(frame)
+        if many_faces:
+            try:
+                return many_faces[position]
+            except IndexError:
+                return many_faces[-1]
+        return None
+
+
+    def find_similar_face(self,frame: Frame, reference_face: Face) -> Optional[Face]:
+        many_faces = self.get_many_faces(frame)
+        if many_faces:
+            for face in many_faces:
+                if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'):
+                    distance = numpy.sum(numpy.square(face.normed_embedding - reference_face.normed_embedding))
+                    if distance < SIMILAR_FACE_DISTANCE:
+                        return face
+        return None
diff --git a/auto_media_publisher/modifiers/face_swap/face_enhancer.py b/auto_media_publisher/modifiers/face_swap/face_enhancer.py
new file mode 100644
index 0000000..1b600fe
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_enhancer.py
@@ -0,0 +1,49 @@
+
+import torch
+from typing import List
+from insightface.app.common import Face
+
+from gfpgan.utils import GFPGANer
+from .typing import Frame # 浣犲凡鏈夊畾涔�
+from auto_media_publisher.config.conf_base import FACE_ENHANCER_MODEL_PATH
+
+SIMILAR_FACE_DISTANCE: float = 0.45     # < 0.4 鍚屼竴浜� 0.4 -0.6 澶ф鏄悓涓�浜� >0.6 涓嶆槸鍚屼竴涓�
+
+
+class FaceEnhancer:
+    def __init__(self):
+        self.device = self._select_device()
+        self._enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=self.device)
+
+    def _select_device(self):
+        if torch.cuda.is_available():
+            return 'cuda'
+        elif torch.backends.mps.is_available():
+            return 'mps'
+        else:
+            return 'cpu'
+        
+    def _enhance_face(self,target_face: Face, temp_frame: Frame) -> Frame:
+        start_x, start_y, end_x, end_y = map(int, target_face['bbox'])
+        padding_x = int((end_x - start_x) * 0.5)
+        padding_y = int((end_y - start_y) * 0.5)
+        start_x = max(0, start_x - padding_x)
+        start_y = max(0, start_y - padding_y)
+        end_x = max(0, end_x + padding_x)
+        end_y = max(0, end_y + padding_y)
+        temp_face = temp_frame[start_y:end_y, start_x:end_x]
+        if temp_face.size:
+            _, _, temp_face = self._enhancer.enhance(
+                temp_face,
+                paste_back=True
+            )
+            temp_frame[start_y:end_y, start_x:end_x] = temp_face
+        return temp_frame
+
+
+    def process_frame(self,temp_frame: Frame,many_faces:List) -> Frame:
+        #many_faces = self._get_many_faces(temp_frame)  check face_analyser.py
+        if many_faces:
+            for target_face in many_faces:
+                temp_frame = self._enhance_face(target_face, temp_frame)
+        return temp_frame
diff --git a/auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py b/auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py
new file mode 100644
index 0000000..8cc5f96
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py
@@ -0,0 +1,28 @@
+import threading
+import numpy as np
+from PIL import Image
+from keras import Model
+import opennsfw2
+
+from .typing import Frame  # 浣犲凡鏈夊畾涔�
+
+MAX_PROBABILITY = 0.85
+
+class NSFWPredictor:
+    def __init__(self):
+        self._predictor = opennsfw2.make_open_nsfw_model()
+
+    def predict_frame(self, frame: Frame) -> bool:
+        image = Image.fromarray(frame)
+        image = opennsfw2.preprocess_image(image, opennsfw2.Preprocessing.YAHOO)
+        views = np.expand_dims(image, axis=0)
+        _, probability = self._predictor.predict(views)[0]
+        return probability > MAX_PROBABILITY
+
+    def predict_image(self, image_path: str) -> bool:
+        probability = opennsfw2.predict_image(image_path)
+        return probability > MAX_PROBABILITY
+
+    def predict_video(self, video_path: str, frame_interval: int = 100) -> bool:
+        _, probabilities = opennsfw2.predict_video_frames(video_path, frame_interval=frame_interval)
+        return any(prob > MAX_PROBABILITY for prob in probabilities)
diff --git a/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py b/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py
new file mode 100644
index 0000000..52b0f41
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py
@@ -0,0 +1,166 @@
+import av
+import torch
+import threading, queue
+from threading import current_thread
+import numpy as np
+import insightface
+import cv2
+import time
+from insightface.app.common import Face
+from gfpgan.utils import GFPGANer
+from .typing import Frame # 浣犲凡鏈夊畾涔�
+from auto_media_publisher.config.conf_base import FACE_ENHANCER_MODEL_PATH,FACE_SWAPPER_MODEL_PATH
+from insightface.app import FaceAnalysis
+
+SIMILAR_FACE_DISTANCE: float = 0.45     # < 0.4 鍚屼竴浜� 0.4 -0.6 澶ф鏄悓涓�浜� >0.6 涓嶆槸鍚屼竴涓�
+
+def _select_device():
+    if torch.cuda.is_available():
+        return 'cuda:0'
+    elif torch.backends.mps.is_available():
+        return 'mps'
+    else:
+        return 'cpu'
+    
+NUM_WORKERS = 1
+
+#face_swapper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider'])
+#face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+
+frame_q = queue.Queue(maxsize=10)
+processed_q = queue.Queue(maxsize=10)
+
+THREAD_SEMAPHORE =threading.Semaphore(4)  # 涓嶉檺鍒朵簡銆�
+
+def enhance_face(thread_face_enhancer,target_face, temp_frame:np.ndarray) -> np.ndarray:
+    start_x, start_y, end_x, end_y = map(int, target_face['bbox'])
+    padding_x = int((end_x - start_x) * 0.5)
+    padding_y = int((end_y - start_y) * 0.5)
+    start_x = max(0, start_x - padding_x)
+    start_y = max(0, start_y - padding_y)
+    end_x = max(0, end_x + padding_x)
+    end_y = max(0, end_y + padding_y)
+    temp_face = temp_frame[start_y:end_y, start_x:end_x]
+    #print(temp_face.size)
+    if temp_face.size:
+        _, _, temp_face = thread_face_enhancer.enhance(
+                temp_face,
+                paste_back=True
+        )
+        temp_frame[start_y:end_y, start_x:end_x] = temp_face
+    return temp_frame
+    
+
+def decode_video(video_path):
+    container = av.open(video_path)
+    for idx, frame in enumerate(container.decode(video=0)):
+        img = frame.to_ndarray(format='rgb24')
+        frame_q.put((idx, img))
+    for _ in range(NUM_WORKERS):
+        frame_q.put((None, None))
+
+def process_frame_loop(new_face):
+    start_time = time.time()
+    thread_face_analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider'])
+    thread_face_analyser.prepare(ctx_id=3, det_size=(640, 640))
+    #thread_face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+    thread_face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+    thread_face_swapper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider'])
+
+    while True:
+        idx, frame = frame_q.get()
+        if idx is None:
+            processed_q.put((None, None))
+            break
+        try:
+            many_faces = thread_face_analyser.get(frame)
+            if many_faces:
+                old_face = many_faces[0]
+                frame = thread_face_swapper.get(frame, old_face, new_face, paste_back=True)
+
+                for sinlge_face in many_faces:
+                    frame_ = enhance_face(thread_face_enhancer,sinlge_face, frame)                
+
+            processed_q.put((idx, frame_))
+            print(f"{current_thread().name} has proceesed frame :{idx}")           
+
+        except Exception as e:
+            print(f"[ERROR] Frame {idx} failed: {e}")
+    
+    print(f"totol time is : {time.time()- start_time}")
+
+def encode_video(output_path, fps, width, height):
+    out = av.open(output_path, mode='w')
+
+    #stream = out.add_stream('h264_nvenc', rate=fps)
+    stream = out.add_stream('libx264', rate=fps)
+    stream.width = width
+    stream.height = height
+    stream.pix_fmt = 'yuv420p'
+
+    stream.options = {'crf': '18', 'preset': 'veryfast'}
+    #stream.options = {'rc': 'constqp', 'qp': '18','preset': 'p7','profile': 'high'}
+
+    next_index = 0
+    buffer = {}
+    end_count = 0
+
+    while True:
+        idx, frame = processed_q.get()
+        if idx is None:
+            end_count += 1
+            if end_count >= NUM_WORKERS:
+                break
+            continue
+        buffer[idx] = frame
+
+        # 鍙鏈夊綋鍓嶈鍐欏叆鐨勫抚锛屽氨杈撳嚭
+        while next_index in buffer:
+            frame = buffer.pop(next_index)
+            video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
+            for packet in stream.encode(video_frame):
+                out.mux(packet)
+            next_index += 1
+
+    # 娓呯┖缂栫爜缂撳瓨
+    for packet in stream.encode():
+        out.mux(packet)
+    out.close()
+# 鍚姩浠诲姟
+video_path = './temp/buhui.mp4'
+output_path = './temp/buhui-wangrong-enhancer.mp4'
+new_face_image_path = './temp/wangrong.jpg'
+face_analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider'])
+face_analyser.prepare(ctx_id=0, det_size=(640, 640))
+
+faces = face_analyser.get(cv2.imread(new_face_image_path))
+if faces:
+    new_face = sorted(faces, key=lambda x: x.bbox[0])[0]
+        
+    #threading.Thread(target=decode_video, args=(video_path,)).start()
+    #threading.Thread(target=process_frame_loop, args=(new_face,)).start()
+    #threading.Thread(target=encode_video, args=(output_path, 25,576,1080)).start()
+    threads = []
+        
+    # 瑙g爜绾跨▼
+    t_decode = threading.Thread(target=decode_video,args=(video_path,))
+    threads.append(t_decode)
+    t_decode.start()
+    
+    # 澶氬鐞嗙嚎绋�
+    for i in range(NUM_WORKERS):
+        t = threading.Thread(target=process_frame_loop, args=(new_face,),name=f"process_worker_{i}")
+        threads.append(t)
+        t.start()
+    
+    # 缂栫爜绾跨▼
+    t_encode = threading.Thread(target=encode_video,args=(output_path, 25,576,1080))
+    threads.append(t_encode)
+    t_encode.start()
+    
+    for t in threads:
+        t.join()
+    
+    print("Processing complete.")
+
+
diff --git a/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py
new file mode 100644
index 0000000..12f4d11
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py
@@ -0,0 +1,238 @@
+import cv2
+import os
+import torch
+import queue
+import threading
+import subprocess
+import json
+import numpy as np
+import time
+from typing import Union
+import insightface
+from insightface.app.common import Face
+from .face_nsfw_predictor import NSFWPredictor
+from .face_enhancer import FaceEnhancer
+from .face_analyser import FaceAnalyser
+from auto_media_publisher.utils.logger import get_logger
+from auto_media_publisher.config.conf_base import FACE_SWAPPER_MODEL_PATH
+from auto_media_publisher.utils.media_input_output import VideoReader,VideoWriter
+from torch.utils.data import DataLoader
+from torchvision import transforms
+
+class FaceSwapWorker:
+    def __init__(self, enable_enhancer:bool = False,enable_nsfw_predictor:bool=False):
+        self.logger = get_logger("FaceSwapWorker", "admin")      
+        self.device = self._select_device()
+        self.face_swaper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider' if self.device == 'cuda' else 'CPUExecutionProvider'])
+        self.main_analyser = FaceAnalyser()
+        self.enable_enhancer = enable_enhancer
+        self.enable_nsfw_predictor = enable_nsfw_predictor
+        self.batch_size = 4
+        if self.enable_nsfw_predictor:
+            self.nsfw_predictor = NSFWPredictor()
+        self._init()
+        
+    def _init(self):
+        self.frame_queue = queue.Queue()
+        self.processed_queue = queue.Queue()
+        self.buffered_frames = {}
+        self.expected_index = 0
+        self.fps = 0
+        self.width = 0
+        self.height = 0
+        self.total_frames = 0
+        self.process_threads = []
+        self.num_processors = 4
+
+    def _select_device(self):
+        if torch.cuda.is_available():
+            return 'cuda'
+        elif torch.backends.mps.is_available():
+            return 'mps'
+        else:
+            return 'cpu'
+    
+    def auto_downsample_ratio(self,h, w):
+        """
+        Automatically find a downsample ratio so that the largest side of the resolution be 512px.
+        """
+        return min(512 / max(h, w), 1)
+
+    def _decode_video(self):
+        transform = transforms.ToTensor()
+        source = VideoReader(self.input_video_path, transform)
+       
+        reader = DataLoader(source, batch_size=self.batch_size, pin_memory=True, num_workers=self.num_processors)
+
+        writer = VideoWriter(
+                path=self.output_video_path,
+                frame_rate=30,
+                bit_rate=int(1 * 1000000))
+        
+        for src in reader:
+
+                if downsample_ratio is None:
+                    downsample_ratio = auto_downsample_ratio(*src.shape[2:])
+
+                src = src.to(device, dtype, non_blocking=True).unsqueeze(0) # [B, T, C, H, W]
+                fgr, pha, *rec = model(src, *rec, downsample_ratio)
+
+                if output_foreground is not None:
+                    writer_fgr.write(fgr[0])
+                if output_alpha is not None:
+                    writer_pha.write(pha[0])
+                if output_composition is not None:
+                    if output_type == 'video':
+                        com = fgr * pha + bgr * (1 - pha)
+                    else:
+                        fgr = fgr * pha.gt(0)
+                        com = torch.cat([fgr, pha], dim=-3)
+                    writer_com.write(com[0])
+        
+
+
+
+    def _process_frame(self):
+        thread_face_analyser = FaceAnalyser()
+        if self.enable_enhancer:
+            thread_face_enhancer = FaceEnhancer()
+        while True:
+            item = self.frame_queue.get()
+            if item is None:
+                self.processed_queue.put(None)  # 鎶婄粨鏉熶俊鍙风户缁紶閫�
+                break
+
+            idx, frame = item
+
+            print(f"[Thread ID: {threading.get_ident()}] is processing frame : {idx} of video file 锛歿self.input_video_path}")
+
+            old_face  = thread_face_analyser.get_one_face(frame)
+            if old_face:
+                frame = self.face_swaper.get(frame,old_face,self.new_face)
+            else:
+                print("face not detected")
+
+            print(self.enable_enhancer)
+
+            if self.enable_enhancer:
+                many_faces= thread_face_analyser.get_many_faces(frame)
+                if many_faces:
+                    frame = thread_face_enhancer.process_frame(frame,many_faces)
+                
+            self.processed_queue.put((idx, frame))
+
+    def _encode_video(self):
+        while not (self.width and self.height and self.fps):
+            time.sleep(0.01)
+        encode_cmd = [
+            "ffmpeg",
+            "-loglevel", "info",
+            "-y",  # 瑕嗙洊杈撳嚭鏂囦欢
+            "-f", "rawvideo",
+            "-pixel_format", "rgb24",
+            "-video_size", f"{self.width}x{self.height}",
+            "-framerate", str(self.fps),
+            "-i", "pipe:0",  # 浠� stdin 璇诲彇鍘熷瑙嗛甯�
+            #"-i", self.input_video_path if self.new_audio_path is None else self.new_audio_path, # 鎻愪緵闊抽
+            "-c:v", "libx264",   # 浣犲彲浠ユ浛鎹㈡垚 h264_nvenc銆乭evc_nvenc銆乭264_amf銆乭264_qsv 绛夌‖浠剁紪鐮佸櫒,璐ㄩ噺鍙兘绋嶅井閫婅壊
+            "-crf", "18",
+            "-preset", "veryfast",
+            "-pix_fmt", "yuv420p",
+             #"-c:a", "copy",
+             #"-map", "0:v:0",
+             #"-map", "1:a:0",
+             #"-shortest",
+            self.output_video_path
+            ]
+
+        process = subprocess.Popen(encode_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,bufsize=10**8)
+        
+        end_signals_received = 0
+        finished = 0
+
+        while True:
+            item = self.processed_queue.get()
+            if item is None:
+                end_signals_received += 1
+                if end_signals_received == self.num_processors:
+                    break
+                else:
+                    continue
+            idx,frame = item
+            self.buffered_frames[idx] = frame
+
+            while self.expected_index in self.buffered_frames:
+                next_frame = self.buffered_frames.pop(self.expected_index)
+                print(f"_encode frame next_frame: {self.expected_index}")
+                try:
+                    process.stdin.write(next_frame.astype(np.uint8).tobytes())
+                except BrokenPipeError:
+                    print("BrokenPipeError: ffmpeg process closed stdin.")
+                    break
+                self.expected_index += 1
+                finished += 1
+                print(f"_encode frame finished: {finished}")
+
+        
+        print(f"Total frames encoded: {finished}")
+
+        process.stdin.close()
+        return_code = process.wait()
+        print("FFmpeg exited with code:", return_code)
+
+    def swap_face(self,new_face_image_path:str,input_video_path: str, output_video_path: str,num_processors=4):
+        """
+        鎵ц鎹㈣劯鎿嶄綔锛屽弬鏁板涓嬶細
+        1銆� 鏂扮殑鑴哥収鐗囷紝璺緞瑕佹纭紝杩樿鍖呭惈涓�寮犱汉鑴革紱
+        2銆� 鑰佺殑瑙嗛锛岃矾寰勮姝g‘
+        3銆� 鏂拌棰� 鏈�缁堣緭鍑虹殑瑙嗛璺緞
+        4銆� 骞跺彂澶勭悊杩涚▼鏁伴噺锛岄粯璁ゆ槸4锛屼緷鎹‖浠舵�ц兘璁剧疆
+        5銆� 鏂拌棰� 鐨勯煶棰戯紝濡傛灉涓嶄负绌猴紝灏变娇鐢紱 鍚﹀垯 浣跨敤鑰佽棰戠殑闊抽
+        """
+        self._init()
+        self.num_processors = num_processors
+
+        if not os.path.exists(new_face_image_path):
+            self.logger.error(f"face swap image_path: {new_face_image_path} does not exists.")
+            return
+        self.new_face = self.main_analyser.get_one_face(cv2.imread(new_face_image_path))
+        if not self.new_face :
+            self.logger.error(f"target image DO NOT has face : {new_face_image_path} does not exists.")
+            return
+        
+        if not os.path.exists(input_video_path):
+            self.logger.error(f"face swap video_path: {input_video_path} does not exists.")
+            return
+       
+        self.input_video_path = input_video_path
+        self.output_video_path = output_video_path
+        #self.new_audio_path =new_audio_path
+
+        if self.enable_nsfw_predictor and self.nsfw_predictor:
+            if self.nsfw_predictor.predict_video(input_video_path):
+                self.logger.error(f" 瑙嗛杩濊锛� {self.input_video_path}")
+                return
+
+        # 1. 鍚姩 encode 绾跨▼锛堥渶瑕佺瓑寰� width/height/fps 璁剧疆锛屼絾鍙互鏀惧湪鍓嶉潰锛�
+        encode_thread = threading.Thread(target=self._encode_video)
+        encode_thread.start()
+
+        # 2. 鍚姩 decode 绾跨▼锛堣礋璐d粠瑙嗛涓鍙栧抚锛�
+        decode_thread = threading.Thread(target=self._decode_video)
+        decode_thread.start()
+
+        # 3. 鍚姩澶勭悊绾跨▼
+        for i in range(self.num_processors):
+            t = threading.Thread(target=self._process_frame)
+            t.start()
+            self.process_threads.append(t)
+
+        # 4. 绛夊緟 decode 瀹屾垚
+        decode_thread.join()
+
+        # 5. 绛夊緟鎵�鏈夊鐞嗙嚎绋嬪畬鎴�
+        for t in self.process_threads:
+            t.join()
+
+        # 6. 绛夊緟 encode 瀹屾垚
+        encode_thread.join()
diff --git a/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py
new file mode 100644
index 0000000..0f2452d
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py
@@ -0,0 +1,241 @@
+import os
+import cv2
+import subprocess
+import threading
+from threading import Thread, current_thread
+import queue
+import numpy as np
+import torch
+import json
+import insightface
+from .face_nsfw_predictor import NSFWPredictor
+from .face_enhancer import FaceEnhancer
+from .face_analyser import FaceAnalyser
+from auto_media_publisher.utils.logger import get_logger
+from auto_media_publisher.config.conf_base import FACE_SWAPPER_MODEL_PATH
+
+
+class FFmpegVideoProcessor:
+    def __init__(self,enable_enhancer:bool=False,enable_nsfw_predictor:bool=False):
+        self.logger = get_logger("FaceSwapWorker", "admin")      
+        self.device = self._select_device()
+        print(f"using device {self.device}")
+        self.face_swaper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider' if self.device == 'cuda' else 'CPUExecutionProvider'])
+        self.main_analyser = FaceAnalyser()
+        self.enable_enhancer = enable_enhancer
+        self.enable_nsfw_predictor = enable_nsfw_predictor
+        if self.enable_nsfw_predictor:
+            self.nsfw_predictor = NSFWPredictor()
+        self._init()
+    
+    def _init(self):
+        self.width = 0
+        self.height = 0
+        self.fps = 0
+        self.frame_queue = queue.Queue(maxsize=2000)  # 瑙g爜绾跨▼鏀惧抚
+        self.processed_queue = queue.Queue(maxsize=2000)  # 澶勭悊绾跨▼鏀惧抚
+        self.buffered_frames = {}
+        self.expected_index = 0
+        
+        self.stop_event = threading.Event()
+
+    def _select_device(self):
+        if torch.cuda.is_available():
+            return 'cuda'
+        elif torch.backends.mps.is_available():
+            return 'mps'
+        else:
+            return 'cpu'
+    
+    def probe_video(self):
+        cmd = [
+            "ffprobe",
+            "-v", "error",
+            "-select_streams", "v:0",
+            "-show_entries", "stream=width,height,r_frame_rate",
+            "-of", "json",
+            self.input_video_path,
+        ]
+        proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
+        info = json.loads(proc.stdout)
+        stream = info["streams"][0]
+        self.width = int(stream["width"])
+        self.height = int(stream["height"])
+        self.fps = eval(stream["r_frame_rate"])
+        print(f"Video info: {self.width}x{self.height} @ {self.fps}fps")
+    
+    def decode_worker(self):
+        decode_cmd = [
+            "ffmpeg",
+            "-loglevel", "error",
+            "-i", self.input_video_path,
+            "-f", "rawvideo",
+            "-pix_fmt", "rgb24",
+            "pipe:1",
+        ]
+        self.decode_proc = subprocess.Popen(decode_cmd, stdout=subprocess.PIPE, bufsize=10**8)
+        
+        frame_size = self.width * self.height * 3
+        idx = 0
+        while not self.stop_event.is_set():
+            raw_frame = self.decode_proc.stdout.read(frame_size)
+            if len(raw_frame) < frame_size:
+                break
+            frame = np.frombuffer(raw_frame, np.uint8).reshape((self.height, self.width, 3))
+            self.frame_queue.put((idx, frame))
+            idx += 1
+        
+        # 缁撴潫淇″彿
+        for _ in range(self.num_workers):
+            self.frame_queue.put(None)
+        self.decode_proc.stdout.close()
+        self.decode_proc.wait()
+    
+    def process_frame(self, frame: np.ndarray,thread_face_analyser,thread_face_enhancer) -> np.ndarray:
+        # 杩欓噷鏀句綘鐨勬崲鑴搞�佸彉閫熺瓑澶勭悊閫昏緫
+        # 渚嬪锛�
+        # frame = your_face_swap_function(frame)
+        # 鎴栬�呯畝鍗曠ず鑼冿細鍙嶈浆棰滆壊
+        #return 255 - frame
+        if self.enable_enhancer:
+            many_faces= thread_face_analyser.get_many_faces(frame)
+            if many_faces:
+                frame = self.face_swaper.get(frame,many_faces[0],self.new_face)
+                frame = thread_face_enhancer.process_frame(frame,many_faces)
+        else:
+            old_face  = thread_face_analyser.get_one_face(frame)
+            if old_face:
+                frame = self.face_swaper.get(frame,old_face,self.new_face)
+            else:
+                print("face not detected")
+
+        return frame
+    
+    def process_worker(self):
+        thread_face_analyser = FaceAnalyser()
+        if self.enable_enhancer:
+            thread_face_enhancer = FaceEnhancer()
+        else:
+            thread_face_enhancer = None
+        while True:
+            item = self.frame_queue.get()
+            if item is None:
+                self.processed_queue.put(None)
+                break
+            idx, frame = item
+            try:
+                processed = self.process_frame(frame,thread_face_analyser,thread_face_enhancer)
+                print(f"{current_thread().name} has proceesed frame :{idx}")
+            except:
+                print(f"{current_thread().name} has error frame :{idx}")
+                processed=frame
+            self.processed_queue.put((idx, processed))
+    
+    def encode_worker(self):
+        input_audio_path = self.input_video_path
+        if self.new_audio_path and os.path.exists(self.new_audio_path):
+            input_audio_path = self.new_audio_path
+        encode_cmd = [
+            "ffmpeg",
+            "-y",
+            "-loglevel", "error",
+            "-f", "rawvideo",
+            "-pixel_format", "rgb24",
+            "-video_size", f"{self.width}x{self.height}",
+            "-framerate", str(self.fps),
+            "-i", "pipe:0",
+            "-i", input_audio_path,
+            "-c:v", "libx264",
+            "-crf", "18",
+            "-preset", "veryfast",
+            "-c:a", "copy",
+            "-map", "0:v:0",
+            "-map", "1:a:0",
+            "-shortest",
+            self.output_video_path,
+        ]
+        self.encode_proc = subprocess.Popen(encode_cmd, stdin=subprocess.PIPE, bufsize=10**8)
+        
+        end_signals = 0
+        finished = 0
+        while True:
+            item = self.processed_queue.get()
+            if item is None:
+                end_signals += 1
+                if end_signals == self.num_workers:
+                    break
+                else:
+                    continue
+            
+            idx, frame = item
+            self.buffered_frames[idx] = frame
+            
+            while self.expected_index in self.buffered_frames:
+                f = self.buffered_frames.pop(self.expected_index)
+                try:
+                    self.encode_proc.stdin.write(f.astype(np.uint8).tobytes())
+                    print(f"{self.expected_index} has been writed into stdin")
+                except BrokenPipeError:
+                    print("FFmpeg encoding pipe broken.")
+                    self.stop_event.set()
+                    break
+                self.expected_index += 1
+                finished += 1
+        
+        self.encode_proc.stdin.close()
+        self.encode_proc.wait()
+        print(f"Encoding finished, total frames: {finished}")
+    
+    def run(self,new_face_image_path,input_video_path, output_video_path, num_workers=4,new_audio_path=None):
+        """
+        鎵ц鎹㈣劯鎿嶄綔锛屽弬鏁板涓嬶細
+        1銆� 鏂扮殑鑴哥収鐗囷紝璺緞瑕佹纭紝杩樿鍖呭惈涓�寮犱汉鑴革紱
+        2銆� 鑰佺殑瑙嗛锛岃矾寰勮姝g‘
+        3銆� 鏂拌棰� 鏈�缁堣緭鍑虹殑瑙嗛璺緞
+        4銆� 骞跺彂澶勭悊杩涚▼鏁伴噺锛岄粯璁ゆ槸4锛屼緷鎹‖浠舵�ц兘璁剧疆
+        5銆� 鏂拌棰� 鐨勯煶棰戯紝濡傛灉涓嶄负绌猴紝灏变娇鐢紱 鍚﹀垯 浣跨敤鑰佽棰戠殑闊抽
+        """
+        if not os.path.exists(new_face_image_path):
+            self.logger.error(f"face swap image_path: {new_face_image_path} does not exists.")
+            return
+        self.new_face = self.main_analyser.get_one_face(cv2.imread(new_face_image_path))
+
+        if not self.new_face :
+            self.logger.error(f"target image DO NOT has face : {new_face_image_path} does not exists.")
+            return
+        
+        if not os.path.exists(input_video_path):
+            self.logger.error(f"face swap video_path: {input_video_path} does not exists.")
+            return
+       
+        self._init()
+        self.input_video_path = input_video_path
+        self.output_video_path = output_video_path
+        self.num_workers = num_workers
+        self.new_audio_path =new_audio_path
+        
+        self.probe_video()
+        
+        threads = []
+        
+        # 瑙g爜绾跨▼
+        t_decode = threading.Thread(target=self.decode_worker)
+        threads.append(t_decode)
+        t_decode.start()
+        
+        # 澶氬鐞嗙嚎绋�
+        for i in range(self.num_workers):
+            t = threading.Thread(target=self.process_worker, name=f"process_worker_{i}")
+            threads.append(t)
+            t.start()
+        
+        # 缂栫爜绾跨▼
+        t_encode = threading.Thread(target=self.encode_worker)
+        threads.append(t_encode)
+        t_encode.start()
+        
+        for t in threads:
+            t.join()
+        
+        print("Processing complete.")
+
diff --git a/auto_media_publisher/modifiers/face_swap/typing.py b/auto_media_publisher/modifiers/face_swap/typing.py
new file mode 100644
index 0000000..bc8d6fc
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/typing.py
@@ -0,0 +1,7 @@
+
+from typing import Any
+from insightface.app.common import Face
+import numpy
+
+Face = Face
+Frame = numpy.ndarray[Any, Any]
\ No newline at end of file
diff --git a/auto_media_publisher/utils/media_input_output.py b/auto_media_publisher/utils/media_input_output.py
new file mode 100644
index 0000000..4e2e209
--- /dev/null
+++ b/auto_media_publisher/utils/media_input_output.py
@@ -0,0 +1,93 @@
+import av
+import os
+import pims
+import numpy as np
+from torch.utils.data import Dataset
+from torchvision.transforms.functional import to_pil_image
+from PIL import Image
+
+
+class VideoReader(Dataset):
+    def __init__(self, path, transform=None):
+        self.video = pims.PyAVVideoReader(path)
+        self.rate = self.video.frame_rate
+        self.transform = transform
+        
+    @property
+    def frame_rate(self):
+        return self.rate
+        
+    def __len__(self):
+        return len(self.video)
+        
+    def __getitem__(self, idx):
+        frame = self.video[idx]
+        frame = Image.fromarray(np.asarray(frame))
+        if self.transform is not None:
+            frame = self.transform(frame)
+        return frame
+
+
+class VideoWriter:
+    def __init__(self, path, frame_rate, bit_rate=1000000):
+        self.container = av.open(path, mode='w')
+        self.stream = self.container.add_stream('h264', rate=round(frame_rate))
+        self.stream.pix_fmt = 'yuv420p'
+        self.stream.bit_rate = bit_rate
+        self.stream.codec_context.options = {
+            'crf': '18',  # 鎺у埗鐢昏川锛岃秺灏忚秺娓呮櫚锛�18鏄珮璐ㄩ噺锛�
+            'preset': 'veryfast',     # 褰卞搷缂栫爜閫熷害锛宮edium 鏄�1x锛宖ast鏄�2x faster鏄�3x veryfast鏄�4x superfast鏄�6x ultrafast鏄�10x锛屾枃浠朵綋绉ぇ绾︿細澧炲姞(浠edium涓哄熀鍑�) 10%锛�15%锛�20% 30% 50%
+            'tune': 'film',
+        }
+    
+    def write(self, frames):
+        # frames: [T, C, H, W]
+        self.stream.width = frames.size(3)
+        self.stream.height = frames.size(2)
+        if frames.size(1) == 1:
+            frames = frames.repeat(1, 3, 1, 1) # convert grayscale to RGB
+        frames = frames.mul(255).byte().cpu().permute(0, 2, 3, 1).numpy()
+        for t in range(frames.shape[0]):
+            frame = frames[t]
+            frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
+            self.container.mux(self.stream.encode(frame))
+                
+    def close(self):
+        self.container.mux(self.stream.encode())
+        self.container.close()
+
+
+class ImageSequenceReader(Dataset):
+    def __init__(self, path, transform=None):
+        self.path = path
+        self.files = sorted(os.listdir(path))
+        self.transform = transform
+        
+    def __len__(self):
+        return len(self.files)
+    
+    def __getitem__(self, idx):
+        with Image.open(os.path.join(self.path, self.files[idx])) as img:
+            img.load()
+        if self.transform is not None:
+            return self.transform(img)
+        return img
+
+
+class ImageSequenceWriter:
+    def __init__(self, path, extension='jpg'):
+        self.path = path
+        self.extension = extension
+        self.counter = 0
+        os.makedirs(path, exist_ok=True)
+    
+    def write(self, frames):
+        # frames: [T, C, H, W]
+        for t in range(frames.shape[0]):
+            to_pil_image(frames[t]).save(os.path.join(
+                self.path, str(self.counter).zfill(4) + '.' + self.extension))
+            self.counter += 1
+            
+    def close(self):
+        pass
+        
\ No newline at end of file

--
Gitblit v1.9.1