Files changed (1) hide show
  1. app.py +252 -51
app.py CHANGED
@@ -1,84 +1,285 @@
 
 
 
 
 
 
1
  import gradio as gr
2
- import os, uuid, time
3
  from gradio_client import Client, handle_file
4
- from moviepy.editor import VideoFileClip
5
 
6
- # ── config ───────────────────────────────────────────────────────────
7
- hf_token = os.environ.get("TOKEN")
8
- output_dir = "uploads/output"
9
- os.makedirs(output_dir, exist_ok=True)
10
- client = Client("leonormar009/Gen-Z", token=hf_token, download_files=output_dir)
 
 
 
 
 
 
 
11
 
12
- UTM = "utm_source=hugging_face_space&utm_medium=banner&utm_campaign=pro_cta"
13
- #PRO_URL = f"https://www.face-swap.co/?{UTM}"
 
 
 
 
 
 
 
 
 
14
  PRO_URL = f"https://marpogi.qzz.io/?{UTM}"
15
 
16
- # ── helpers ──────────────────────────────────────────────────────────
17
- def preprocess_video(path: str, target_fps: int = 12,
18
- target_size: int = 800, target_len: int = 4) -> str:
19
- clip = VideoFileClip(path)
20
- if clip.duration > target_len:
21
- clip = clip.subclip(0, target_len)
22
- w, h = clip.size
23
- clip = clip.resize(width=target_size) if w >= h else clip.resize(height=target_size)
24
- clip = clip.set_fps(target_fps)
25
- out_path = os.path.join(output_dir, f"pre_{uuid.uuid4().hex}.mp4")
26
- clip.write_videofile(out_path, codec="libx264", audio_codec="aac",
27
- fps=target_fps, verbose=False, logger=None)
28
- clip.close()
29
- return out_path
30
-
31
- # ── main generate ────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def generate(input_image, input_video, gender):
33
- if not input_image:
 
 
 
34
  raise gr.Error("Missing source image.")
35
- if not input_video:
 
36
  raise gr.Error("Missing target video.")
37
 
38
- if gender == "all":
39
- gender = None
40
 
41
  try:
42
  print("Starting generation...", flush=True)
43
 
44
- pre_video = preprocess_video(input_video)
45
 
46
  job = client.submit(
47
- input_image=handle_file(input_image),
48
  input_video=handle_file(pre_video),
49
  device="cpu",
50
  selector="many",
51
- gender=gender,
52
  race=None,
53
  order=None,
54
- api_name="/generate"
55
  )
56
 
57
- while not job.done():
58
- time.sleep(2)
 
59
 
60
- outputs = job.outputs()
61
- if not outputs:
62
- raise gr.Error("Remote Space returned no outputs.")
63
-
64
- first = outputs[0]
65
- video_path = first.get("video") or first.get("path") if isinstance(first, dict) else first
66
-
67
- if not video_path:
68
- raise gr.Error("Could not find output video.")
69
 
70
  print("Generation finished.", flush=True)
71
 
72
  gr.Info(
73
  "✨ Your preview is ready.<br>"
74
- "<strong>Get HD</strong> (4Γ— quality, no watermark, priority) β€” "
75
- '<a href="https://www.face-swap.co/?utm_source=hfspace_deepfakevideo&utm_medium=info" target="_blank" rel="noopener">Upgrade on face-swap.co</a>',
76
- duration=8
 
 
 
 
77
  )
78
 
79
- return video_path
 
 
 
 
80
 
81
- except Exception as e:
82
- print(f"Generation failed: {e}", flush=True)
83
- raise gr.Error(f"Generation failed: {e}")
84
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
  import gradio as gr
 
8
  from gradio_client import Client, handle_file
 
9
 
10
+ # MoviePy 2.x moved VideoFileClip out of moviepy.editor.
11
+ try:
12
+ from moviepy import VideoFileClip
13
+ except ImportError:
14
+ from moviepy.editor import VideoFileClip
15
+
16
+
17
+ # ── config ────────────────────────────────────────────────────────────
18
+ HF_TOKEN = os.environ.get("TOKEN")
19
+
20
+ OUTPUT_DIR = Path("uploads/output").resolve()
21
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
22
 
