mrdarkbr/wan22-code-backup
055
1"""
2video_api_client.py — Cliente de geração de vídeo para o Naked AI Bot.
3Conecta ao servidor Modal e gerencia a geração de vídeo com updates diretos ao Telegram.
4"""
5import asyncio
6import aiohttp
7from http_client import get_http_session
8import base64
9import os
10import tempfile
11import time
12import uuid
13
14# ── URL do servidor Modal (T2V — imagem para vídeo) ───────────────────────────
15VIDEO_API_URL = "https://uch2f82placa--wan22-api-v7-comfyuiworker-gerar-video.modal.run"
16
17# ── URL do servidor Modal (Animate — foto + vídeo referência) ─────────────────
18ANIMATE_API_URL = "https://uch2f82placa--wan22-animate-api-v1-animateworker-gerar-v-fcfdcf.modal.run"
19
20# ── URL do servidor Modal (MiniMax H3 — foto + prompt → vídeo realístico) ─────
21MINIMAX_API_URL = "https://uch2f82placa--minimax-api-v1-minimaxworker-gerar-video.modal.run"
22MINIMAX_EXTEND_API_URL = "https://uch2f82placa--minimax-h3-extend-minimaxh3extender-extend.modal.run"
23
24# Pasta de saída dos vídeos
25OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "video_outputs")
26os.makedirs(OUTPUT_DIR, exist_ok=True)
27
28# Arquivo de timestamp para detectar cold start
29_STAMP_FILE = os.path.join(os.path.dirname(__file__), ".modal_video_last_success")
30
31
32def _load_stamp() -> float:
33 try:
34 with open(_STAMP_FILE) as f:
35 return float(f.read().strip())
36 except Exception:
37 return 0.0
38
39
40def _save_stamp():
41 try:
42 with open(_STAMP_FILE, "w") as f:
43 f.write(str(time.time()))
44 except Exception:
45 pass
46
47
48def _cold_start() -> bool:
49 last = _load_stamp()
50 return last == 0 or (time.time() - last) > 600
51
52
53async def gerar_video_async(
54 image_path: str,
55 prompt_text: str = "",
56 length_frames: int = 81,
57 resolution: int = 1080,
58 bot_token: str = "",
59 chat_id: int = 0,
60 message_id: int = 0,
61 preset_name: str = "custom",
62) -> str | None:
63 """
64 Envia a imagem para o servidor Modal e aguarda o vídeo gerado.
65 O Modal atualiza o status diretamente no Telegram via bot_token/chat_id/message_id.
66 Retorna o caminho local do .mp4 gerado, ou None em caso de erro.
67 """
68 print(f"[VideoAPI] Processando imagem local: {image_path}")
69 with open(image_path, "rb") as f:
70 image_b64 = base64.b64encode(f.read()).decode()
71
72 payload = {
73 "image_b64": image_b64,
74 "prompt": prompt_text,
75 "length": length_frames,
76 "resolution": resolution,
77 "bot_token": bot_token,
78 "chat_id": chat_id,
79 "message_id": message_id,
80 "preset_name": preset_name,
81 }
82
83 is_cold = _cold_start()
84 timeout_total = 1800 if is_cold else 1500
85 print(f"[VideoAPI] {'🥶 Cold start' if is_cold else '🔥 Warm start'} — timeout: {timeout_total}s")
86
87 try:
88 session = await get_http_session()
89 async with asyncio.timeout(timeout_total):
90 print(f"[VideoAPI] Enviando para {VIDEO_API_URL}...")
91 async with session.post(VIDEO_API_URL, json=payload) as resp:
92 if resp.status != 200:
93 body = await resp.text()
94 print(f"[VideoAPI] ❌ HTTP {resp.status}: {body[:300]}")
95 return None
96
97 data = await resp.json()
98
99 if data.get("status") == "error":
100 print(f"[VideoAPI] ❌ Erro na API: {data.get('message')}")
101 return None
102 elif data.get("status") == "success":
103 video_b64 = data.get("video_b64", "")
104 if not video_b64:
105 print("[VideoAPI] ❌ video_b64 vazio na resposta")
106 return None
107
108 print(f"[VideoAPI] ✅ Vídeo recebido! ({len(video_b64)} chars)")
109
110 fd, local_path = tempfile.mkstemp(suffix=".mp4", prefix="nakedvideo_", dir=OUTPUT_DIR)
111 os.close(fd)
112
113 with open(local_path, "wb") as f:
114 f.write(base64.b64decode(video_b64))
115
116 _save_stamp()
117 print(f"[VideoAPI] ✅ Vídeo salvo: {local_path}")
118 return local_path
119 return None
120
121 except asyncio.TimeoutError:
122 print(f"[VideoAPI] ⏰ Timeout após {timeout_total}s")
123 return None
124 except Exception as e:
125 print(f"[VideoAPI] ❌ Exceção: {e}")
126 return None
127
128
129# ── Arquivo de timestamp para MiniMax ─────────────────────────────────────────
130_MINIMAX_STAMP_FILE = os.path.join(os.path.dirname(__file__), ".minimax_last_success")
131
132
133def _load_minimax_stamp() -> float:
134 try:
135 with open(_MINIMAX_STAMP_FILE) as f:
136 return float(f.read().strip())
137 except Exception:
138 return 0.0
139
140
141def _save_minimax_stamp():
142 try:
143 with open(_MINIMAX_STAMP_FILE, "w") as f:
144 f.write(str(time.time()))
145 except Exception:
146 pass
147
148
149def _minimax_cold_start() -> bool:
150 last = _load_minimax_stamp()
151 return last == 0 or (time.time() - last) > 600
152
153
154async def gerar_minimax_video_async(
155 image_path: str,
156 prompt_text: str = "The person moves naturally",
157 image_path2: str = None,
158 lora_name: str = "HMNSFW_AIO_V2.safetensors",
159 lora_strength: float = 0.5,
160 bot_token: str = "",
161 chat_id: int = 0,
162 message_id: int = 0,
163 duration: float = 10.5,
164 mp: float = 0.5,
165 first_frame_path: str = None, # Caminho para o frame inicial (usado no Expand)
166) -> str | None:
167 """
168 Envia imagem + prompt ao servidor Modal e aguarda o vídeo gerado.
169 Quando first_frame_path é fornecido, o vídeo começa exatamente desse frame.
170 Retorna o caminho local do .mp4 gerado, ou None em caso de erro.
171 """
172 print(f"[MiniMaxAPI] Processando imagem local: {image_path}")
173 with open(image_path, "rb") as f:
174 image_b64 = base64.b64encode(f.read()).decode()
175
176 # Se houver first_frame, encodar também
177 first_frame_b64 = ""
178 if first_frame_path and os.path.exists(first_frame_path):
179 with open(first_frame_path, "rb") as f:
180 first_frame_b64 = base64.b64encode(f.read()).decode()
181 print(f"[MiniMaxAPI] 🎬 First frame carregado: {first_frame_path}")
182
183 image2_b64 = ""
184 if image_path2 and os.path.exists(image_path2):
185 with open(image_path2, "rb") as f2:
186 image2_b64 = base64.b64encode(f2.read()).decode()
187
188 payload = {
189 "mode": "generate",
190 "steps": 25,
191 "image_b64": image_b64,
192 "image2_b64": image2_b64,
193 "first_frame_b64": first_frame_b64,
194 "prompt": prompt_text,
195 "lora_name": lora_name,
196 "lora_strength": lora_strength,
197 "bot_token": bot_token,
198 "chat_id": chat_id,
199 "message_id": message_id,
200 "duration": duration,
201 "mp": mp,
202 }
203
204 is_cold = _minimax_cold_start()
205 timeout_total = 1800 if is_cold else 1500
206 print(f"[MiniMaxAPI] {'🥶 Cold start' if is_cold else '🔥 Warm start'} — timeout: {timeout_total}s")
207
208 try:
209 session = await get_http_session()
210 async with asyncio.timeout(timeout_total):
211 print(f"[MiniMaxAPI] Enviando para {MINIMAX_API_URL}...")
212 async with session.post(MINIMAX_API_URL, json=payload) as resp:
213 if resp.status != 200:
214 body = await resp.text()
215 print(f"[MiniMaxAPI] ❌ HTTP {resp.status}: {body[:300]}")
216 return None
217
218 data = await resp.json()
219
220 if data.get("status") == "error":
221 print(f"[MiniMaxAPI] ❌ Erro na API: {data.get('message')}")
222 return None
223 elif data.get("status") == "success":
224 video_b64 = data.get("video_b64", "")
225 if not video_b64:
226 print("[MiniMaxAPI] ❌ video_b64 vazio na resposta")
227 return None
228
229 print(f"[MiniMaxAPI] ✅ Vídeo recebido! ({len(video_b64)} chars)")
230
231 fd, local_path = tempfile.mkstemp(
232 suffix=".mp4", prefix="pornvideo_", dir=OUTPUT_DIR
233 )
234 os.close(fd)
235
236 with open(local_path, "wb") as f:
237 f.write(base64.b64decode(video_b64))
238
239 _save_minimax_stamp()
240 print(f"[MiniMaxAPI] ✅ Vídeo salvo: {local_path}")
241 return local_path
242 return None
243
244 except asyncio.TimeoutError:
245 print(f"[MiniMaxAPI] ⏰ Timeout após {timeout_total}s")
246 return None
247 except Exception as e:
248 print(f"[MiniMaxAPI] ❌ Exceção: {e}")
249 return None
250
251async def gerar_video_referencia_async(
252 image_path: str,
253 video_path: str,
254 prompt_text: str = "",
255 bot_token: str = "",
256 chat_id: int = 0,
257 message_id: int = 0,
258) -> str | None:
259 """
260 Envia a imagem e o vídeo de referência para o servidor Modal e aguarda o vídeo gerado.
261 Retorna o caminho local do .mp4 gerado, ou None em caso de erro.
262 """
263 print(f"[VideoAPI-Ref] Processando imagem: {image_path} | video: {video_path}")
264 with open(image_path, "rb") as f:
265 image_b64 = base64.b64encode(f.read()).decode()
266
267 with open(video_path, "rb") as f:
268 video_b64 = base64.b64encode(f.read()).decode()
269
270 payload = {
271 "image_b64": image_b64,
272 "video_b64": video_b64,
273 "prompt": prompt_text,
274 "bot_token": bot_token,
275 "chat_id": chat_id,
276 "message_id": message_id,
277 }
278
279 is_cold = _cold_start()
280 timeout_total = 2000 if is_cold else 1800
281 print(f"[VideoAPI-Ref] {'🥶 Cold start' if is_cold else '🔥 Warm start'} — timeout: {timeout_total}s")
282
283 try:
284 session = await get_http_session()
285 async with asyncio.timeout(timeout_total):
286 print(f"[VideoAPI-Ref] Enviando para {ANIMATE_API_URL}...")
287 async with session.post(ANIMATE_API_URL, json=payload) as resp:
288 if resp.status != 200:
289 body = await resp.text()
290 print(f"[VideoAPI-Ref] ❌ HTTP {resp.status}: {body[:300]}")
291 return None
292
293 data = await resp.json()
294
295 if data.get("status") == "error":
296 print(f"[VideoAPI-Ref] ❌ Erro na API: {data.get('message')}")
297 return None
298 elif data.get("status") == "success":
299 video_out_b64 = data.get("video_b64", "")
300 if not video_out_b64:
301 print("[VideoAPI-Ref] ❌ video_b64 vazio na resposta")
302 return None
303
304 print(f"[VideoAPI-Ref] ✅ Vídeo recebido! ({len(video_out_b64)} chars)")
305
306 fd, local_path = tempfile.mkstemp(suffix=".mp4", prefix="nakedrefvideo_", dir=OUTPUT_DIR)
307 os.close(fd)
308
309 with open(local_path, "wb") as f:
310 f.write(base64.b64decode(video_out_b64))
311
312 _save_stamp()
313 print(f"[VideoAPI-Ref] ✅ Vídeo salvo: {local_path}")
314 return local_path
315 return None
316
317 except asyncio.TimeoutError:
318 print(f"[VideoAPI-Ref] ⏰ Timeout após {timeout_total}s")
319 return None
320 except Exception as e:
321 print(f"[VideoAPI-Ref] ❌ Exceção: {e}")
322 return None
323
324async def expand_video_async(
325 video_path: str,
326 prompt_text: str = "",
327 length_frames: int = 153,
328 resolution: int = 1080,
329 bot_token: str = "",
330 chat_id: int = 0,
331 message_id: int = 0,
332 preset_name: str = "custom",
333 duration_seconds: float = 10.0,
334 continuation_mode: str = "guide",
335 context_length: int = 22,
336 steps: int = 20,
337) -> str | None:
338 """Expande um MP4 usando a cauda temporal como contexto MiniMax H3."""
339 print(f"[MiniMaxContext-Expand] Expandindo video local: {video_path}\n[MiniMaxContext-Expand] Prompt final: {prompt_text}")
340 if not os.path.isfile(video_path) or os.path.getsize(video_path) == 0:
341 print("[MiniMaxContext-Expand] Video original ausente ou vazio")
342 return None
343 try:
344 with open(video_path, "rb") as source:
345 video_b64 = base64.b64encode(source.read()).decode("ascii")
346 payload = {
347 "video_b64": video_b64,
348 "prompt": prompt_text,
349 "duration": float(duration_seconds),
350 "continuation_mode": continuation_mode,
351 "context_length": int(context_length),
352 "steps": int(steps),
353 "prepend_original": True,
354 "run_name": f"user_{chat_id or 'web'}_{int(time.time())}_{os.urandom(4).hex()}",
355 }
356 session = await get_http_session()
357 async with asyncio.timeout(1800):
358 print(f"[MiniMaxContext-Expand] Enviando para {MINIMAX_EXTEND_API_URL}")
359 async with session.post(MINIMAX_EXTEND_API_URL, json=payload) as response:
360 if response.status != 200:
361 error = await response.text()
362 print(f"[MiniMaxContext-Expand] HTTP {response.status}: {error[:500]}")
363 return None
364 data = await response.json()
365 if data.get("status") != "success" or not data.get("video_b64"):
366 print(f"[MiniMaxContext-Expand] API falhou: {data.get('message', data)}")
367 return None
368 fd, result_path = tempfile.mkstemp(
369 suffix=".mp4", prefix="pornvideo_ext_", dir=OUTPUT_DIR
370 )
371 os.close(fd)
372 with open(result_path, "wb") as result:
373 result.write(base64.b64decode(data["video_b64"]))
374 _save_stamp()
375 print(f"[MiniMaxContext-Expand] Video contextual salvo: {result_path}")
376 return result_path
377 except asyncio.TimeoutError:
378 print("[MiniMaxContext-Expand] Timeout apos 1800s")
379 return None
380 except Exception as exc:
381 print(f"[MiniMaxContext-Expand] Excecao: {type(exc).__name__}: {exc}")
382 return None
383
384
385async def _expand_video_legacy(
386 video_path: str,
387 prompt_text: str = "",
388 length_frames: int = 153,
389 resolution: int = 1080,
390 bot_token: str = "",
391 chat_id: int = 0,
392 message_id: int = 0,
393 preset_name: str = "custom",
394) -> str | None:
395 """
396 Envia o vídeo para o servidor Modal e aguarda o vídeo expandido.
397 """
398 print(f"[VideoAPI] Expandindo vídeo local: {video_path}")
399 with open(video_path, "rb") as f:
400 video_b64 = base64.b64encode(f.read()).decode()
401
402 payload = {
403 "mode": "extend",
404 "video_b64": video_b64,
405 "prompt": prompt_text,
406 "length": length_frames,
407 "resolution": resolution,
408 "bot_token": bot_token,
409 "chat_id": chat_id,
410 "message_id": message_id,
411 "preset_name": preset_name,
412 }
413
414 is_cold = _cold_start()
415 timeout_total = 1800 if is_cold else 1500
416 print(f"[VideoAPI] {'🥶 Cold start' if is_cold else '🔥 Warm start'} — timeout: {timeout_total}s")
417
418 try:
419 session = await get_http_session()
420 async with asyncio.timeout(timeout_total):
421 print(f"[VideoAPI] Enviando para {VIDEO_API_URL}...")
422 async with session.post(VIDEO_API_URL, json=payload) as resp:
423 if resp.status != 200:
424 body = await resp.text()
425 print(f"[VideoAPI] ❌ HTTP {resp.status}: {body[:300]}")
426 return None
427
428 data = await resp.json()
429
430 if data.get("status") == "error":
431 print(f"[VideoAPI] ❌ Erro na API: {data.get('message')}")
432 return None
433 elif data.get("status") == "success":
434 video_out_b64 = data.get("video_b64", "")
435 if not video_out_b64:
436 print("[VideoAPI] ❌ video_b64 vazio na resposta")
437 return None
438
439 print(f"[VideoAPI] ✅ Vídeo expandido recebido! ({len(video_out_b64)} chars)")
440
441 fd, local_path = tempfile.mkstemp(suffix=".mp4", prefix="nakedvideo_ext_", dir=OUTPUT_DIR)
442 os.close(fd)
443
444 with open(local_path, "wb") as f:
445 f.write(base64.b64decode(video_out_b64))
446
447 _save_stamp()
448 print(f"[VideoAPI] ✅ Vídeo Expandido salvo: {local_path}")
449 return local_path
450 return None
451
452 except asyncio.TimeoutError:
453 print(f"[VideoAPI] ⏰ Timeout após {timeout_total}s")
454 return None
455 except Exception as e:
456 print(f"[VideoAPI] ❌ Exceção: {e}")
457 return None
458
459
460
461async def upscale_video_async(video_path: str, api_key: str = "", target_resolution: str = "1080p") -> str | None:
462 """
463 Aplica FlashVSR 2x Upscale diretamente na GPU do Modal.
464 """
465 import base64
466 import asyncio
467 import uuid
468 import time
469 import json
470
471 if not os.path.isfile(video_path):
472 print(f"[VideoUpscaler] Arquivo de vídeo não encontrado: {video_path}")
473 return None
474
475 MINIMAX_UPSCALE_URL = "https://uch2f82placa--minimax-api-v1-minimaxworker-upscale-video.modal.run"
476
477 try:
478 print(f"[VideoUpscaler] 🚀 Enviando {video_path} para FlashVSR no Modal ({MINIMAX_UPSCALE_URL})...")
479 with open(video_path, "rb") as f:
480 video_b64 = base64.b64encode(f.read()).decode("utf-8")
481
482 session = await get_http_session()
483 payload = {"video_b64": video_b64}
484
485 async with asyncio.timeout(600):
486 async with session.post(MINIMAX_UPSCALE_URL, json=payload) as resp:
487 if resp.status != 200:
488 err_text = await resp.text()
489 print(f"[VideoUpscaler] ❌ HTTP {resp.status}: {err_text[:300]}")
490 return None
491
492 data = await resp.json()
493 if data.get("status") != "success" or not data.get("video_b64"):
494 print(f"[VideoUpscaler] ❌ Erro da API Modal: {data.get('message', 'Sem video_b64')}")
495 return None
496
497 out_bytes = base64.b64decode(data["video_b64"])
498 final_name = f"upscaled_{int(time.time())}_{uuid.uuid4().hex[:8]}.mp4"
499 final_path = os.path.join(os.path.dirname(video_path), final_name)
500 with open(final_path, "wb") as out_f:
501 out_f.write(out_bytes)
502 print(f"[VideoUpscaler] ✅ Vídeo upscalado 2x salvo em: {final_path}")
503 return final_path
504 except Exception as e:
505 print(f"[VideoUpscaler] ❌ Exceção ao upscalar vídeo no Modal: {e}")
506 return None
507
508
509
510async def expand_minimax_video_async(
511 video_path: str,
512 prompt_text: str = "The person moves naturally",
513 duration: float = 10.0,
514 bot_token: str = "",
515 chat_id: int = 0,
516 message_id: int = 0,
517 mp: float = 0.5,
518) -> str | None:
519 """
520 Uses the minimax-h3-extend endpoint to seamlessly expand an existing video.
521 """
522 import base64
523 import asyncio
524 import uuid
525 import json
526 import os
527
528 MINIMAX_EXTEND_URL = "https://uch2f82placa--minimax-h3-extend-minimaxh3extender-extend.modal.run"
529
530 print(f"[MiniMaxExpandAPI] Processando video local: {video_path}")
531 with open(video_path, "rb") as f:
532 video_b64 = base64.b64encode(f.read()).decode()
533
534 payload = {
535 "video_b64": video_b64,
536 "prompt": prompt_text,
537 "duration": duration,
538 "continuation_mode": "masked_av",
539 "context_length": 39,
540 "steps": 20,
541 "prepend_original": True,
542 "run_name": f"expand_tg_{uuid.uuid4().hex[:8]}",
543 "bot_token": bot_token,
544 "chat_id": chat_id,
545 "message_id": message_id,
546 "mp": mp,
547 }
548
549 try:
550 session = await get_http_session()
551 async with asyncio.timeout(1800):
552 print(f"[MiniMaxExpandAPI] Enviando para {MINIMAX_EXTEND_URL}...")
553 async with session.post(MINIMAX_EXTEND_URL, json=payload) as resp:
554 if resp.status != 200:
555 body = await resp.text()
556 print(f"[MiniMaxExpandAPI] HTTP {resp.status}: {body[:300]}")
557 return None
558 data = await resp.json()
559 if data.get("status") != "success" or not data.get("video_b64"):
560 print(f"[MiniMaxExpandAPI] Erro na API: {data.get('message', 'Sem resposta de video')}")
561 return None
562
563 output_bytes = base64.b64decode(data["video_b64"])
564 _video_outputs_dir = os.path.join(os.path.dirname(__file__), "video_outputs")
565 os.makedirs(_video_outputs_dir, exist_ok=True)
566 out_path = os.path.join(_video_outputs_dir, f"pornvideo_expanded_{uuid.uuid4().hex[:8]}.mp4")
567 with open(out_path, "wb") as f:
568 f.write(output_bytes)
569 print(f"[MiniMaxExpandAPI] Video expandido salvo em {out_path}")
570 return out_path
571 except Exception as e:
572 print(f"[MiniMaxExpandAPI] Erro interno: {e}")
573 return None
574
575
576# ── URLs do servidor Modal (Face Swap em Vídeo com MaskVid e GFPGAN HD) ─────────
577FACESWAP_ASYNC_START_URL = os.getenv(
578 "FACESWAP_ASYNC_START_URL",
579 "https://uch2f82placa--maskvid-faceswap-maskvidworker-api-swap-start.modal.run"
580)
581FACESWAP_ASYNC_STATUS_URL = os.getenv(
582 "FACESWAP_ASYNC_STATUS_URL",
583 "https://uch2f82placa--maskvid-faceswap-maskvidworker-api-swap-status.modal.run"
584)
585FACESWAP_VIDEO_API_URL = os.getenv(
586 "FACESWAP_VIDEO_API_URL",
587 "https://uch2f82placa--maskvid-faceswap-maskvidworker-api-swap.modal.run"
588)
589
590
591async def gerar_video_faceswap_async(
592 image_path: str,
593 video_path: str,
594 enhance: bool = True,
595 enhance_weight: float = 0.90,
596 megapixels: float = 1.0,
597 crop_scale: float = 2.2,
598) -> str | None:
599 """
600 Envia a foto de origem e o vídeo de destino para o servidor Modal Face Swap.
601 Executa MaskVid (rastreamento temporal sem jitter) + GFPGAN HD (restauração facial).
602 Retorna o caminho do arquivo .mp4 salvo localmente ou None em caso de falha.
603 """
604 print(f"[FaceSwapVideoAPI] Iniciando Face Swap em Vídeo...")
605 print(f" Foto: {image_path}")
606 print(f" Vídeo: {video_path}")
607
608 if not os.path.exists(image_path) or not os.path.exists(video_path):
609 print(f"[FaceSwapVideoAPI] ❌ Arquivos de entrada não encontrados.")
610 return None
611
612 # 1. Tentativa prioritária via Modal SDK direto (se configurado)
613 try:
614 import modal
615 cls = modal.Cls.from_name("maskvid-faceswap", "MaskVidWorker")
616 worker = cls()
617 with open(image_path, "rb") as f_img:
618 src_bytes = f_img.read()
619 with open(video_path, "rb") as f_vid:
620 tgt_bytes = f_vid.read()
621 print(f"[FaceSwapVideoAPI] 🚀 Executando via Modal SDK direto...")
622 res_bytes = await worker.process_face_swap.remote.aio(
623 source_image_bytes=src_bytes,
624 target_video_bytes=tgt_bytes,
625 crop_scale=crop_scale,
626 enhance=enhance,
627 enhance_weight=enhance_weight,
628 megapixels=megapixels,
629 )
630 if res_bytes and len(res_bytes) > 1000:
631 out_filename = f"faceswap_video_{uuid.uuid4().hex[:12]}.mp4"
632 out_path = os.path.join(OUTPUT_DIR, out_filename)
633 with open(out_path, "wb") as f_out:
634 f_out.write(res_bytes)
635 print(f"[FaceSwapVideoAPI] ✅ Vídeo gerado com sucesso via Modal SDK: {out_path} ({len(res_bytes)} bytes)")
636 return out_path
637 except Exception as sdk_err:
638 print(f"[FaceSwapVideoAPI] ℹ️ Modal SDK direto indisponível ({sdk_err}), usando API HTTP...")
639
640 with open(image_path, "rb") as f_img:
641 source_b64 = base64.b64encode(f_img.read()).decode("utf-8")
642 with open(video_path, "rb") as f_vid:
643 target_b64 = base64.b64encode(f_vid.read()).decode("utf-8")
644
645 payload = {
646 "source_image": source_b64,
647 "target_video": target_b64,
648 "enhance": enhance,
649 "enhance_weight": enhance_weight,
650 "megapixels": megapixels,
651 "crop_scale": crop_scale,
652 }
653
654 # 2. Fluxo Principal: Despacho Assíncrono com Polling de Status (100% livre de timeouts de proxy)
655 try:
656 session = await get_http_session()
657 print(f"[FaceSwapVideoAPI] 📤 Despachando tarefa para {FACESWAP_ASYNC_START_URL}...")
658 async with asyncio.timeout(60):
659 async with session.post(FACESWAP_ASYNC_START_URL, json=payload) as start_resp:
660 if start_resp.status == 200:
661 start_data = await start_resp.json()
662 job_id = start_data.get("job_id")
663 if job_id:
664 print(f"[FaceSwapVideoAPI] ⏳ Job registrado com sucesso (ID: {job_id})! Monitorando progresso...")
665 # Polling a cada 4 segundos até 600 segundos (10 minutos)
666 for attempt in range(150):
667 await asyncio.sleep(4)
668 try:
669 async with session.get(f"{FACESWAP_ASYNC_STATUS_URL}?job_id={job_id}") as status_resp:
670 if status_resp.status == 200:
671 status_data = await status_resp.json()
672 st = status_data.get("status")
673 if st == "success" and status_data.get("video_base64"):
674 video_bytes = base64.b64decode(status_data["video_base64"])
675 out_filename = f"faceswap_video_{uuid.uuid4().hex[:12]}.mp4"
676 out_path = os.path.join(OUTPUT_DIR, out_filename)
677 with open(out_path, "wb") as f_out:
678 f_out.write(video_bytes)
679 print(f"[FaceSwapVideoAPI] 🎉 Vídeo gerado e recebido com sucesso: {out_path} ({len(video_bytes)} bytes)")
680 return out_path
681 elif st == "error":
682 print(f"[FaceSwapVideoAPI] ❌ Erro reportado pelo worker: {status_data.get('message')}")
683 break
684 except Exception as poll_err:
685 print(f"[FaceSwapVideoAPI] ⚠️ Aviso no polling do job {job_id}: {poll_err}")
686 continue
687 except Exception as async_err:
688 print(f"[FaceSwapVideoAPI] ⚠️ Erro no despacho assíncrono: {async_err}")
689
690 # 3. Fallback: Requisição Síncrona Tradicional
691 endpoints = [
692 FACESWAP_VIDEO_API_URL,
693 "https://uch2f82placa--maskvid-faceswap-maskvidworker-api-swap.modal.run",
694 ]
695
696 try:
697 session = await get_http_session()
698 for ep in endpoints:
699 try:
700 print(f"[FaceSwapVideoAPI] Enviando requisição síncrona para {ep}...")
701 async with asyncio.timeout(600):
702 async with session.post(ep, json=payload) as resp:
703 if resp.status == 200:
704 data = await resp.json()
705 if data.get("status") == "success" and data.get("video_base64"):
706 video_bytes = base64.b64decode(data["video_base64"])
707 out_filename = f"faceswap_video_{uuid.uuid4().hex[:12]}.mp4"
708 out_path = os.path.join(OUTPUT_DIR, out_filename)
709 with open(out_path, "wb") as f_out:
710 f_out.write(video_bytes)
711 print(f"[FaceSwapVideoAPI] ✅ Vídeo gerado com sucesso: {out_path} ({len(video_bytes)} bytes)")
712 return out_path
713 except Exception as ep_err:
714 print(f"[FaceSwapVideoAPI] ⚠️ Tentativa síncrona falhou: {ep_err}")
715 continue
716
717 # Fallback: se os endpoints HTTP falharem, executa via Modal CLI / Python Runner
718 print("[FaceSwapVideoAPI] Tentando fallback via modal run...")
719 script_path = r"E:\Wan2.2\maskvid_modal.py"
720 out_filename = f"faceswap_video_{uuid.uuid4().hex[:12]}.mp4"
721 out_path = os.path.join(OUTPUT_DIR, out_filename)
722
723 cmd = [
724 "python", "-m", "modal", "run", script_path,
725 "--source", image_path,
726 "--target", video_path,
727 "--output", out_path,
728 "--enhance-weight", str(enhance_weight),
729 "--megapixels", str(megapixels),
730 "--crop-scale", str(crop_scale),
731 ]
732 proc = await asyncio.create_subprocess_exec(
733 *cmd,
734 stdout=asyncio.subprocess.PIPE,
735 stderr=asyncio.subprocess.PIPE,
736 )
737 stdout, stderr = await proc.communicate()
738 if proc.returncode == 0 and os.path.exists(out_path) and os.path.getsize(out_path) > 1000:
739 print(f"[FaceSwapVideoAPI] ✅ Fallback Modal CLI concluído com sucesso: {out_path}")
740 return out_path
741 else:
742 print(f"[FaceSwapVideoAPI] ❌ Fallback Modal CLI falhou ({proc.returncode}): {stderr.decode()[:300]}")
743 return None
744
745 except Exception as e:
746 print(f"[FaceSwapVideoAPI] ❌ Erro geral em gerar_video_faceswap_async: {e}")
747 return None
748
749 