Spaces:
Running on Zero
Running on Zero
Download modelos/ltx.py from ConectaPrimoAI/conecta-primo-gpu: direct link, hf CLI and curl.
- Browser
- Download file 7.83 kB
-
https://huggingface.co/spaces/ConectaPrimoAI/conecta-primo-gpu/resolve/main/modelos/ltx.py
- Command line
-
hf download hf://spaces/ConectaPrimoAI/conecta-primo-gpu/modelos/ltx.py
-
curl -L -o ltx.py https://huggingface.co/spaces/ConectaPrimoAI/conecta-primo-gpu/resolve/main/modelos/ltx.py
7.83 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| VÍDEO — a família LTX-2 da Lightricks (LTX-2, LTX-2.3, LTX-2.5), via diffusers. | |
| Pedido do dono: "quero um modelo de vídeo muito bom". Este é o caminho da | |
| qualidade; o Wan 2.2 continua sendo o caminho do custo zero, e qual dos dois | |
| responde é o `VIDEO_MODEL` — o `escolha.py` manda pela FAMÍLIA do id. | |
| ── O QUE O LEVANTAMENTO MOSTROU (Hub, 2026-09-15) ────────────────────────── | |
| Perguntei ao Hub quem lidera image-to-video em diffusers, em vez de escolher | |
| de memória. A ordem é: `Lightricks/LTX-2.5` (1,58 milhão de downloads, 3.932 | |
| curtidas), `Lightricks/LTX-2.3` (1,12 milhão), `Lightricks/LTX-Video` (762 | |
| mil). O Wan 2.2 TI2V-5B que está publicado aqui aparece com 172 mil. Não é | |
| empate: a família LTX-2 é a escolha da comunidade por uma ordem de grandeza. | |
| E ela NÃO é de graça, e este é o ponto que decide tudo: | |
| LTX-2.5 200,9 GB | |
| LTX-2.3 156,0 GB | |
| LTX-2.3-fp8 58,7 GB <- o único que chega perto de caber | |
| Wan2.2-TI2V-5B 34,2 GB <- o padrão de hoje | |
| Num Space ZeroGPU gratuito o disco é efêmero: cada vez que ele dorme, os | |
| pesos VOLTAM a ser baixados. 34 GB já são minutos; 200 GB não é lentidão, é | |
| um recurso que nunca termina de subir. Por isso o padrão do registro CONTINUA | |
| sendo o Wan — trocá-lo por decisão minha entregaria um vídeo que não gera. | |
| ── ENTÃO POR QUE ESTE ARQUIVO EXISTE AGORA ───────────────────────────────── | |
| Porque antes ele era um ESQUELETO que levantava `NotImplementedError`, e o | |
| `escolha.py` passou a rotear a família LTX para cá: apontar o `VIDEO_MODEL` | |
| para um LTX daria um erro seco em vez de um vídeo. Um caminho que o roteamento | |
| alcança tem de funcionar. | |
| Com uma GPU dedicada (ver a tabela de custo em `docs/ZEROGPU.md`), o dono | |
| troca UMA variável — `VIDEO_MODEL=Lightricks/LTX-2.3-fp8` — e o vídeo bom | |
| entra sem deploy nenhum. É para isso que o registro existe. | |
| ── O QUE NÃO FOI VALIDADO, E É HONESTO DIZER ─────────────────────────────── | |
| A inferência NÃO rodou: não há GPU nem pesos neste ambiente, e baixar 58 GB | |
| para provar uma chamada não caberia aqui de qualquer jeito. O que está | |
| provado é o que dá para provar: que os ids existem, que o `model_index.json` | |
| do LTX-2 declara `LTX2Pipeline`, e o roteamento por família | |
| (`t_modelo_imagem.py`). O que conferir no primeiro deploy com GPU está | |
| nomeado em `docs/MODELS.md`. | |
| """ | |
| from __future__ import annotations | |
| import base64 as b64 | |
| import io | |
| import logging | |
| import os | |
| import tempfile | |
| from typing import Dict, Optional | |
| from . import base | |
| logger = logging.getLogger("worker.ltx") | |
| VRAM_MINIMA_GB = 20 | |
| _pipe = None | |
| _modelo_carregado = "" | |
| def _modelo() -> str: | |
| return os.getenv("VIDEO_MODEL", "Lightricks/LTX-2.3-fp8").strip() | |
| def baixar(progresso: base.Progresso) -> None: | |
| base.baixar_repo(_modelo(), progresso) | |
| def carregar(progresso: base.Progresso) -> None: | |
| global _pipe, _modelo_carregado | |
| if base.modo_mock(): | |
| _pipe = "mock" | |
| return | |
| if _pipe is not None and _modelo_carregado == _modelo(): | |
| return | |
| progresso(None, "Carregando o modelo de vídeo…") | |
| import torch | |
| from diffusers import DiffusionPipeline | |
| # `DiffusionPipeline` resolve a classe pelo `model_index.json` do próprio | |
| # repositório (`LTX2Pipeline` no LTX-2). Importar a classe por nome | |
| # quebraria com ImportError num diffusers que a renomeasse, e prenderia | |
| # este arquivo a UMA versão da família — o oposto do que ele existe para | |
| # fazer. | |
| _pipe = DiffusionPipeline.from_pretrained( | |
| _modelo(), torch_dtype=torch.bfloat16, | |
| token=os.getenv("HF_TOKEN") or None) | |
| # Vídeo é o maior dos quatro: offload SEMPRE, mesmo com VRAM folgada — o | |
| # pico do VAE de vídeo no decode é traiçoeiro e é onde o OOM mora. É a | |
| # mesma decisão do wan22.py, e pelo mesmo motivo medido. | |
| _pipe.enable_model_cpu_offload() | |
| _modelo_carregado = _modelo() | |
| def gerar(params: Dict, progresso: base.Progresso) -> base.Saida: | |
| if base.modo_mock(): | |
| return b"", "video/mp4" | |
| prompt = (params.get("prompt") or "").strip() | |
| estilo = (params.get("style") or "").strip() | |
| if estilo: | |
| prompt = f"{prompt}, {estilo} style" | |
| largura = int(params.get("width") or 1280) | |
| altura = int(params.get("height") or 704) | |
| segundos = max(2, min(int(params.get("duration") or 5), | |
| int(os.getenv("VIDEO_MAX_SEGUNDOS", "8")))) | |
| fps = int(params.get("fps") or 24) | |
| # O LTX quer contagem de quadros em 8n+1 (o VAE temporal comprime de 8 em | |
| # 8, e um resto sobrando sai como um quadro final corrompido). A conta | |
| # ARREDONDA PARA BAIXO: passar do que foi pedido custaria prazo de GPU num | |
| # modelo que já é o mais caro dos quatro. | |
| quadros = max(9, ((segundos * fps) // 8) * 8 + 1) | |
| passos = int(params.get("steps") or 30) | |
| imagem = None | |
| if params.get("image_url"): | |
| imagem = _abrir_imagem(params["image_url"], largura, altura) | |
| def a_cada_passo(pipe, passo, t, kw): | |
| progresso(int(95 * (passo + 1) / max(1, passos)), "Gerando o vídeo…") | |
| return kw | |
| extras = {"image": imagem} if imagem is not None else {} | |
| saida = _pipe(prompt=prompt, | |
| negative_prompt=params.get("negative_prompt") or None, | |
| width=largura, height=altura, num_frames=quadros, | |
| frame_rate=fps, | |
| num_inference_steps=passos, | |
| generator=base.semente(params), | |
| callback_on_step_end=a_cada_passo, **extras) | |
| return _para_mp4(saida.frames[0], fps), "video/mp4" | |
| def descarregar() -> None: | |
| global _pipe | |
| _pipe = None | |
| base.limpar_vram() | |
| def config_menor(params: Dict) -> Optional[Dict]: | |
| """Depois de um OOM: menos pixels, depois menos tempo, depois menos passos. | |
| A ORDEM é a do wan22.py e não é arbitrária — no vídeo o pico de memória é | |
| quadros × pixels, então cortar resolução rende mais que cortar passos, e | |
| cortar passos é o que mais estraga a imagem. | |
| """ | |
| p = dict(params) | |
| w = int(p.get("width") or 1280) | |
| if w > 704: | |
| p["width"] = 704 | |
| p["height"] = max(384, int((p.get("height") or 704) * 704 // max(1, w))) | |
| return p | |
| seg = int(p.get("duration") or 5) | |
| if seg > 3: | |
| p["duration"] = 3 | |
| return p | |
| passos = int(p.get("steps") or 30) | |
| if passos > 20: | |
| p["steps"] = 20 | |
| return p | |
| return None | |
| def _abrir_imagem(dado: str, largura: int, altura: int): | |
| """A primeira imagem do image-to-video. Aceita data URL e endereço, que | |
| são as duas formas em que ela chega do app.""" | |
| from PIL import Image | |
| if dado.startswith("data:"): | |
| bruto = b64.b64decode(dado.split(",", 1)[1]) | |
| else: | |
| import requests | |
| bruto = requests.get(dado, timeout=30).content | |
| img = Image.open(io.BytesIO(bruto)).convert("RGB") | |
| return img.resize((largura, altura)) | |
| def _para_mp4(quadros, fps: int) -> bytes: | |
| """Os quadros viram um MP4 em arquivo temporário. | |
| `export_to_video` do diffusers escreve em disco e não devolve bytes, e o | |
| contrato desta casa é bytes — quem guarda é o `armazenamento.py`, que | |
| pode estar num bucket. O temporário morre aqui. | |
| """ | |
| from diffusers.utils import export_to_video | |
| with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f: | |
| caminho = f.name | |
| try: | |
| export_to_video(quadros, caminho, fps=fps) | |
| with open(caminho, "rb") as f: | |
| return f.read() | |
| finally: | |
| try: | |
| os.unlink(caminho) | |
| except OSError: | |
| pass | |