wangzhibo
2025-07-30 c706a69abfe9bae1322adbd344dfdaae37cdf564
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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、hevc_nvenc、h264_amf、h264_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、 老的视频,路径要正确
        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 线程(负责从视频中读取帧)
        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()