Spaces:
Sleeping
Sleeping
Download app.py from ConectaPrimoAI/conecta-primo-gpu: direct link, hf CLI and curl.
- Browser
- Download file 7.73 kB
-
https://huggingface.co/spaces/ConectaPrimoAI/conecta-primo-gpu/resolve/main/app.py
- Command line
-
hf download hf://spaces/ConectaPrimoAI/conecta-primo-gpu/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ConectaPrimoAI/conecta-primo-gpu/resolve/main/app.py
7.73 kB
| # -*- 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 os | |
| 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).""" | |
| 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 | |
| # A VOZ DO LIVE (13/10/2026): o Rápido TTS (Apache 2.0, pt-BR), na CPU deste | |
| # Space — FORA do @spaces.GPU, então não gasta quota de GPU nenhuma. Ele mora | |
| # aqui porque hospedar Gradio em CPU gratuita passou a exigir PRO no Hugging | |
| # Face, e este Space já existe. O código dele é clonado pelo `publicar.sh`. | |
| _RAPIDO = {"tts": None, "erro": ""} | |
| def _falar_live(segredo: str, texto: str): | |
| recusa = nucleo.autenticar(segredo) | |
| if recusa: | |
| return {"erro": recusa} | |
| texto = str(texto or "").strip()[:600] | |
| if not texto: | |
| return {"erro": "texto vazio"} | |
| try: | |
| if _RAPIDO["tts"] is None: | |
| import torch | |
| from rapido import Rapido | |
| # Poucos núcleos: a CPU do Space é dividida, e núcleos demais brigam. | |
| torch.set_num_threads(max(1, min(4, os.cpu_count() or 2))) | |
| _RAPIDO["tts"] = Rapido.from_pretrained(device="cpu") | |
| import base64, io, time as _t, wave | |
| import numpy as np | |
| t0 = _t.time() | |
| # 4 passos e não 8: na CPU deste Space a frase levava ~6 s, e numa | |
| # conversa falada isso é silêncio. Medido: 4 passos cortam o tempo da | |
| # difusão quase pela metade, com a mesma fala. | |
| r = _RAPIDO["tts"].synthesize(texto, steps=4) | |
| audio, taxa = r.output["audio"], r.output["sample_rate"] | |
| if taxa == 48000: | |
| audio = audio[: len(audio) // 2 * 2].reshape(-1, 2).mean(axis=1) | |
| taxa = 24000 | |
| pcm = (np.clip(audio, -1, 1) * 32767).astype(np.int16) | |
| buf = io.BytesIO() | |
| with wave.open(buf, "wb") as w: | |
| w.setnchannels(1); w.setsampwidth(2); w.setframerate(taxa); w.writeframes(pcm.tobytes()) | |
| return {"audio_base64": base64.b64encode(buf.getvalue()).decode(), "mime": "audio/wav", | |
| "segundos": round(_t.time() - t0, 2), "nucleos": os.cpu_count()} | |
| except Exception as e: | |
| return {"erro": f"{type(e).__name__}: {str(e)[:200]}"} | |
| 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) | |
| # A VOZ TEM FILA PRÓPRIA (`concurrency_id`): na fila das gerações ela | |
| # esperaria um vídeo de minutos terminar, e o Live ficaria mudo. | |
| _btn_voz = gr.Button(visible=False) | |
| _btn_voz.click(_falar_live, inputs=[segredo_in, params_in], outputs=[saida], | |
| api_name="falar_live", concurrency_limit=2, concurrency_id="voz") | |
| _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) | |