"""Aplicação Gradio — pipeline multimodal do Ateliê Generativo (estilo lambe-lambe). Encadeia: LLM (expande o tema) -> Stable Diffusion v1-5 + LoRA (gera imagem) -> TTS (narra). Publicação: Hugging Face Spaces (SDK Gradio). IMPORTANTE: credenciais SOMENTE em Settings -> Variables and secrets do Space. Nunca no código. Modelos são carregados de forma preguiçosa (lazy): só na primeira geração, para não estourar a memória na inicialização do Space e não pagar o custo de carga se ninguém usar. """ import base64 import mimetypes import os import torch import gradio as gr from diffusers import StableDiffusionPipeline from transformers import AutoModelForCausalLM, AutoTokenizer, VitsModel # --- Configuração (sobrescrevível por variável de ambiente no Space) --------- BASE_MODEL = os.environ.get("BASE_MODEL", "stable-diffusion-v1-5/stable-diffusion-v1-5") LORA_REPO = os.environ.get("LORA_REPO", "lamble-lambe/atelie") # pesos oficiais da equipe # Versão do LoRA em produção. Vazio = último push no main (muda a cada treino). # Recomendado: defina uma tag semver (ex.: "v1.0.0") nas Settings do Space para fixar a produção. LORA_REVISION = os.environ.get("LORA_REVISION", "").strip() LLM_MODEL = os.environ.get("LLM_MODEL", "Qwen/Qwen2.5-0.5B-Instruct") TTS_MODEL = os.environ.get("TTS_MODEL", "facebook/mms-tts-por") # TTS neural em português STYLE_TOKEN = "estilo_lambelambe," # Reforço de estilo: acrescentado ao fim de todo prompt para firmar o P&B vintage # mesmo em temas fora da distribuição de treino (retratos). STYLE_SUFFIX = "black and white, vintage photograph, grainy, classic composition" # Negative prompt fixo: empurra para longe de cor/estética moderna. NEGATIVE_PROMPT = "color, colorful, modern, blurry, out of focus, deformed, disfigured, deformed face, extra fingers, extra limbs, mutated hands, bad anatomy, ugly, low quality, low resolution, jpeg artifacts, watermark, text" # --- Identificação do projeto (exibida na aba "Sobre") ---------------------- DISCIPLINA = "Inteligência Artificial Generativa e Modelos Multimodais — UniCEUB" PROFESSOR = "Prof. Romes Heriberto" PARTICIPANTES = [ "Diego Nunes de Morais", "Eduardo Deodoro de Moraes Florindo", "Higo Soares do Lago", "Lucio Flavio Vilar de Azevedo", "Paulo Victor Torres Martins", ] # Versão vigente do modelo em produção (vinda de LORA_REVISION; vazio = última do main) VERSAO_VIGENTE = LORA_REVISION or "main (última publicada)" SOBRE_MD = f""" ### Sobre o projeto **Ateliê Generativo — Estilo Lambe-Lambe.** Especialização do **Stable Diffusion v1-5** no estilo de **fotografia lambe-lambe / retrato vintage** (preto e branco, granulação, composição clássica) via **LoRA**, integrada a um **pipeline multimodal**: um LLM expande o tema, o difusor com o LoRA gera a imagem no estilo treinado e um modelo de voz narra a descrição. - **Disciplina:** {DISCIPLINA} - **Professor:** {PROFESSOR} - **Participantes:** {", ".join(PARTICIPANTES)} - **Modelo:** [`{LORA_REPO}`](https://huggingface.co/{LORA_REPO}) - **Versão vigente (produção):** `{VERSAO_VIGENTE}` """ INSTRUCOES_MD = """ ### Como usar 1. **Escreva o tema em inglês.** O Stable Diffusion v1-5 é nativo em inglês — prompts em inglês produzem resultados **mais fiéis e nítidos**. Ex.: `boys playing football in the street`, `an old sunday market`, `a street musician`. 2. Clique em **Gerar**. O sistema expande o tema, gera a imagem no estilo lambe-lambe e narra a descrição. **Guidance scale** — o quanto a imagem obedece ao prompt: - **Baixo (1–5):** mais criativo/variado, porém menos fiel ao texto. - **Médio (7–9):** equilíbrio recomendado (padrão **7.5**). - **Alto (12–15):** segue o prompt ao máximo, mas pode ficar saturado/artificial. **Seed** — controla a aleatoriedade: a **mesma seed + mesmo prompt** reproduz a **mesma imagem** (útil para comparar). Mude a seed para gerar variações. **Negative prompt** (em *Opções avançadas*) — lista o que a imagem deve **evitar**. Já vem preenchido para reforçar o estilo (`color, modern, blurry, ...`), afastando cor e aparência moderna. Edite para ajustar ou deixe vazio para desativar. > Dica: como o modelo foi treinado com **retratos**, cenas de pessoas *posando* saem melhor que ação. > Na primeira geração os modelos são carregados sob demanda — no hardware gratuito (CPU) isso leva alguns minutos. """ DEVICE = "cuda" if torch.cuda.is_available() else "cpu" DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32 # --- Estado dos modelos (preenchido sob demanda) ---------------------------- _diffusion = None _llm = None _llm_tok = None _tts = None _tts_tok = None def _get_diffusion(): global _diffusion if _diffusion is None: pipe = StableDiffusionPipeline.from_pretrained( BASE_MODEL, torch_dtype=DTYPE, safety_checker=None ) try: # revision=None -> main (última versão); revision="vX.Y.Z" -> versão fixada pipe.load_lora_weights(LORA_REPO, revision=LORA_REVISION or None) print(f"[ok] LoRA aplicado: {LORA_REPO}@{LORA_REVISION or 'main'}") except Exception as erro: # sem LoRA publicado ainda -> cai no modelo base print(f"[aviso] LoRA não carregado ({erro}); usando somente o modelo base") _diffusion = pipe.to(DEVICE) return _diffusion def _get_llm(): global _llm, _llm_tok if _llm is None: _llm_tok = AutoTokenizer.from_pretrained(LLM_MODEL) _llm = AutoModelForCausalLM.from_pretrained(LLM_MODEL, torch_dtype=DTYPE).to(DEVICE) return _llm, _llm_tok def _get_tts(): global _tts, _tts_tok if _tts is None: _tts_tok = AutoTokenizer.from_pretrained(TTS_MODEL) _tts = VitsModel.from_pretrained(TTS_MODEL).to(DEVICE) return _tts, _tts_tok # --- Etapas do pipeline ----------------------------------------------------- def expandir_prompt(tema: str) -> str: """Expande um tema curto em uma descrição visual rica, em inglês. O prompt de difusão sai em inglês porque o Stable Diffusion v1-5 é nativo em inglês; o token do estilo (STYLE_TOKEN) é quem aciona o LoRA, independente do idioma. Se o LoRA for treinado com legendas em português (frente de dados), trocar a instrução abaixo para gerar em português e alinhar com a equipe. """ model, tok = _get_llm() mensagens = [ { "role": "system", "content": ( "You expand a short theme into a single rich, concrete visual " "description for image generation. Reply with one English sentence, " "no preamble, no quotes." ), }, {"role": "user", "content": f"Theme: {tema}"}, ] entrada = tok.apply_chat_template(mensagens, tokenize=False, add_generation_prompt=True) ids = tok(entrada, return_tensors="pt").to(DEVICE) with torch.no_grad(): saida = model.generate(**ids, max_new_tokens=80, do_sample=True, temperature=0.7) texto = tok.decode(saida[0][ids.input_ids.shape[1]:], skip_special_tokens=True).strip() return f"{STYLE_TOKEN} {texto}, {STYLE_SUFFIX}" def gerar_imagem(prompt: str, guidance_scale: float = 7.5, seed: int = 0, negative_prompt: str = NEGATIVE_PROMPT): """Gera a imagem com Stable Diffusion v1-5 + pesos LoRA da equipe.""" # Garante que a palavra-gatilho do estilo esteja SEMPRE no prompt (aciona o LoRA). if "estilo_lambelambe" not in prompt.lower(): prompt = f"{STYLE_TOKEN} {prompt}" pipe = _get_diffusion() gerador = torch.Generator(device=DEVICE).manual_seed(int(seed)) passos = 20 if DEVICE == "cpu" else 30 # menos passos em CPU (Space grátis) para não travar resultado = pipe( prompt, negative_prompt=(negative_prompt or None), # 1 reforça P&B/vintage afastando cor/estética moderna guidance_scale=float(guidance_scale), num_inference_steps=passos, generator=gerador, ) return resultado.images[0] def narrar(tema: str): """Sintetiza narração em português (VITS/MMS-TTS) a partir do tema. Devolve (sample_rate, waveform) — formato aceito por gr.Audio. """ model, tok = _get_tts() frase = f"Uma imagem em estilo lambe-lambe inspirada em {tema}." ids = tok(frase, return_tensors="pt").to(DEVICE) with torch.no_grad(): onda = model(**ids).waveform return model.config.sampling_rate, onda.squeeze().cpu().numpy() def pipeline(tema: str, guidance_scale: float, seed: int, negative_prompt: str): prompt = expandir_prompt(tema) imagem = gerar_imagem(prompt, guidance_scale, seed, negative_prompt) audio = narrar(tema) return imagem, prompt, audio # --- Identidade visual (cores UniCEUB) -------------------------------------- CEUB_ROXO = "#56148a" # roxo institucional UniCEUB (extraído do site oficial) CEUB_ROXO_ESCURO = "#330066" CEUB_DOURADO = "#f0cc25" # dourado de destaque LOGO_PATH = os.environ.get("LOGO_PATH", "ceub_logo.png") # logo oficial (logoCEUB2021.png) na raiz do Space def _logo_data_uri(path: str) -> str: """Embute a logo como data-URI (evita depender do file-serving do Space).""" if not os.path.exists(path): return "" mime = mimetypes.guess_type(path)[0] or "image/png" dados = base64.b64encode(open(path, "rb").read()).decode() return f"data:{mime};base64,{dados}" LOGO_URI = _logo_data_uri(LOGO_PATH) TEMA_CEUB = gr.themes.Default( primary_hue=gr.themes.colors.purple, neutral_hue=gr.themes.colors.gray, ) CSS_CEUB = f""" #cabecalho {{ display:flex; align-items:center; gap:16px; border-bottom:4px solid {CEUB_DOURADO}; padding-bottom:12px; margin-bottom:6px; }} #cabecalho img {{ height:56px; width:auto; }} #cabecalho h1 {{ margin:0; color:{CEUB_ROXO}; font-weight:800; line-height:1.1; }} button.primary, .primary {{ background:{CEUB_ROXO} !important; border-color:{CEUB_ROXO} !important; }} button.primary:hover, .primary:hover {{ background:{CEUB_ROXO_ESCURO} !important; }} a {{ color:{CEUB_ROXO}; }} """ # --- Interface Gradio ------------------------------------------------------- with gr.Blocks(title="Ateliê Generativo — Lambe-Lambe") as demo: _logo_img = f'UniCEUB' if LOGO_URI else "" gr.HTML(f'
{_logo_img}

