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