File size: 6,483 Bytes
67812a9
71694c3
 
 
 
 
242c739
 
 
 
 
 
9bd2cd2
242c739
 
 
f63630b
 
 
242c739
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f568b
 
9bd2cd2
 
97f568b
5273177
 
97f568b
 
 
5273177
 
97f568b
5273177
97f568b
 
242c739
 
 
 
 
 
 
 
 
 
97f568b
242c739
 
 
 
 
 
 
 
 
 
 
97f568b
242c739
 
 
97f568b
 
242c739
97f568b
242c739
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f568b
5273177
 
97f568b
9bd2cd2
97f568b
 
242c739
 
97f568b
242c739
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f568b
242c739
 
97f568b
 
5273177
97f568b
242c739
 
 
 
 
 
 
 
 
9bd2cd2
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
import os
import torch
print(f"Is CUDA available: {torch.cuda.is_available()}")
# True
print(f"CUDA device: {torch.cuda.get_device_name(torch.cuda.current_device())}")
# Tesla T4import os
import shutil
import subprocess
import tempfile
from pathlib import Path
import cv2
import gradio as gr
import onnxruntime as ort
from huggingface_hub import hf_hub_download
from insightface.app import FaceAnalysis
from insightface.model_zoo import get_model
import tensorflow as tf
print(tf.config.list_physical_devices('GPU'))
# [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
APP_DIR = Path(__file__).resolve().parent
MODELS_DIR = APP_DIR / "models"
TMP_DIR = APP_DIR / "tmp"
MODELS_DIR.mkdir(exist_ok=True)
TMP_DIR.mkdir(exist_ok=True)

MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "ezioruan/inswapper_128.onnx")
MODEL_FILENAME = os.environ.get("MODEL_FILENAME", "inswapper_128.onnx")
MODEL_REVISION = os.environ.get("MODEL_REVISION")
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
FACE_MODEL_NAME = os.environ.get("FACE_MODEL_NAME", "buffalo_l")
DETECTION_SIZE = int(os.environ.get("DETECTION_SIZE", "640"))
MAX_FRAMES = int(os.environ.get("MAX_FRAMES", "0"))

face_analyzer = None
face_swapper = None
model_path = None


def ensure_ffmpeg():
    if shutil.which("ffmpeg") is None:
        raise RuntimeError("ffmpeg is required but not available in PATH.")


def available_providers():
    return ort.get_available_providers()


def gpu_ready():
    providers = available_providers()
    return "CUDAExecutionProvider" in providers, providers


def gpu_guard():
    ok, providers = gpu_ready()
    if not ok:
        raise RuntimeError(
            "GPU device not attached. CUDAExecutionProvider is missing. "
            f"Available providers: {providers}"
        )


def ensure_swap_model() -> str:
    global model_path
    if model_path and os.path.exists(model_path):
        return model_path
    local_target = MODELS_DIR / MODEL_FILENAME
    if local_target.exists():
        model_path = str(local_target)
        return model_path
    model_path = hf_hub_download(
        repo_id=MODEL_REPO_ID,
        filename=MODEL_FILENAME,
        revision=MODEL_REVISION,
        token=HF_TOKEN,
        local_dir=str(MODELS_DIR),
    )
    return model_path


def load_models():
    global face_analyzer, face_swapper
    gpu_guard()
    if face_analyzer is not None and face_swapper is not None:
        return face_analyzer, face_swapper
    swap_model_path = ensure_swap_model()
    providers = ["CUDAExecutionProvider"]
    face_analyzer = FaceAnalysis(name=FACE_MODEL_NAME, providers=providers)
    face_analyzer.prepare(ctx_id=0, det_size=(DETECTION_SIZE, DETECTION_SIZE))
    face_swapper = get_model(swap_model_path, providers=providers)
    return face_analyzer, face_swapper


def pick_largest_face(faces):
    if not faces:
        return None
    return max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))


def extract_audio(input_video: str, audio_path: str):
    subprocess.run(["ffmpeg", "-y", "-i", input_video, "-vn", "-acodec", "copy", audio_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def mux_audio(video_path: str, audio_path: str, output_path: str):
    if not os.path.exists(audio_path):
        shutil.move(video_path, output_path)
        return
    subprocess.run(["ffmpeg", "-y", "-i", video_path, "-i", audio_path, "-c:v", "copy", "-c:a", "aac", "-shortest", output_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    os.remove(video_path)


def swap_frame(frame_bgr, source_face, analyzer, swapper):
    target_faces = analyzer.get(frame_bgr)
    if not target_faces:
        return frame_bgr
    result = frame_bgr.copy()
    for target_face in target_faces:
        result = swapper.get(result, target_face, source_face, paste_back=True)
    return result


def startup_status():
    ok, providers = gpu_ready()
    return f"GPU ready: {ok} | Providers: {providers}"


def process_video(face_image_path, video_path, progress=gr.Progress(track_tqdm=False)):
    gpu_guard()
    ensure_ffmpeg()
    analyzer, swapper = load_models()

    src = cv2.imread(face_image_path)
    if src is None:
        raise ValueError("Could not read the source face image.")
    source_face = pick_largest_face(analyzer.get(src))
    if source_face is None:
        raise ValueError("No source face detected in the uploaded image.")

    work_dir = Path(tempfile.mkdtemp(dir=TMP_DIR))
    silent_video = str(work_dir / "silent.mp4")
    audio_file = str(work_dir / "audio.aac")
    final_video = str(work_dir / "faceswapped.mp4")

    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        raise ValueError("Could not open the uploaded video.")

    fps = cap.get(cv2.CAP_PROP_FPS) or 24.0
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1
    limit = min(frame_count, MAX_FRAMES) if MAX_FRAMES > 0 else frame_count

    writer = cv2.VideoWriter(silent_video, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
    extract_audio(video_path, audio_file)

    idx = 0
    while idx < limit:
        ok, frame = cap.read()
        if not ok:
            break
        writer.write(swap_frame(frame, source_face, analyzer, swapper))
        idx += 1
        progress(idx / max(limit, 1), desc=f"Processed {idx}/{limit} frames")

    cap.release()
    writer.release()
    mux_audio(silent_video, audio_file, final_video)
    return final_video, f"Done. Processed {idx} frame(s)."


with gr.Blocks(title="Video FaceSwap GPU Only") as demo:
    gr.Markdown("""# Video FaceSwap GPU Only
This Space only works when a real GPU is attached. If CUDA is missing, processing is blocked.""")
    gr.Textbox(label="GPU status", value=startup_status(), interactive=False)
    with gr.Row():
        face_image = gr.Image(type="filepath", label="Source face image")
        target_video = gr.Video(label="Target video")
    run_btn = gr.Button("Run face swap", variant="primary")
    output_video = gr.Video(label="Output video")
    status = gr.Textbox(label="Status")
    run_btn.click(fn=process_video, inputs=[face_image, target_video], outputs=[output_video, status], api_name="faceswap_video")

if __name__ == "__main__":
    demo.queue(max_size=8).launch(server_name="0.0.0.0", server_port=7860, share=False)