23
+ client = Client(
24
+ "leonormar009/Gen-Z",
25
+ token=HF_TOKEN,
26
+ download_files=str(OUTPUT_DIR),
27
+ )
28
+
29
+ UTM = (
30
+ "utm_source=hugging_face_space"
31
+ "&utm_medium=banner"
32
+ "&utm_campaign=pro_cta"
33
+ )
34
  PRO_URL = f"https://marpogi.qzz.io/?{UTM}"
35
 
36
+
37
+ # ── helpers ───────────────────────────────────────────────────────────
38
+ def _get_file_path(value: Any) -> Optional[str]:
39
+ """
40
+ Convert common Gradio file values to a local file path.
41
+
42
+ Depending on the Gradio component configuration and Gradio version,
43
+ uploaded files may be returned as a string, pathlib.Path, or dict.
44
+ """
45
+ if value is None:
46
+ return None
47
+
48
+ if isinstance(value, (str, os.PathLike)):
49
+ return os.fspath(value)
50
+
51
+ if isinstance(value, dict):
52
+ for key in ("path", "filepath", "name"):
53
+ path = value.get(key)
54
+ if path:
55
+ return os.fspath(path)
56
+
57
+ # Some Gradio versions return objects with a `path` attribute.
58
+ path = getattr(value, "path", None)
59
+ if path:
60
+ return os.fspath(path)
61
+
62
+ return None
63
+
64
+
65
+ def _resize_clip(clip, width: int, height: int):
66
+ """Support both MoviePy 1.x and MoviePy 2.x APIs."""
67
+ if hasattr(clip, "resized"): # MoviePy 2.x
68
+ return clip.resized(new_size=(width, height))
69
+
70
+ # MoviePy 1.x
71
+ return clip.resize(newsize=(width, height))
72
+
73
+
74
+ def _set_clip_fps(clip, fps: int):
75
+ """Support both MoviePy 1.x and MoviePy 2.x APIs."""
76
+ if hasattr(clip, "with_fps"): # MoviePy 2.x
77
+ return clip.with_fps(fps)
78
+
79
+ # MoviePy 1.x
80
+ return clip.set_fps(fps)
81
+
82
+
83
+ def _subclip(clip, start: float, end: float):
84
+ """Support both MoviePy 1.x and MoviePy 2.x APIs."""
85
+ if hasattr(clip, "subclipped"): # MoviePy 2.x
86
+ return clip.subclipped(start, end)
87
+
88
+ # MoviePy 1.x
89
+ return clip.subclip(start, end)
90
+
91
+
92
+ def preprocess_video(
93
+ path: str,
94
+ target_fps: int = 12,
95
+ target_size: int = 800,
96
+ target_len: float = 4,
97
+ ) -> str:
98
+ """
99
+ Trim, resize, and re-encode a video for the remote API.
100
+
101
+ `target_size` is applied to the video's longest edge.
102
+ """
103
+ if not path or not os.path.isfile(path):
104
+ raise ValueError(f"Input video does not exist: {path!r}")
105
+
106
+ if target_fps <= 0:
107
+ raise ValueError("target_fps must be greater than zero.")
108
+
109
+ if target_size <= 0:
110
+ raise ValueError("target_size must be greater than zero.")
111
+
112
+ source_clip = None
113
+ processed_clip = None
114
+
115
+ try:
116
+ source_clip = VideoFileClip(path)
117
+
118
+ duration = float(source_clip.duration or 0)
119
+ if duration <= 0:
120
+ raise ValueError("The input video has no valid duration.")
121
+
122
+ processed_clip = source_clip
123
+
124
+ if duration > target_len:
125
+ processed_clip = _subclip(processed_clip, 0, target_len)
126
+
127
+ source_width, source_height = map(int, source_clip.size)
128
+
129
+ if source_width >= source_height:
130
+ new_width = target_size
131
+ new_height = round(target_size * source_height / source_width)
132
+ else:
133
+ new_height = target_size
134
+ new_width = round(target_size * source_width / source_height)
135
+
136
+ # H.264 generally requires even dimensions.
137
+ new_width = max(2, new_width - (new_width % 2))
138
+ new_height = max(2, new_height - (new_height % 2))
139
+
140
+ processed_clip = _resize_clip(
141
+ processed_clip,
142
+ width=new_width,
143
+ height=new_height,
144
+ )
145
+ processed_clip = _set_clip_fps(processed_clip, target_fps)
146
+
147
+ output_path = OUTPUT_DIR / f"pre_{uuid.uuid4().hex}.mp4"
148
+
149
+ # Do not pass `verbose=False`: that argument was removed in MoviePy 2.x.
150
+ processed_clip.write_videofile(
151
+ str(output_path),
152
+ codec="libx264",
153
+ audio_codec="aac",
154
+ fps=target_fps,
155
+ logger=None,
156
+ )
157
+
158
+ return str(output_path)
159
+
160
+ finally:
161
+ # Closing the clips prevents leaked file handles and locked temp files.
162
+ if processed_clip is not None:
163
+ try:
164
+ processed_clip.close()
165
+ except Exception:
166
+ pass
167
+
168
+ if source_clip is not None and source_clip is not processed_clip:
169
+ try:
170
+ source_clip.close()
171
+ except Exception:
172
+ pass
173
+
174
+
175
+ def _find_video_path(value: Any) -> Optional[str]:
176
+ """
177
+ Extract a video path or URL from the different result formats that
178
+ gradio_client may return.
179
+ """
180
+ if value is None:
181
+ return None
182
+
183
+ if isinstance(value, (str, os.PathLike)):
184
+ return os.fspath(value)
185
+
186
+ if isinstance(value, dict):
187
+ # The API may return {"video": ...}, {"path": ...}, or FileData.
188
+ for key in ("video", "path", "filepath", "url", "name"):
189
+ if key in value:
190
+ result = _find_video_path(value[key])
191
+ if result:
192
+ return result
193
+
194
+ # Handle nested result structures.
195
+ for nested_value in value.values():
196
+ result = _find_video_path(nested_value)
197
+ if result:
198
+ return result
199
+
200
+ if isinstance(value, (list, tuple)):
201
+ for item in value:
202
+ result = _find_video_path(item)
203
+ if result:
204
+ return result
205
+
206
+ path = getattr(value, "path", None)
207
+ if path:
208
+ return os.fspath(path)
209
+
210
+ return None
211
+
212
+
213
+ # ── main generate ─────────────────────────────────────────────────────
214
  def generate(input_image, input_video, gender):