Ateliê Generativo — Estilo Lambe-Lambe

') gr.Markdown(f"*{DISCIPLINA} · {PROFESSOR} · modelo `{VERSAO_VIGENTE}`*") with gr.Accordion("Sobre o projeto", open=True): gr.Markdown(SOBRE_MD) with gr.Accordion("Como usar", open=False): gr.Markdown(INSTRUCOES_MD) with gr.Row(): tema = gr.Textbox(label="Tema", placeholder="e.g.: an old sunday market") with gr.Row(): guidance = gr.Slider(1.0, 15.0, value=7.5, label="Guidance scale") seed = gr.Number(value=0, label="Seed", precision=0) with gr.Accordion("Opções avançadas", open=False): negative = gr.Textbox( label="Negative prompt (o que evitar)", value=NEGATIVE_PROMPT, info="Termos que a imagem deve evitar. Mantém o estilo P&B/vintage. Deixe vazio para desativar.", ) btn = gr.Button("Gerar", variant="primary") with gr.Row(): out_img = gr.Image(label="Imagem gerada") with gr.Column(): out_prompt = gr.Textbox(label="Prompt expandido") out_audio = gr.Audio(label="Narração") btn.click(pipeline, inputs=[tema, guidance, seed, negative], outputs=[out_img, out_prompt, out_audio]) if __name__ == "__main__": # Gradio 6: theme e css são passados no launch() (não mais no Blocks()). demo.launch(theme=TEMA_CEUB, css=CSS_CEUB)