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)