| File was renamed from auto_media_publisher/utils/face_swap/face_swaper_ffmpeg.py |
| | |
| | | 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.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() |
| | |
| | | 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): |
| | | # === Step 1: 用 ffprobe 获取视频信息 === |
| | | probe_cmd = [ |
| | | "ffprobe", |
| | | "-v", "error", |
| | | "-select_streams", "v:0", |
| | | "-show_entries", "stream=width,height,r_frame_rate", |
| | | "-of", "json", |
| | | self.input_video_path |
| | | ] |
| | | probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True) |
| | | probe_data = json.loads(probe_result.stdout) |
| | | video_stream = probe_data['streams'][0] |
| | | self.width = int(video_stream['width']) |
| | | self.height = int(video_stream['height']) |
| | | self.fps = eval(video_stream['r_frame_rate']) # e.g. "30/1" |
| | | 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) |
| | | |
| | | print(f"Video info: {self.width}x{self.height} @ {self.fps}fps") |
| | | writer = VideoWriter( |
| | | path=self.output_video_path, |
| | | frame_rate=30, |
| | | bit_rate=int(1 * 1000000)) |
| | | |
| | | for src in reader: |
| | | |
| | | # === Step 2: 用 ffmpeg 解码输出 rawvideo 到 stdout pipe === |
| | | decode_cmd = [ |
| | | "ffmpeg", |
| | | "-loglevel", "info", |
| | | "-i", self.input_video_path, |
| | | "-f", "rawvideo", |
| | | "-pix_fmt", "rgb24", |
| | | "pipe:1" |
| | | ] |
| | | process = subprocess.Popen(decode_cmd, stdout=subprocess.PIPE, bufsize=10**8) |
| | | if downsample_ratio is None: |
| | | downsample_ratio = auto_downsample_ratio(*src.shape[2:]) |
| | | |
| | | frame_size = self.width * self.height * 3 |
| | | idx = 0 |
| | | while True: |
| | | in_bytes = process.stdout.read(frame_size) |
| | | if not in_bytes: |
| | | break |
| | | frame = np.frombuffer(in_bytes, dtype=np.uint8).reshape((self.height, self.width, 3)) |
| | | self.frame_queue.put((idx, frame)) # 改为 tuple 形式 |
| | | idx += 1 |
| | | src = src.to(device, dtype, non_blocking=True).unsqueeze(0) # [B, T, C, H, W] |
| | | fgr, pha, *rec = model(src, *rec, downsample_ratio) |
| | | |
| | | self.total_frames = idx |
| | | print(f"_decode_video Total frames: {self.total_frames}") |
| | | 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]) |
| | | |
| | | |
| | | process.stdout.close() |
| | | process.wait() |
| | | |
| | | # 通知处理线程结束 |
| | | for _ in range(self.num_processors): |
| | | self.frame_queue.put(None) |
| | | |
| | | def _process_frame(self): |
| | | thread_face_analyser = FaceAnalyser() |