Spaces:
Running on Zero
Running on Zero
File size: 5,401 Bytes
c7f05a9 550d35b 46d55e6 550d35b a769839 c7f05a9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | # -*- coding: utf-8 -*-
"""
A CASCA GRADIO DO ADAPTER ZEROGPU — o que o Hugging Face executa.
Este arquivo é fino de propósito: toda a lógica mora em `nucleo.py` (testado
no repositório, sem Gradio). Aqui só se faz o que EXIGE o ambiente do Space:
- decorar a geração com `@spaces.GPU(duration=...)` — é o decorator que
faz o ZeroGPU alocar a placa durante a chamada, e a duração é POR TIPO
(IMAGE_GPU_SECONDS etc., lidas no nucleo);
- expor as funções como API do Gradio (`api_name=...`), que é o único
transporte que um Space ZeroGPU oferece: POST → event_id → SSE. Os
yields do gerador viram eventos `generating` (o progresso do app);
- manter a PÁGINA do Space inofensiva: quem abrir a URL num navegador vê
um aviso, não um formulário que gasta quota — gerar exige o segredo, e a
conferência roda ANTES do decorator de GPU ser invocado.
Fora do Space (sem `spaces` instalado) o decorator vira identidade e o
WORKER_MOCK=1 permite subir a casca inteira num notebook para teste.
"""
from __future__ import annotations
import gradio as gr
import nucleo
try:
import spaces
_GPU = spaces.GPU
except ImportError: # fora do Hugging Face: sem decorator
def _GPU(*a, **k):
def envolve(fn):
return fn
return envolve
def _fazer_endpoint(tipo: str):
"""Uma função por tipo, porque a DURAÇÃO da GPU é do decorator e vídeo
não pode viver com o prazo de imagem (abortaria no meio, com a quota já
gasta)."""
@_GPU(duration=nucleo.duracao_gpu(tipo))
def _na_gpu(params_json: str):
yield from nucleo.gerar(tipo, params_json)
def endpoint(segredo: str, params_json: str):
# A porta fica AQUI FORA: recusar antes de `_na_gpu` significa que a
# chamada sem segredo não aloca GPU e não consome quota de ninguém.
recusa = nucleo.autenticar(segredo)
if recusa:
yield {"erro": recusa}
return
# O DOWNLOAD dos pesos roda AQUI FORA da GPU: é rede, não CUDA, e
# dentro do @spaces.GPU ele queimava o prazo — a duração declarada só
# precisa cobrir carregar-do-disco + inferência.
yield {"etapa": "Baixando o modelo…", "progress": None}
falha = nucleo.preparar(tipo)
if falha:
# erro_gpu, e não erro: download que falhou (repo errado, rede,
# Hub fora) é ESTE runtime não conseguindo atender agora — o
# backend degrada para o gerador local em vez de devolver a
# falha à pessoa. Medido: o 404 de um id de modelo errado saía
# como erro do pedido e a música morria sem o fallback.
yield {"erro_gpu": falha}
return
# O try daqui não é redundância do try do nucleo: a ALOCAÇÃO do
# @spaces.GPU acontece ANTES de o corpo rodar, e é ali que nascem os
# erros de quota e de duração do ZeroGPU. Sem esta cerca eles
# estouravam dentro do Gradio, que os engole (show_error desligado) —
# o backend recebia "o Space abortou a chamada: None", sem motivo
# nenhum para agir. Medido na primeira geração real.
try:
yield from _na_gpu(params_json)
except BaseException as e:
if isinstance(e, (KeyboardInterrupt, SystemExit)):
raise
if nucleo.e_erro_de_gpu(e):
yield {"erro_gpu": f"ZeroGPU indisponível agora: {str(e)[:200]}"}
else:
yield {"erro": f"{type(e).__name__}: {str(e)[:300]}"}
return endpoint
def _saude(segredo: str):
recusa = nucleo.autenticar(segredo)
if recusa:
return {"erro": recusa}
return nucleo.saude()
with gr.Blocks(title="Conecta Primo — GPU") as demo:
# A página humana diz o mínimo. Nada aqui dispara geração: os botões nem
# existem — só a API, e ela pede o segredo.
gr.Markdown("## Conecta Primo — worker de GPU\n"
"Este Space atende **somente** o backend do aplicativo, "
"autenticado por segredo. Não há nada para usar nesta página.")
segredo_in = gr.Textbox(visible=False)
params_in = gr.Textbox(visible=False)
saida = gr.JSON(visible=False)
# gr.api registra função como endpoint puro (Gradio >= 4.44). Os botões
# invisíveis são a forma equivalente que funciona em toda versão 4/5 —
# ficam os dois? Não: DOIS caminhos para a mesma função é o defeito de
# sempre. Fica o dos componentes, que é o estável.
for _tipo, _nome in (("image", "gerar_imagem"), ("voice", "gerar_fala"),
("music", "gerar_musica"), ("video", "gerar_video")):
_btn = gr.Button(visible=False)
_btn.click(_fazer_endpoint(_tipo), inputs=[segredo_in, params_in],
outputs=[saida], api_name=_nome)
_btn_saude = gr.Button(visible=False)
_btn_saude.click(_saude, inputs=[segredo_in], outputs=[saida],
api_name="saude")
if __name__ == "__main__":
# Fila do Gradio LIGADA com concorrência 1: no ZeroGPU cada chamada é uma
# alocação de GPU, e duas em paralelo é OOM ou quota dobrada — a mesma
# decisão do MAX_CONCURRENT_JOBS=1 do worker CUDA, no dialeto do Gradio.
demo.queue(default_concurrency_limit=1, max_size=16)
demo.launch(show_api=False)
|