| |
| """ |
| ComfyUI Hugging Face Space |
| Versão otimizada para execução gratuita no Hugging Face Spaces |
| """ |
|
|
| import subprocess |
| import os |
| import time |
| import socket |
| import threading |
| import logging |
| import json |
| import gradio as gr |
| from pathlib import Path |
| from typing import Dict, List, Optional, Any |
| import psutil |
| import signal |
| import sys |
| import requests |
| from datetime import datetime |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', |
| handlers=[ |
| logging.StreamHandler(sys.stdout), |
| logging.FileHandler('/tmp/comfyui.log') |
| ] |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| class ComfyUISpaceManager: |
| """Gerenciador principal do ComfyUI no Hugging Face Space""" |
| |
| def __init__(self): |
| self.app_dir = Path("/tmp/ComfyUI") |
| self.models_dir = Path("/data/models") |
| self.port = 7860 |
| self.comfy_port = 8188 |
| self.process = None |
| self.setup_complete = False |
| self.setup_logs = [] |
| |
| |
| self.app_dir.mkdir(parents=True, exist_ok=True) |
| self.models_dir.mkdir(parents=True, exist_ok=True) |
| |
| def log_setup(self, message: str, level: str = "info"): |
| """Adiciona mensagem aos logs de setup""" |
| timestamp = datetime.now().strftime("%H:%M:%S") |
| log_entry = f"[{timestamp}] {message}" |
| self.setup_logs.append(log_entry) |
| |
| if level == "info": |
| logger.info(message) |
| elif level == "warning": |
| logger.warning(message) |
| elif level == "error": |
| logger.error(message) |
| |
| |
| if len(self.setup_logs) > 100: |
| self.setup_logs = self.setup_logs[-100:] |
| |
| def get_hf_token(self) -> str: |
| """Obtém token do Hugging Face""" |
| token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_TOKEN") |
| if not token: |
| self.log_setup("⚠️ Token HF não encontrado, alguns modelos podem não baixar", "warning") |
| return "" |
| self.log_setup(f"✅ Token HF configurado: ...{token[-8:]}") |
| return token |
| |
| def setup_comfyui(self) -> bool: |
| """Setup completo do ComfyUI""" |
| try: |
| self.log_setup("🚀 Iniciando setup do ComfyUI...") |
| |
| |
| if not self._clone_repository(): |
| return False |
| |
| |
| if not self._install_dependencies(): |
| return False |
| |
| |
| if not self._setup_models(): |
| return False |
| |
| |
| if not self._setup_custom_nodes(): |
| return False |
| |
| self.log_setup("✅ Setup completo! ComfyUI pronto para usar") |
| self.setup_complete = True |
| return True |
| |
| except Exception as e: |
| self.log_setup(f"❌ Erro no setup: {e}", "error") |
| return False |
| |
| def _clone_repository(self) -> bool: |
| """Clona repositório do ComfyUI""" |
| if (self.app_dir / ".git").exists(): |
| self.log_setup("📁 Repositório já existe, atualizando...") |
| try: |
| os.chdir(self.app_dir) |
| subprocess.run(["git", "pull"], check=True, capture_output=True) |
| return True |
| except subprocess.CalledProcessError as e: |
| self.log_setup(f"⚠️ Erro ao atualizar repositório: {e}", "warning") |
| |
| |
| self.log_setup("📥 Clonando ComfyUI...") |
| try: |
| subprocess.run([ |
| "git", "clone", "https://github.com/comfyanonymous/ComfyUI.git", |
| str(self.app_dir) |
| ], check=True, capture_output=True) |
| self.log_setup("✅ ComfyUI clonado com sucesso") |
| return True |
| except subprocess.CalledProcessError as e: |
| self.log_setup(f"❌ Erro ao clonar ComfyUI: {e}", "error") |
| return False |
| |
| def _install_dependencies(self) -> bool: |
| """Instala dependências do ComfyUI""" |
| self.log_setup("📦 Instalando dependências...") |
| |
| |
| dependencies = [ |
| "torch>=2.0.0", |
| "torchvision", |
| "torchaudio", |
| "numpy<2.0", |
| "pillow", |
| "transformers>=4.28.1", |
| "accelerate", |
| "diffusers", |
| "huggingface-hub[hf_transfer]", |
| "safetensors>=0.4.2", |
| "aiohttp", |
| "psutil", |
| "opencv-python-headless" |
| ] |
| |
| for dep in dependencies: |
| try: |
| self.log_setup(f"📦 Instalando {dep}...") |
| subprocess.run([ |
| "pip", "install", "--upgrade", dep |
| ], check=True, capture_output=True) |
| except subprocess.CalledProcessError as e: |
| self.log_setup(f"⚠️ Falha ao instalar {dep}: {e}", "warning") |
| |
| |
| self.log_setup("✅ Dependências instaladas") |
| return True |
| |
| def _setup_models(self) -> bool: |
| """Setup de modelos com cache persistente""" |
| self.log_setup("🤖 Configurando modelos...") |
| |
| |
| models_exist = self._check_existing_models() |
| |
| if models_exist: |
| self.log_setup("✅ Modelos já existem no cache, vinculando...") |
| self._link_models() |
| return True |
| |
| |
| self.log_setup("📥 Primeira execução - baixando modelos essenciais...") |
| self._download_essential_models() |
| |
| return True |
| |
| def _check_existing_models(self) -> bool: |
| """Verifica se modelos já existem no storage persistente""" |
| essential_models = [ |
| "checkpoints/flux1-schnell.safetensors", |
| "vae/ae.safetensors", |
| "clip/clip_l.safetensors" |
| ] |
| |
| for model_path in essential_models: |
| full_path = self.models_dir / model_path |
| if not full_path.exists() or full_path.stat().st_size < 1024*1024: |
| return False |
| |
| return True |
| |
| def _link_models(self): |
| """Vincula modelos do storage persistente ao ComfyUI""" |
| comfy_models = self.app_dir / "models" |
| |
| |
| if comfy_models.exists(): |
| import shutil |
| shutil.rmtree(comfy_models) |
| |
| |
| comfy_models.symlink_to(self.models_dir) |
| self.log_setup("🔗 Modelos vinculados ao storage persistente") |
| |
| def _download_essential_models(self): |
| """Download de modelos essenciais""" |
| token = self.get_hf_token() |
| |
| |
| essential_models = [ |
| { |
| "repo": "black-forest-labs/FLUX.1-schnell", |
| "file": "flux1-schnell.safetensors", |
| "dir": "checkpoints" |
| }, |
| { |
| "repo": "black-forest-labs/FLUX.1-schnell", |
| "file": "ae.safetensors", |
| "dir": "vae" |
| }, |
| { |
| "repo": "openai/clip-vit-large-patch14", |
| "file": "pytorch_model.bin", |
| "dir": "clip", |
| "rename": "clip_l.safetensors" |
| } |
| ] |
| |
| for model in essential_models: |
| self._download_hf_model( |
| model["repo"], |
| model["file"], |
| model["dir"], |
| token, |
| model.get("rename") |
| ) |
| |
| |
| self._download_popular_loras() |
| |
| |
| self._link_models() |
| |
| def _download_hf_model(self, repo: str, filename: str, target_dir: str, token: str, rename: str = None): |
| """Download de modelo individual do HuggingFace""" |
| target_path = self.models_dir / target_dir |
| target_path.mkdir(parents=True, exist_ok=True) |
| |
| final_name = rename if rename else filename |
| file_path = target_path / final_name |
| |
| if file_path.exists(): |
| self.log_setup(f"✅ {final_name} já existe") |
| return |
| |
| self.log_setup(f"📥 Baixando {final_name} de {repo}...") |
| |
| try: |
| from huggingface_hub import hf_hub_download |
| |
| downloaded_path = hf_hub_download( |
| repo_id=repo, |
| filename=filename, |
| token=token if token else None, |
| cache_dir="/tmp/hf_cache" |
| ) |
| |
| |
| import shutil |
| shutil.copy2(downloaded_path, file_path) |
| |
| size_mb = file_path.stat().st_size / (1024*1024) |
| self.log_setup(f"✅ {final_name} baixado ({size_mb:.1f}MB)") |
| |
| except Exception as e: |
| self.log_setup(f"⚠️ Erro ao baixar {final_name}: {e}", "warning") |
| |
| def _download_popular_loras(self): |
| """Download de LoRAs populares e pequenos""" |
| loras_dir = self.models_dir / "loras" |
| loras_dir.mkdir(exist_ok=True) |
| |
| |
| popular_loras = [ |
| "https://huggingface.co/alvdansen/pony-realism/resolve/main/ponyRealismV21MainVAE.safetensors", |
| "https://huggingface.co/Hyper-SD/Hyper-FLUX.1-dev-8steps-lora/resolve/main/Hyper-FLUX.1-dev-8steps-lora.safetensors" |
| ] |
| |
| for url in popular_loras: |
| try: |
| filename = url.split("/")[-1] |
| file_path = loras_dir / filename |
| |
| if file_path.exists(): |
| continue |
| |
| self.log_setup(f"📥 Baixando LoRA {filename}...") |
| subprocess.run([ |
| "wget", "-q", "--timeout=60", url, "-O", str(file_path) |
| ], check=True) |
| |
| if file_path.stat().st_size > 1024: |
| self.log_setup(f"✅ LoRA {filename} baixado") |
| else: |
| file_path.unlink() |
| |
| except Exception as e: |
| self.log_setup(f"⚠️ Erro ao baixar LoRA: {e}", "warning") |
| |
| def _setup_custom_nodes(self) -> bool: |
| """Setup de custom nodes essenciais""" |
| self.log_setup("🔧 Configurando custom nodes...") |
| |
| custom_nodes_dir = self.app_dir / "custom_nodes" |
| custom_nodes_dir.mkdir(exist_ok=True) |
| |
| |
| essential_nodes = [ |
| "https://github.com/ltdrdata/ComfyUI-Manager.git", |
| "https://github.com/WASasquatch/was-node-suite-comfyui.git" |
| ] |
| |
| for repo_url in essential_nodes: |
| try: |
| repo_name = repo_url.split("/")[-1].replace(".git", "") |
| node_dir = custom_nodes_dir / repo_name |
| |
| if node_dir.exists(): |
| self.log_setup(f"✅ {repo_name} já existe") |
| continue |
| |
| self.log_setup(f"📥 Clonando {repo_name}...") |
| subprocess.run([ |
| "git", "clone", "--depth", "1", repo_url, str(node_dir) |
| ], check=True, capture_output=True, timeout=120) |
| |
| |
| req_file = node_dir / "requirements.txt" |
| if req_file.exists(): |
| subprocess.run([ |
| "pip", "install", "-r", str(req_file) |
| ], check=False, capture_output=True, timeout=120) |
| |
| self.log_setup(f"✅ {repo_name} instalado") |
| |
| except Exception as e: |
| self.log_setup(f"⚠️ Erro ao instalar {repo_name}: {e}", "warning") |
| |
| self.log_setup("✅ Custom nodes configurados") |
| return True |
| |
| def start_comfyui(self) -> bool: |
| """Inicia o servidor ComfyUI""" |
| if self.process and self.process.poll() is None: |
| self.log_setup("✅ ComfyUI já está rodando") |
| return True |
| |
| self.log_setup("🚀 Iniciando servidor ComfyUI...") |
| |
| try: |
| os.chdir(self.app_dir) |
| |
| |
| self.process = subprocess.Popen([ |
| "python", "main.py", |
| "--listen", "0.0.0.0", |
| "--port", str(self.comfy_port), |
| "--preview-method", "auto", |
| "--dont-print-server" |
| ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) |
| |
| |
| for attempt in range(60): |
| if self._test_comfyui_connection(): |
| self.log_setup("✅ ComfyUI está rodando!") |
| return True |
| |
| if self.process.poll() is not None: |
| stdout, stderr = self.process.communicate() |
| self.log_setup(f"❌ ComfyUI parou: {stderr}", "error") |
| return False |
| |
| time.sleep(2) |
| |
| self.log_setup("❌ Timeout aguardando ComfyUI", "error") |
| return False |
| |
| except Exception as e: |
| self.log_setup(f"❌ Erro ao iniciar ComfyUI: {e}", "error") |
| return False |
| |
| def _test_comfyui_connection(self) -> bool: |
| """Testa conexão com ComfyUI""" |
| try: |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| sock.settimeout(1) |
| return sock.connect_ex(('127.0.0.1', self.comfy_port)) == 0 |
| except: |
| return False |
| |
| def get_status(self) -> Dict[str, Any]: |
| """Retorna status do sistema""" |
| return { |
| "setup_complete": self.setup_complete, |
| "comfyui_running": self.process and self.process.poll() is None, |
| "comfyui_url": f"http://127.0.0.1:{self.comfy_port}" if self.setup_complete else None, |
| "logs": self.setup_logs[-20:], |
| "memory_usage": psutil.virtual_memory().percent, |
| "disk_usage": psutil.disk_usage('/').percent if os.path.exists('/') else 0, |
| "models_dir_size": self._get_dir_size(self.models_dir) |
| } |
| |
| def _get_dir_size(self, path: Path) -> str: |
| """Retorna tamanho do diretório formatado""" |
| try: |
| total_size = sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) |
| size_gb = total_size / (1024**3) |
| return f"{size_gb:.2f} GB" |
| except: |
| return "N/A" |
| |
| def stop_comfyui(self): |
| """Para o ComfyUI""" |
| if self.process: |
| self.log_setup("🛑 Parando ComfyUI...") |
| self.process.terminate() |
| try: |
| self.process.wait(timeout=10) |
| except subprocess.TimeoutExpired: |
| self.process.kill() |
| self.process = None |
|
|
| |
| comfy_manager = ComfyUISpaceManager() |
|
|
| def setup_comfyui(): |
| """Interface para setup do ComfyUI""" |
| if comfy_manager.setup_complete: |
| return "✅ Setup já completo!", comfy_manager.get_status() |
| |
| |
| def run_setup(): |
| comfy_manager.setup_comfyui() |
| comfy_manager.start_comfyui() |
| |
| setup_thread = threading.Thread(target=run_setup, daemon=True) |
| setup_thread.start() |
| |
| return "🚀 Setup iniciado! Acompanhe o progresso abaixo...", comfy_manager.get_status() |
|
|
| def get_status_update(): |
| """Atualização de status para interface""" |
| status = comfy_manager.get_status() |
| |
| |
| logs_text = "\n".join(status["logs"]) if status["logs"] else "Aguardando logs..." |
| |
| |
| if status["setup_complete"] and status["comfyui_running"]: |
| main_status = "🟢 ComfyUI Rodando - Pronto para usar!" |
| comfyui_link = f'<a href="http://127.0.0.1:{comfy_manager.comfy_port}" target="_blank">🎯 Abrir ComfyUI</a>' |
| elif status["setup_complete"]: |
| main_status = "🟡 Setup completo - Iniciando ComfyUI..." |
| comfyui_link = "⏳ Aguardando..." |
| else: |
| main_status = "🟡 Setup em progresso..." |
| comfyui_link = "⏳ Aguardando setup..." |
| |
| |
| system_info = f""" |
| 💾 Memória: {status['memory_usage']:.1f}% |
| 💿 Disco: {status['disk_usage']:.1f}% |
| 📁 Modelos: {status['models_dir_size']} |
| """ |
| |
| return main_status, logs_text, system_info, comfyui_link |
|
|
| def restart_comfyui(): |
| """Reinicia ComfyUI""" |
| comfy_manager.stop_comfyui() |
| time.sleep(2) |
| success = comfy_manager.start_comfyui() |
| |
| if success: |
| return "✅ ComfyUI reiniciado com sucesso!" |
| else: |
| return "❌ Erro ao reiniciar ComfyUI" |
|
|
| |
| def create_interface(): |
| """Cria interface Gradio""" |
| |
| with gr.Blocks( |
| title="ComfyUI Hugging Face Space", |
| theme=gr.themes.Soft(), |
| css=""" |
| .status-box { padding: 20px; border-radius: 10px; margin: 10px 0; } |
| .running { background-color: #d4edda; border: 1px solid #c3e6cb; } |
| .setup { background-color: #fff3cd; border: 1px solid #ffeaa7; } |
| .error { background-color: #f8d7da; border: 1px solid #f5c6cb; } |
| """ |
| ) as demo: |
| |
| gr.Markdown(""" |
| # 🚀 ComfyUI Hugging Face Space |
| |
| **ComfyUI executando gratuitamente no Hugging Face Spaces!** |
| |
| ✅ GPU Tesla T4 gratuita |
| ✅ Modelos persistentes |
| ✅ Interface web completa |
| ✅ Custom nodes incluídos |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=2): |
| |
| status_display = gr.Markdown("🟡 Aguardando inicialização...", elem_classes=["status-box", "setup"]) |
| |
| |
| with gr.Row(): |
| setup_btn = gr.Button("🚀 Iniciar Setup", variant="primary", scale=2) |
| restart_btn = gr.Button("🔄 Reiniciar", scale=1) |
| |
| |
| comfyui_link = gr.HTML("⏳ Aguardando setup...") |
| |
| |
| system_info = gr.Textbox( |
| label="📊 Informações do Sistema", |
| interactive=False, |
| lines=4 |
| ) |
| |
| with gr.Column(scale=3): |
| |
| logs_display = gr.Textbox( |
| label="📋 Logs do Setup", |
| lines=20, |
| max_lines=30, |
| interactive=False, |
| autoscroll=True |
| ) |
| |
| |
| with gr.Accordion("📚 Como Usar", open=False): |
| gr.Markdown(""" |
| ### 🎯 Passos para usar: |
| |
| 1. **Clique em "Iniciar Setup"** - O sistema irá baixar e configurar tudo automaticamente |
| 2. **Aguarde ~10-15 minutos** na primeira execução (downloads dos modelos) |
| 3. **Clique em "Abrir ComfyUI"** quando aparecer o link |
| 4. **Use normalmente** - Todos os dados ficam salvos automaticamente! |
| |
| ### 🔧 Recursos incluídos: |
| - **FLUX.1-schnell** - Modelo de geração rápida |
| - **VAE e CLIP** - Encoders necessários |
| - **LoRAs populares** - Para estilos específicos |
| - **ComfyUI Manager** - Para instalar novos nodes |
| - **WAS Node Suite** - Nodes úteis extras |
| |
| ### 💡 Dicas: |
| - ✅ Seus modelos ficam salvos entre sessões |
| - ✅ Pode fechar e abrir o Space sem perder dados |
| - ✅ Use "Reiniciar" se algo der errado |
| - ✅ Logs mostram o progresso detalhado |
| """) |
| |
| |
| setup_btn.click( |
| fn=setup_comfyui, |
| outputs=[status_display, logs_display] |
| ) |
| |
| restart_btn.click( |
| fn=restart_comfyui, |
| outputs=[status_display] |
| ) |
| |
| |
| demo.load( |
| fn=get_status_update, |
| outputs=[status_display, logs_display, system_info, comfyui_link], |
| every=3 |
| ) |
| |
| return demo |
|
|
| |
| def signal_handler(signum, frame): |
| """Handler para shutdown gracioso""" |
| logger.info(f"Recebido sinal {signum}, parando ComfyUI...") |
| comfy_manager.stop_comfyui() |
| sys.exit(0) |
|
|
| signal.signal(signal.SIGINT, signal_handler) |
| signal.signal(signal.SIGTERM, signal_handler) |
|
|
| |
| if __name__ == "__main__": |
| |
| if not comfy_manager.setup_complete: |
| setup_thread = threading.Thread( |
| target=lambda: comfy_manager.setup_comfyui() and comfy_manager.start_comfyui(), |
| daemon=True |
| ) |
| setup_thread.start() |
| |
| |
| demo = create_interface() |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=False, |
| inbrowser=False |
| ) |
|
|