CoolFace
Datasetpublic

mrdarkbr/wan22-code-backup

sourceHugging Faceupdated 28d agoView on Hugging Face
0likes55downloads
video_api_client.py749 linesDownload Raw Back to root
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