215
+ image_path = _get_file_path(input_image)
216
+ video_path = _get_file_path(input_video)
217
+
218
+ if not image_path:
219
  raise gr.Error("Missing source image.")
220
+
221
+ if not video_path:
222
  raise gr.Error("Missing target video.")
223
 
224
+ pre_video = None
 
225
 
226
  try:
227
  print("Starting generation...", flush=True)
228
 
229
+ pre_video = preprocess_video(video_path)
230
 
231
  job = client.submit(
232
+ input_image=handle_file(image_path),
233
  input_video=handle_file(pre_video),
234
  device="cpu",
235
  selector="many",
236
+ gender=None if gender == "all" else gender,
237
  race=None,
238
  order=None,
239
+ api_name="/generate",
240
  )
241
 
242
+ # result() waits for completion and propagates remote errors.
243
+ result = job.result()
244
+ output_video = _find_video_path(result)
245
 
246
+ if not output_video:
247
+ raise RuntimeError(
248
+ f"Remote Space returned no usable output video. "
249
+ f"Received: {result!r}"
250
+ )
 
 
 
 
251
 
252
  print("Generation finished.", flush=True)
253
 
254
  gr.Info(
255
  "✨ Your preview is ready.<br>"
256
+ "<strong>Get HD</strong> "
257
+ "(4Γ— quality, no watermark, priority) β€” "
258
+ '<a href="https://www.face-swap.co/'
259
+ '?utm_source=hfspace_deepfakevideo'
260
+ '&utm_medium=info" target="_blank" rel="noopener">'
261
+ "Upgrade on face-swap.co</a>",
262
+ duration=8,
263
  )
264
 
265
+ return output_video
266
+
267
+ except gr.Error:
268
+ # Do not wrap an existing Gradio error a second time.
269
+ raise
270
 
271
+ except Exception as exc:
272
+ print(f"Generation failed: {exc}", flush=True)
273
+ raise gr.Error(f"Generation failed: {exc}") from exc
274
 
275
+ finally:
276
+ # The preprocessed file is only needed while submitting the job.
277
+ if pre_video and os.path.isfile(pre_video):
278
+ try:
279
+ os.remove(pre_video)
280
+ except OSError as exc:
281
+ print(
282
+ f"Warning: could not remove temporary file "
283
+ f"{pre_video}: {exc}",
284
+ flush=True,
285
+ )