Spaces:
Sleeping
Sleeping
Update app.py
Browse filesAtualização com as novas implementações da Aula 12
app.py
CHANGED
|
@@ -1,20 +1,28 @@
|
|
| 1 |
# CHATBOT E GERADOR DE POSTS PARA REDES SOCIAIS
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
import gradio as gr
|
| 7 |
import requests
|
| 8 |
import os
|
| 9 |
-
import json
|
| 10 |
-
import time
|
| 11 |
from datetime import datetime
|
| 12 |
from zoneinfo import ZoneInfo
|
| 13 |
from PIL import Image, ImageDraw, ImageFont
|
| 14 |
from io import BytesIO
|
| 15 |
from huggingface_hub import InferenceClient
|
| 16 |
-
|
| 17 |
-
import
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Importar firebase-admin
|
| 20 |
import firebase_admin
|
|
@@ -27,12 +35,11 @@ HUGGINGFACE_API_KEY = os.environ.get("Capoeira")
|
|
| 27 |
if not HUGGINGFACE_API_KEY:
|
| 28 |
print("⚠️ API Key do Hugging Face não configurada! Certifique-se de que a variável de ambiente 'Capoeira' está definida.")
|
| 29 |
|
| 30 |
-
# URLs e modelos
|
| 31 |
BASE_URL = "https://router.huggingface.co/v1"
|
| 32 |
MODELO_TEXTO = "meta-llama/Llama-3.1-8B-Instruct"
|
| 33 |
MODELO_TRADUCA = "Helsinki-NLP/opus-mt-pt-en"
|
| 34 |
|
| 35 |
-
# ATUALIZADO: Lista de modelos de imagem
|
| 36 |
MODELOS_IMAGEM = [
|
| 37 |
{
|
| 38 |
"nome": "FLUX.1-schnell",
|
|
@@ -54,7 +61,6 @@ MODELOS_IMAGEM = [
|
|
| 54 |
}
|
| 55 |
]
|
| 56 |
|
| 57 |
-
|
| 58 |
# Headers para requisições
|
| 59 |
headers = {
|
| 60 |
"Authorization": f"Bearer {HUGGINGFACE_API_KEY}",
|
|
@@ -84,7 +90,6 @@ ESTILOS_DISPONIVEIS = [
|
|
| 84 |
"Tutorial/Passo a Passo",
|
| 85 |
]
|
| 86 |
|
| 87 |
-
# NOVO: Dicionário de estilos de imagem (baseado na Solicitação 2)
|
| 88 |
ESTILOS_DE_IMAGEM = {
|
| 89 |
"Nenhum (Automático)": "standard photography, high quality, 4k",
|
| 90 |
"Fotografia Vintage": "vintage photography, retro style, film grain, analog",
|
|
@@ -103,8 +108,6 @@ ESTILOS_DE_IMAGEM = {
|
|
| 103 |
"Minimalista": "minimalist, clean background, simple, elegant",
|
| 104 |
}
|
| 105 |
|
| 106 |
-
|
| 107 |
-
# NOVO: Dicionário de filtros de imagem
|
| 108 |
FILTROS_IMAGEM = {
|
| 109 |
"Nenhum": "",
|
| 110 |
"Preto e Branco": "black and white, monochrome, high contrast",
|
|
@@ -114,23 +117,23 @@ FILTROS_IMAGEM = {
|
|
| 114 |
"Frio (Moderno)": "cool tones, modern aesthetic, clean, desaturated blues",
|
| 115 |
}
|
| 116 |
|
| 117 |
-
|
| 118 |
-
# CORRIGIDO: O dicionário do LinkedIn estava quebrado
|
| 119 |
-
# ATUALIZADO: Funcionalidade de formato reativada (Solicitação 1)
|
| 120 |
FORMATO_CONFIGS = {
|
| 121 |
"Instagram (Post)": {"tamanho": "100-150 palavras", "estrutura": "gancho inicial + desenvolvimento + call-to-action", "tom_adicional": "próximo, empático e motivador", "max_tokens": 350, "limite_palavras_ia": "150 palavras", "hashtags": "Incluir 4-5 hashtags relevantes no final. Incluir no máximo 3 emojis relevantes no texto."},
|
| 122 |
"Twitter/X (Curto)": {"tamanho": "Até 280 caracteres", "estrutura": "frase de impacto + link/hashtag", "tom_adicional": "direto e conciso, ideal para tweets", "max_tokens": 150, "limite_palavras_ia": "280 caracteres", "hashtags": "Incluir no máximo 2 hashtags."},
|
|
|
|
| 123 |
"LinkedIn (Artigo)": {"tamanho": "250-400 palavras", "estrutura": "título chamativo + desenvolvimento profissional + reflexão", "tom_adicional": "profissional e autoritário, focado em insights", "max_tokens": 700, "limite_palavras_ia": "400 palavras", "hashtags": "Incluir 3-4 hashtags profissionais no final."},
|
| 124 |
}
|
| 125 |
|
| 126 |
-
#
|
| 127 |
-
post_history = []
|
| 128 |
-
# Variável para a instância do Firestore
|
| 129 |
db = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
# ============================================
|
| 133 |
-
# FUNÇÕES DE PERSISTÊNCIA (FIREBASE
|
| 134 |
# ============================================
|
| 135 |
|
| 136 |
def _inicializar_firestore():
|
|
@@ -138,9 +141,8 @@ def _inicializar_firestore():
|
|
| 138 |
Inicializa o Firebase Admin SDK usando as credenciais
|
| 139 |
armazenadas nos Secrets do Hugging Face Spaces.
|
| 140 |
"""
|
| 141 |
-
global db
|
| 142 |
|
| 143 |
-
# Nome do Secret que você criou no HF Spaces
|
| 144 |
secret_name = "FIREBASE_SERVICE_ACCOUNT_JSON"
|
| 145 |
secret_json_string = os.environ.get(secret_name)
|
| 146 |
|
|
@@ -148,30 +150,26 @@ def _inicializar_firestore():
|
|
| 148 |
print(f"❌ Erro de Configuração do Firebase: Secret '{secret_name}' não encontrado.")
|
| 149 |
print("Usando apenas histórico de sessão (temporário).")
|
| 150 |
db = None
|
|
|
|
| 151 |
return
|
| 152 |
|
| 153 |
if not firebase_admin._apps:
|
| 154 |
try:
|
| 155 |
-
# Converter a string JSON (do Secret) em um dicionário
|
| 156 |
service_account_info = json.loads(secret_json_string)
|
| 157 |
-
|
| 158 |
-
# Usar o dicionário para criar as credenciais
|
| 159 |
cred = credentials.Certificate(service_account_info)
|
| 160 |
-
|
| 161 |
firebase_admin.initialize_app(cred)
|
| 162 |
db = firestore.client()
|
| 163 |
print("✅ Firestore inicializado com sucesso.")
|
|
|
|
|
|
|
| 164 |
except Exception as e:
|
| 165 |
print(f"❌ Erro ao inicializar Firestore. Usando histórico de sessão. Detalhe: {e}")
|
| 166 |
db = None
|
|
|
|
| 167 |
|
| 168 |
def _adicionar_post_firestore(entry):
|
| 169 |
-
"""
|
| 170 |
-
Adiciona um novo documento à coleção 'posts' no Firestore.
|
| 171 |
-
"""
|
| 172 |
if db:
|
| 173 |
try:
|
| 174 |
-
# Adiciona o dicionário 'entry' como um novo documento
|
| 175 |
db.collection('posts').add(entry)
|
| 176 |
return True
|
| 177 |
except Exception as e:
|
|
@@ -180,16 +178,10 @@ def _adicionar_post_firestore(entry):
|
|
| 180 |
return False
|
| 181 |
|
| 182 |
def _obter_historico_firestore():
|
| 183 |
-
"""
|
| 184 |
-
Obtém os últimos 50 documentos da coleção 'posts', ordenados por data.
|
| 185 |
-
Retorna uma lista de dicionários.
|
| 186 |
-
"""
|
| 187 |
if db:
|
| 188 |
try:
|
| 189 |
-
|
| 190 |
-
posts_query = db.collection('posts').order_by('Data/Hora', direction=firestore.Query.DESCENDING).limit(50)
|
| 191 |
posts_stream = posts_query.stream()
|
| 192 |
-
|
| 193 |
history = [post.to_dict() for post in posts_stream]
|
| 194 |
return history
|
| 195 |
except Exception as e:
|
|
@@ -198,66 +190,229 @@ def _obter_historico_firestore():
|
|
| 198 |
return []
|
| 199 |
|
| 200 |
def atualizar_historico(entry):
|
| 201 |
-
"""
|
| 202 |
-
Função unificada para salvar no Firestore E atualizar o cache de sessão.
|
| 203 |
-
"""
|
| 204 |
global post_history
|
| 205 |
-
|
| 206 |
-
# Tenta salvar no Firestore
|
| 207 |
_adicionar_post_firestore(entry)
|
| 208 |
-
|
| 209 |
-
# Atualiza o cache de sessão local (para a UI)
|
| 210 |
post_history.insert(0, entry)
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
return post_history
|
| 213 |
|
| 214 |
def carregar_historico_inicial():
|
| 215 |
-
"""
|
| 216 |
-
Função para carregar o histórico ao iniciar o aplicativo.
|
| 217 |
-
Prioriza o DB e preenche o cache de sessão.
|
| 218 |
-
"""
|
| 219 |
global post_history
|
| 220 |
-
|
| 221 |
-
# Tenta carregar do Firestore
|
| 222 |
historico_db = _obter_historico_firestore()
|
| 223 |
-
|
| 224 |
if historico_db:
|
| 225 |
post_history = historico_db
|
| 226 |
-
|
| 227 |
-
# Retorna o histórico (do DB ou vazio)
|
| 228 |
return post_history
|
| 229 |
|
| 230 |
-
#
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
# ============================================
|
| 234 |
-
# HELPER
|
| 235 |
# ============================================
|
|
|
|
| 236 |
def _formatar_historico_para_df(history_list):
|
| 237 |
-
"""
|
| 238 |
-
Converte a lista de dicionários (do Firestore) para uma
|
| 239 |
-
lista de listas (para o gr.Dataframe).
|
| 240 |
-
"""
|
| 241 |
formatted_list = []
|
| 242 |
if not history_list:
|
| 243 |
return []
|
| 244 |
|
| 245 |
-
#
|
| 246 |
for entry in history_list:
|
|
|
|
| 247 |
formatted_list.append([
|
|
|
|
| 248 |
entry.get("Data/Hora", ""),
|
| 249 |
entry.get("Tema", ""),
|
| 250 |
entry.get("Nicho", ""),
|
| 251 |
entry.get("Estilo", ""),
|
| 252 |
entry.get("Formato", ""),
|
| 253 |
-
|
| 254 |
entry.get("Status", "")
|
| 255 |
])
|
| 256 |
return formatted_list
|
| 257 |
|
| 258 |
-
# ============================================
|
| 259 |
-
# FUNÇÃO DE ALERTA (ATUALIZADA para HTML)
|
| 260 |
-
# ============================================
|
| 261 |
def criar_alerta(tipo, mensagem):
|
| 262 |
"""Cria alerta HTML colorido"""
|
| 263 |
cores = {
|
|
@@ -277,41 +432,47 @@ def criar_alerta(tipo, mensagem):
|
|
| 277 |
</div>
|
| 278 |
"""
|
| 279 |
|
| 280 |
-
# ============================================
|
| 281 |
-
# FUNÇÕES AUXILIARES (Adicionadas)
|
| 282 |
-
# ============================================
|
| 283 |
-
|
| 284 |
def copiar_feedback(texto):
|
| 285 |
if texto:
|
| 286 |
return criar_alerta('success', '✅ Texto copiado!')
|
| 287 |
return criar_alerta('warning', '⚠️ Nada para copiar')
|
| 288 |
|
| 289 |
def limpar_tudo():
|
| 290 |
-
|
|
|
|
| 291 |
return (
|
|
|
|
| 292 |
"", # texto_output
|
| 293 |
None, # imagem_output
|
| 294 |
criar_alerta('info', '🧹 Interface limpa!'), # status_output
|
| 295 |
0, # palavras_output
|
| 296 |
0, # caracteres_output
|
| 297 |
0, # hashtags_output
|
| 298 |
-
list(FORMATO_CONFIGS.keys())[0], # formato_input
|
| 299 |
"Nenhum (Automático)", # estilo_img_input
|
| 300 |
"Balanceada", # qualidade_img_input
|
| 301 |
"Nenhum", # filtro_img_input
|
| 302 |
-
None # download_output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
)
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
| 311 |
|
| 312 |
-
# ============================================
|
| 313 |
-
# NOVA FUNÇÃO: INTERPRETAR ERROS (Solicitação 3)
|
| 314 |
-
# ============================================
|
| 315 |
def interpretar_erro_api(erro_str):
|
| 316 |
"""Interpreta erros comuns da API para o usuário em Português."""
|
| 317 |
erro_str_lower = erro_str.lower()
|
|
@@ -336,21 +497,95 @@ def interpretar_erro_api(erro_str):
|
|
| 336 |
if "authorization" in erro_str_lower or "401" in erro_str_lower:
|
| 337 |
return ("Erro 401: Autenticação falhou. A Chave da API (Secret 'Capoeira') pode estar inválida ou ausente.")
|
| 338 |
|
| 339 |
-
|
| 340 |
-
return f"Erro inesperado: {erro_str[:200]}..." # Trunca o erro
|
| 341 |
|
| 342 |
# ============================================
|
| 343 |
-
# FUNÇÕES DE
|
| 344 |
# ============================================
|
| 345 |
|
| 346 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
"""
|
| 348 |
Gera texto usando API do Hugging Face com base no formato escolhido.
|
| 349 |
"""
|
| 350 |
|
| 351 |
-
# ATUALIZADO: Linha hardcoded removida
|
| 352 |
-
# formato = "Instagram (Post)"
|
| 353 |
-
|
| 354 |
if not HUGGINGFACE_API_KEY:
|
| 355 |
return "❌ Erro de Configuração: API Key não está definida."
|
| 356 |
|
|
@@ -395,25 +630,19 @@ Escreva apenas o conteúdo, sem introduções ou explicações."""
|
|
| 395 |
else:
|
| 396 |
return f"❌ Erro na resposta da API: Resposta vazia ou inesperada.\n{resultado}"
|
| 397 |
else:
|
| 398 |
-
# ATUALIZADO: Interpretar erro
|
| 399 |
return f"❌ {interpretar_erro_api(f'Erro {response.status_code}: {response.text}')}"
|
| 400 |
|
| 401 |
except Exception as e:
|
| 402 |
-
# ATUALIZADO: Interpretar erro
|
| 403 |
return f"❌ {interpretar_erro_api(str(e))}"
|
| 404 |
|
| 405 |
def traduzir_texto(texto_pt):
|
| 406 |
"""Traduz texto de Português (PT) para Inglês (EN) usando API do Hugging Face.
|
| 407 |
"""
|
| 408 |
-
|
| 409 |
if not HUGGINGFACE_API_KEY:
|
| 410 |
return texto_pt
|
| 411 |
|
| 412 |
url = f"https://api-inference.huggingface.co/models/{MODELO_TRADUCA}"
|
| 413 |
-
|
| 414 |
-
payload = {
|
| 415 |
-
"inputs": texto_pt,
|
| 416 |
-
}
|
| 417 |
|
| 418 |
try:
|
| 419 |
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
|
@@ -427,30 +656,18 @@ def traduzir_texto(texto_pt):
|
|
| 427 |
return texto_pt # Fallback
|
| 428 |
else:
|
| 429 |
return texto_pt # Fallback
|
| 430 |
-
|
| 431 |
except Exception as e:
|
| 432 |
print(f"Falha na tradução (fallback para PT): {e}")
|
| 433 |
return texto_pt # Fallback
|
| 434 |
|
| 435 |
-
# ============================================
|
| 436 |
-
# NOVAS FUNÇÕES DE GERAÇÃO DE IMAGEM
|
| 437 |
-
# ============================================
|
| 438 |
-
|
| 439 |
def otimizar_prompt_imagem(descricao_pt, estilo_escolhido, filtro_escolhido):
|
| 440 |
"""Combina as escolhas do usuário em um prompt otimizado (em Português)."""
|
| 441 |
|
| 442 |
-
# 1. Obter estilo
|
| 443 |
-
# ATUALIZADO: Usar o novo dicionário ESTILOS_DE_IMAGEM (Solicitação 2)
|
| 444 |
estilo = ESTILOS_DE_IMAGEM.get(estilo_escolhido, ESTILOS_DE_IMAGEM["Nenhum (Automático)"])
|
| 445 |
-
|
| 446 |
-
# 2. Obter filtro
|
| 447 |
filtro = FILTROS_IMAGEM.get(filtro_escolhido, FILTROS_IMAGEM["Nenhum"])
|
| 448 |
|
| 449 |
-
# 3. Montar o prompt
|
| 450 |
-
# Adiciona tags de qualidade padrão
|
| 451 |
prompt_final = f"{descricao_pt}, {estilo}, {filtro}, best quality, 4k"
|
| 452 |
|
| 453 |
-
# Limpar tags duplicadas ou vazias (ex: "Nenhum" vira "")
|
| 454 |
prompt_final = prompt_final.replace(", ,", ",").replace(", ,", ",")
|
| 455 |
return prompt_final
|
| 456 |
|
|
@@ -498,15 +715,13 @@ def gerar_imagem_robusta(descricao_pt, estilo_escolhido, qualidade, filtro_escol
|
|
| 498 |
|
| 499 |
client = InferenceClient(api_key=HUGGINGFACE_API_KEY)
|
| 500 |
|
| 501 |
-
# Gerar imagem
|
| 502 |
imagem = client.text_to_image(
|
| 503 |
prompt=prompt_final_en,
|
| 504 |
model=modelo_config['id'],
|
| 505 |
negative_prompt=negative_prompt,
|
| 506 |
-
num_inference_steps=config['steps']
|
| 507 |
)
|
| 508 |
|
| 509 |
-
# Sucesso!
|
| 510 |
print(f"✅ Imagem gerada com {modelo_config['nome']}")
|
| 511 |
mensagem = f"✅ Imagem gerada com {modelo_config['nome']}"
|
| 512 |
|
|
@@ -515,51 +730,37 @@ def gerar_imagem_robusta(descricao_pt, estilo_escolhido, qualidade, filtro_escol
|
|
| 515 |
except Exception as e:
|
| 516 |
print(f"❌ Falha com {modelo_config['nome']}: {str(e)}")
|
| 517 |
|
| 518 |
-
# Se não for o último modelo, continua tentando
|
| 519 |
if i < len(config['modelos']) - 1:
|
| 520 |
print(f"⏭️ Tentando próximo modelo...")
|
| 521 |
continue
|
| 522 |
else:
|
| 523 |
-
# Último modelo falhou, retornar erro
|
| 524 |
-
# ATUALIZADO: Interpretar erro
|
| 525 |
mensagem = f"❌ {interpretar_erro_api(str(e))}"
|
| 526 |
return (None, mensagem)
|
| 527 |
|
| 528 |
-
# Se chegou aqui, algo deu errado
|
| 529 |
return (None, "❌ Erro inesperado ao gerar imagem")
|
| 530 |
|
| 531 |
|
| 532 |
# ============================================
|
| 533 |
-
#
|
| 534 |
# ============================================
|
| 535 |
def responder_chat(message, chat_history):
|
| 536 |
-
"""
|
| 537 |
-
Processa uma mensagem do usuário e retorna uma resposta do LLM para o chatbot.
|
| 538 |
-
"""
|
| 539 |
if not HUGGINGFACE_API_KEY:
|
| 540 |
return "❌ Erro de Configuração: API Key não está definida."
|
| 541 |
|
| 542 |
url = f"{BASE_URL}/chat/completions"
|
| 543 |
|
| 544 |
-
# 1. Definir o System Prompt para o assistente
|
| 545 |
system_prompt = "Você é um assistente virtual prestativo e amigável, especializado em marketing de mídias sociais e criação de conteúdo, mas pode responder sobre qualquer tópico. Seja direto e útil."
|
| 546 |
|
| 547 |
-
# 2. Construir o histórico no formato da API
|
| 548 |
messages = [{"role": "system", "content": system_prompt}]
|
| 549 |
-
|
| 550 |
-
# CORREÇÃO: chat_history (quando type="messages") já é uma List[Dict[str, str]]
|
| 551 |
-
# Podemos simplesmente estendê-lo
|
| 552 |
messages.extend(chat_history)
|
| 553 |
-
|
| 554 |
-
# 3. Adicionar a nova mensagem do usuário
|
| 555 |
messages.append({"role": "user", "content": message})
|
| 556 |
|
| 557 |
payload = {
|
| 558 |
-
"model": MODELO_TEXTO,
|
| 559 |
"messages": messages,
|
| 560 |
-
"max_tokens": 1500,
|
| 561 |
"temperature": 0.7,
|
| 562 |
-
"stream": False
|
| 563 |
}
|
| 564 |
|
| 565 |
try:
|
|
@@ -573,15 +774,13 @@ def responder_chat(message, chat_history):
|
|
| 573 |
else:
|
| 574 |
return f"❌ Erro na resposta da API: Resposta vazia ou inesperada.\n{resultado}"
|
| 575 |
else:
|
| 576 |
-
# ATUALIZADO: Interpretar erro
|
| 577 |
return f"❌ {interpretar_erro_api(f'Erro {response.status_code}: {response.text}')}"
|
| 578 |
|
| 579 |
except Exception as e:
|
| 580 |
-
# ATUALIZADO: Interpretar erro
|
| 581 |
return f"❌ {interpretar_erro_api(str(e))}"
|
| 582 |
|
| 583 |
# ============================================
|
| 584 |
-
#
|
| 585 |
# ============================================
|
| 586 |
|
| 587 |
def criar_post_completo(texto, imagem_pil, tema):
|
|
@@ -594,16 +793,13 @@ def criar_post_completo(texto, imagem_pil, tema):
|
|
| 594 |
return None
|
| 595 |
|
| 596 |
try:
|
| 597 |
-
# Configurações
|
| 598 |
LARGURA_POST = 1080
|
| 599 |
ALTURA_IMAGEM = 1080
|
| 600 |
PADDING = 60
|
| 601 |
COR_FUNDO = (255, 255, 255)
|
| 602 |
COR_TEXTO = (0, 0, 0)
|
| 603 |
|
| 604 |
-
# Tentar carregar uma fonte (fallback para a fonte padrão do PIL)
|
| 605 |
try:
|
| 606 |
-
# Fontes comuns em ambientes Linux (como o Hugging Face Spaces)
|
| 607 |
fonte_texto = ImageFont.truetype("DejaVuSans.ttf", size=42)
|
| 608 |
fonte_titulo = ImageFont.truetype("DejaVuSans-Bold.ttf", size=55)
|
| 609 |
except IOError:
|
|
@@ -611,49 +807,35 @@ def criar_post_completo(texto, imagem_pil, tema):
|
|
| 611 |
fonte_texto = ImageFont.load_default()
|
| 612 |
fonte_titulo = ImageFont.load_default()
|
| 613 |
|
| 614 |
-
# 1. Redimensionar imagem original para 1080x1080 (formato Instagram)
|
| 615 |
imagem_quadrada = imagem_pil.resize((LARGURA_POST, ALTURA_IMAGEM), Image.Resampling.LANCZOS)
|
| 616 |
|
| 617 |
-
# 2. Preparar texto
|
| 618 |
-
# Usar o 'tema' como título e 'texto' como corpo
|
| 619 |
linhas_titulo = textwrap.wrap(tema.upper(), width=40)
|
| 620 |
-
linhas_texto = textwrap.wrap(texto, width=50)
|
| 621 |
|
| 622 |
-
|
| 623 |
-
# (altura da linha * num linhas)
|
| 624 |
-
altura_titulo = len(linhas_titulo) * 60
|
| 625 |
altura_texto = len(linhas_texto) * 45
|
| 626 |
-
|
| 627 |
-
altura_total_texto = altura_titulo + 20 + altura_texto + (PADDING * 2)
|
| 628 |
|
| 629 |
-
# 4. Criar nova imagem (canvas)
|
| 630 |
altura_total = ALTURA_IMAGEM + altura_total_texto
|
| 631 |
post_completo = Image.new('RGB', (LARGURA_POST, int(altura_total)), COR_FUNDO)
|
| 632 |
|
| 633 |
-
# 5. Colar imagem gerada
|
| 634 |
post_completo.paste(imagem_quadrada, (0, 0))
|
| 635 |
|
| 636 |
-
# 6. Desenhar texto
|
| 637 |
draw = ImageDraw.Draw(post_completo)
|
| 638 |
pos_y = ALTURA_IMAGEM + PADDING
|
| 639 |
|
| 640 |
-
# Desenhar Título
|
| 641 |
for linha in linhas_titulo:
|
| 642 |
-
# Centralizar título (opcional, mas fica melhor)
|
| 643 |
largura_linha = draw.textlength(linha, font=fonte_titulo)
|
| 644 |
pos_x_titulo = (LARGURA_POST - largura_linha) / 2
|
| 645 |
draw.text((pos_x_titulo, pos_y), linha, font=fonte_titulo, fill=COR_TEXTO)
|
| 646 |
-
pos_y += 60
|
| 647 |
|
| 648 |
-
pos_y += 20
|
| 649 |
|
| 650 |
-
# Desenhar Texto (Corpo)
|
| 651 |
for linha in linhas_texto:
|
| 652 |
draw.text((PADDING, pos_y), linha, font=fonte_texto, fill=COR_TEXTO)
|
| 653 |
-
pos_y += 45
|
| 654 |
|
| 655 |
-
# 7. Salvar em arquivo temporário
|
| 656 |
-
# Usar tempfile para criar um arquivo nomeado que o Gradio possa acessar
|
| 657 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
|
| 658 |
post_completo.save(f, 'PNG')
|
| 659 |
print(f"Arquivo temporário salvo em: {f.name}")
|
|
@@ -665,81 +847,102 @@ def criar_post_completo(texto, imagem_pil, tema):
|
|
| 665 |
|
| 666 |
def preparar_download(texto, imagem_pil, tema):
|
| 667 |
"""
|
| 668 |
-
Prepara o arquivo
|
| 669 |
Retorna o caminho do arquivo para o gr.File ou None.
|
| 670 |
"""
|
| 671 |
if not texto or not imagem_pil:
|
| 672 |
-
return None
|
| 673 |
|
| 674 |
caminho_arquivo = criar_post_completo(texto, imagem_pil, tema)
|
| 675 |
|
| 676 |
if caminho_arquivo:
|
| 677 |
-
# Retorna o caminho do arquivo para o componente gr.File
|
| 678 |
return caminho_arquivo
|
| 679 |
|
| 680 |
-
# Se falhar, retorna None (o botão de download não fará nada)
|
| 681 |
return None
|
| 682 |
|
| 683 |
-
|
| 684 |
# ============================================
|
| 685 |
-
# FUNÇÃO PRINCIPAL
|
| 686 |
# ============================================
|
| 687 |
|
| 688 |
-
def gerar_post_interface(tema, nicho, estilo, formato,
|
| 689 |
-
descricao_imagem, gerar_img,
|
| 690 |
-
estilo_img_input, qualidade_img_input, filtro_img_input,
|
| 691 |
progress=gr.Progress()):
|
| 692 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
-
# Início
|
| 695 |
progress(0, desc="🚀 Iniciando...")
|
| 696 |
time.sleep(0.3)
|
| 697 |
|
| 698 |
-
# Validação
|
| 699 |
progress(0.1, desc="✅ Validando...")
|
| 700 |
if not tema or len(tema.strip()) < 3:
|
| 701 |
status_final = criar_alerta('error', '⚠️ Digite um tema válido!')
|
| 702 |
-
|
| 703 |
-
return ("", None, status_final, 0, 0, 0)
|
| 704 |
time.sleep(0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
|
| 706 |
-
|
| 707 |
-
progress(0.2, desc="📝 Preparando prompt de texto...")
|
| 708 |
-
time.sleep(0.5)
|
| 709 |
-
|
| 710 |
-
# Gerando texto
|
| 711 |
progress(0.3, desc="🤖 Gerando texto (Llama 3.1)...")
|
| 712 |
-
# ATUALIZADO: 'formato' passado para gerar_texto
|
| 713 |
-
texto = gerar_texto(tema, nicho, estilo, formato)
|
| 714 |
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
| 716 |
status_final = criar_alerta('error', f'{texto}')
|
| 717 |
-
return (texto, None, status_final, 0, 0, 0)
|
| 718 |
|
| 719 |
progress(0.5, desc="✅ Texto pronto!")
|
| 720 |
time.sleep(0.5)
|
| 721 |
|
| 722 |
-
#
|
| 723 |
imagem = None
|
| 724 |
status_imagem = ""
|
| 725 |
if gerar_img:
|
| 726 |
-
# Descrição base em Português
|
| 727 |
descricao_pt = descricao_imagem or f"{tema} imagem"
|
| 728 |
|
| 729 |
-
# Chamar a nova função robusta
|
| 730 |
(imagem, status_imagem) = gerar_imagem_robusta(
|
| 731 |
-
descricao_pt,
|
| 732 |
-
estilo_img_input,
|
| 733 |
-
qualidade_img_input,
|
| 734 |
-
filtro_img_input,
|
| 735 |
-
progress
|
| 736 |
)
|
| 737 |
|
| 738 |
if imagem:
|
| 739 |
status_final = criar_alerta('success', f'🎉 Post completo gerado! ({status_imagem})')
|
| 740 |
else:
|
| 741 |
-
# status_imagem conterá a mensagem de erro (ex: "Todos os modelos falharam")
|
| 742 |
-
# ATUALIZADO: status_imagem já vem formatado com o erro
|
| 743 |
status_final = criar_alerta('warning', f'✅ Texto OK, mas imagem falhou: {status_imagem}')
|
| 744 |
else:
|
| 745 |
progress(0.7, desc="⏭️ Pulando geração de imagem...")
|
|
@@ -747,72 +950,65 @@ def gerar_post_interface(tema, nicho, estilo, formato, # ATUALIZADO: 'formato' a
|
|
| 747 |
|
| 748 |
time.sleep(0.5)
|
| 749 |
|
| 750 |
-
# Estatísticas
|
| 751 |
progress(0.9, desc="📊 Calculando estatísticas...")
|
| 752 |
palavras = len(texto.split())
|
| 753 |
caracteres = len(texto)
|
| 754 |
hashtags = texto.count('#')
|
| 755 |
time.sleep(0.3)
|
| 756 |
|
| 757 |
-
#
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
#
|
| 768 |
-
status_final_historico = status_final # Usar o status HTML (o Firestore lida bem com strings)
|
| 769 |
-
|
| 770 |
-
# Se a geração falhou, usamos a mensagem de status como preview
|
| 771 |
-
texto_preview = texto[:100].replace('\n', ' ') + "..." if not texto.startswith("❌") else status_final
|
| 772 |
-
|
| 773 |
history_entry = {
|
| 774 |
"Data/Hora": datetime.now(ZoneInfo("America/Bahia")).strftime("%Y-%m-%d %H:%M:%S"),
|
| 775 |
-
"Tema": tema,
|
| 776 |
-
"
|
| 777 |
-
"
|
| 778 |
-
"
|
| 779 |
-
"Texto (Preview)": texto_preview,
|
| 780 |
-
"Status": status_imagem or "Texto Gerado", # Salva o status da imagem (ex: "Gerada com FLUX")
|
| 781 |
}
|
| 782 |
-
|
| 783 |
-
# Salvar no Histórico (DB e cache)
|
| 784 |
atualizar_historico(history_entry)
|
| 785 |
|
| 786 |
-
#
|
| 787 |
-
|
| 788 |
-
#
|
| 789 |
|
| 790 |
-
|
| 791 |
-
|
|
|
|
| 792 |
|
| 793 |
|
| 794 |
# ============================================
|
| 795 |
-
# INTERFACE GRADIO
|
| 796 |
# ============================================
|
| 797 |
|
| 798 |
-
# ATUALIZADO: gr.themes.Glass é obsoleto no Gradio 4.x. Usando gr.themes.Soft.
|
| 799 |
custom_theme = gr.themes.Soft(
|
| 800 |
-
primary_hue="blue",
|
| 801 |
-
secondary_hue="gray",
|
| 802 |
-
neutral_hue="stone",
|
| 803 |
-
font=["Helvetica", "Georgia", "sans-serif"]
|
| 804 |
)
|
| 805 |
|
|
|
|
|
|
|
| 806 |
|
| 807 |
-
with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
| 808 |
|
| 809 |
gr.Markdown("""
|
| 810 |
-
# 🚀 Gerador de Posts e Assistente de Mídias Sociais
|
| 811 |
-
### Powered by Hugging Face, Gradio
|
| 812 |
""")
|
| 813 |
|
| 814 |
-
with gr.Tabs():
|
| 815 |
-
with gr.TabItem("✨ Gerar Post"):
|
| 816 |
with gr.Row():
|
| 817 |
with gr.Column(scale=1):
|
| 818 |
gr.Markdown("### ⚙️ 1. Configurações do Texto")
|
|
@@ -836,13 +1032,16 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 836 |
placeholder="Ex: Transforme seu corpo, transforme sua vida"
|
| 837 |
)
|
| 838 |
|
| 839 |
-
# ATUALIZADO: Reativado (Solicitação 1)
|
| 840 |
formato_input = gr.Radio(
|
| 841 |
choices=list(FORMATO_CONFIGS.keys()),
|
| 842 |
label="Escolha o Formato de Saída",
|
| 843 |
value=list(FORMATO_CONFIGS.keys())[0],
|
| 844 |
-
interactive=True
|
| 845 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
)
|
| 847 |
|
| 848 |
gr.Markdown("### 🎨 2. Configurações da Imagem (Opcional)")
|
|
@@ -852,14 +1051,12 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 852 |
value=False
|
| 853 |
)
|
| 854 |
|
| 855 |
-
# Inputs de Imagem (inicialmente ocultos)
|
| 856 |
descricao_img_input = gr.Textbox(
|
| 857 |
label="Descrição da imagem (em Português)",
|
| 858 |
placeholder="Ex: Pessoa correndo ao nascer do sol",
|
| 859 |
visible=False
|
| 860 |
)
|
| 861 |
|
| 862 |
-
# ATUALIZADO: Usando ESTILOS_DE_IMAGEM (Solicitação 2)
|
| 863 |
estilo_img_input = gr.Dropdown(
|
| 864 |
label="Estilo da Imagem",
|
| 865 |
choices=list(ESTILOS_DE_IMAGEM.keys()),
|
|
@@ -884,9 +1081,7 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 884 |
interactive=True
|
| 885 |
)
|
| 886 |
|
| 887 |
-
# Função para mostrar/ocultar todos os controles de imagem
|
| 888 |
def toggle_descricao_img(gerar):
|
| 889 |
-
# ATUALIZADO: Retorna 4 componentes
|
| 890 |
return (
|
| 891 |
gr.Textbox(visible=gerar),
|
| 892 |
gr.Dropdown(visible=gerar),
|
|
@@ -899,13 +1094,14 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 899 |
inputs=[gerar_img_checkbox],
|
| 900 |
outputs=[descricao_img_input, estilo_img_input, qualidade_img_input, filtro_img_input]
|
| 901 |
)
|
|
|
|
|
|
|
| 902 |
|
| 903 |
gerar_btn = gr.Button("✨ Gerar Post", variant="primary")
|
| 904 |
|
| 905 |
with gr.Column(scale=1):
|
| 906 |
gr.Markdown("### 📋 3. Resultado")
|
| 907 |
|
| 908 |
-
# ATUALIZADO: Status agora é HTML para alertas coloridos
|
| 909 |
status_output = gr.HTML(
|
| 910 |
label="Status",
|
| 911 |
value=criar_alerta('info', 'Pronto para gerar!')
|
|
@@ -914,21 +1110,19 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 914 |
texto_output = gr.Textbox(
|
| 915 |
label="Texto Gerado",
|
| 916 |
lines=10,
|
| 917 |
-
interactive=
|
| 918 |
-
show_copy_button=True
|
| 919 |
)
|
| 920 |
|
| 921 |
-
# Botões de ação adicionados
|
| 922 |
with gr.Row():
|
| 923 |
copiar_btn = gr.Button("📋 Copiar Texto", variant="secondary")
|
| 924 |
limpar_btn = gr.Button("🧹 Limpar Tudo", variant="stop")
|
| 925 |
-
|
| 926 |
imagem_output = gr.Image(
|
| 927 |
label="Imagem Gerada",
|
| 928 |
type="pil"
|
| 929 |
)
|
| 930 |
|
| 931 |
-
# NOVO: Seção de Download
|
| 932 |
gr.Markdown("### 📥 4. Download")
|
| 933 |
download_btn = gr.Button(
|
| 934 |
"Baixar Post Completo (Imagem + Texto)",
|
|
@@ -936,21 +1130,17 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 936 |
)
|
| 937 |
download_output = gr.File(
|
| 938 |
label="Clique para baixar",
|
| 939 |
-
visible=True
|
| 940 |
)
|
| 941 |
-
|
| 942 |
|
| 943 |
-
# Estatísticas adicionadas
|
| 944 |
gr.Markdown("### 📊 Estatísticas do Texto")
|
| 945 |
with gr.Row():
|
| 946 |
palavras_output = gr.Number(label="Palavras", value=0, interactive=False)
|
| 947 |
caracteres_output = gr.Number(label="Caracteres", value=0, interactive=False)
|
| 948 |
hashtags_output = gr.Number(label="Hashtags", value=0, interactive=False)
|
| 949 |
|
| 950 |
-
|
| 951 |
gr.Markdown("### 💡 Experimente estes exemplos:")
|
| 952 |
-
|
| 953 |
-
# ATUALIZADO: Exemplos agora incluem o formato
|
| 954 |
gr.Examples(
|
| 955 |
examples=[
|
| 956 |
[NICHOS_DISPONIVEIS[2], ESTILOS_DISPONIVEIS[0], "Frases marcantes de pessoas importantes", "Instagram (Post)"],
|
|
@@ -958,119 +1148,90 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 958 |
[NICHOS_DISPONIVEIS[5], ESTILOS_DISPONIVEIS[3], "O futuro da IA em 2025", "LinkedIn (Artigo)"],
|
| 959 |
[NICHOS_DISPONIVEIS[4], ESTILOS_DISPONIVEIS[1], "Melhores destinos para lua de mel na Europa", "Twitter/X (Curto)"],
|
| 960 |
],
|
| 961 |
-
inputs=[nicho_input, estilo_input, tema_input, formato_input],
|
| 962 |
outputs=[texto_output, imagem_output, status_output]
|
| 963 |
)
|
| 964 |
-
|
| 965 |
-
# ============================================
|
| 966 |
-
# CONECTAR EVENTOS (Bloco Novo)
|
| 967 |
-
# ============================================
|
| 968 |
|
| 969 |
-
|
| 970 |
-
# CORREÇÃO: Capturar o evento de clique para usá-lo na outra aba
|
| 971 |
-
click_event = gerar_btn.click(
|
| 972 |
-
fn=gerar_post_interface,
|
| 973 |
-
inputs=[
|
| 974 |
-
tema_input, nicho_input, estilo_input,
|
| 975 |
-
formato_input, # ATUALIZADO: 'formato_input' adicionado
|
| 976 |
-
descricao_img_input, gerar_img_checkbox,
|
| 977 |
-
estilo_img_input, qualidade_img_input, filtro_img_input # Novos inputs
|
| 978 |
-
],
|
| 979 |
-
outputs=[
|
| 980 |
-
texto_output, imagem_output, status_output,
|
| 981 |
-
palavras_output, caracteres_output, hashtags_output
|
| 982 |
-
],
|
| 983 |
-
show_progress="full"
|
| 984 |
-
)
|
| 985 |
-
|
| 986 |
-
# Botão copiar
|
| 987 |
-
copiar_btn.click(
|
| 988 |
-
fn=copiar_feedback,
|
| 989 |
-
inputs=[texto_output],
|
| 990 |
-
outputs=[status_output]
|
| 991 |
-
)
|
| 992 |
-
|
| 993 |
-
# Botão limpar
|
| 994 |
-
limpar_btn.click(
|
| 995 |
-
fn=limpar_tudo,
|
| 996 |
-
inputs=[],
|
| 997 |
-
outputs=[
|
| 998 |
-
texto_output, imagem_output, status_output,
|
| 999 |
-
palavras_output, caracteres_output, hashtags_output,
|
| 1000 |
-
formato_input, # ATUALIZADO: 'formato_input' adicionado
|
| 1001 |
-
estilo_img_input, qualidade_img_input, filtro_img_input, # Limpa novos inputs
|
| 1002 |
-
download_output # Limpa o arquivo de download
|
| 1003 |
-
]
|
| 1004 |
-
)
|
| 1005 |
-
|
| 1006 |
-
# NOVO: Botão de Download
|
| 1007 |
-
download_btn.click(
|
| 1008 |
-
fn=preparar_download,
|
| 1009 |
-
inputs=[texto_output, imagem_output, tema_input],
|
| 1010 |
-
outputs=[download_output]
|
| 1011 |
-
)
|
| 1012 |
-
|
| 1013 |
-
# ============================================
|
| 1014 |
-
# NOVA ABA: CHATBOT
|
| 1015 |
-
# ============================================
|
| 1016 |
-
with gr.TabItem("💬 Chatbot Assistente"):
|
| 1017 |
gr.Markdown("### 🤖 Assistente Virtual")
|
| 1018 |
gr.Markdown("Faça perguntas sobre mídias sociais, IA, peça ideias rápidas ou qualquer outro tópico.")
|
| 1019 |
|
| 1020 |
-
# 1. Instanciar o Chatbot com os parâmetros solicitados
|
| 1021 |
chatbot_para_interface = gr.Chatbot(
|
| 1022 |
-
height=500,
|
| 1023 |
-
type="messages"
|
| 1024 |
)
|
| 1025 |
|
| 1026 |
-
# 2. Passar o componente chatbot para a ChatInterface
|
| 1027 |
gr.ChatInterface(
|
| 1028 |
fn=responder_chat,
|
| 1029 |
title="Assistente Virtual",
|
| 1030 |
description="Converse com o Llama 3.1 para obter ajuda e insights.",
|
| 1031 |
examples=[
|
| 1032 |
-
"O que é um 'gancho' para Instagram?",
|
| 1033 |
-
"Me dê 3 ideias de post para um nicho de 'Fitness'",
|
| 1034 |
-
"Como o Llama 3.1 funciona?",
|
| 1035 |
"Qual a diferença entre um post para Instagram e um para LinkedIn?"
|
| 1036 |
],
|
| 1037 |
-
chatbot=chatbot_para_interface,
|
| 1038 |
textbox=gr.Textbox(placeholder="Digite sua mensagem aqui...", scale=7),
|
| 1039 |
-
submit_btn="Enviar"
|
| 1040 |
)
|
| 1041 |
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
gr.Markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
|
| 1046 |
-
# Dataframe de exibição que é atualizado pelo output do botão
|
| 1047 |
historico_display = gr.Dataframe(
|
| 1048 |
-
headers=["Data/Hora", "Tema", "Nicho", "Estilo", "Formato", "Texto (Preview)", "Status"],
|
| 1049 |
-
interactive=
|
| 1050 |
-
value=_formatar_historico_para_df(carregar_historico_inicial()),
|
| 1051 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
# ============================================
|
| 1056 |
-
# Conectar o evento de clique do 'gerar_btn' (da outra aba)
|
| 1057 |
-
# para também atualizar 'historico_display' *depois* que a geração terminar.
|
| 1058 |
-
click_event.then(
|
| 1059 |
-
fn=recarregar_e_formatar_historico,
|
| 1060 |
-
inputs=None,
|
| 1061 |
-
outputs=[historico_display]
|
| 1062 |
)
|
| 1063 |
-
|
| 1064 |
-
gr.Markdown("""
|
| 1065 |
-
---
|
| 1066 |
-
*Nota sobre Persistência:*
|
| 1067 |
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
|
| 1072 |
-
|
| 1073 |
-
with gr.TabItem("⚙️ Configurações"):
|
| 1074 |
gr.Markdown("### Configurações do Gerador")
|
| 1075 |
gr.Markdown("**Modelo de Texto (LLM):** Llama 3.1 8B (Usado para Posts e Chatbot)")
|
| 1076 |
gr.Markdown("**Modelos de Imagem:** FLUX.1-schnell, FLUX.1-dev, SDXL 1.0")
|
|
@@ -1078,17 +1239,19 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 1078 |
gr.Markdown("**API Provider:** Hugging Face Inference")
|
| 1079 |
gr.Markdown("**Database:** Google Firestore (via Firebase Admin)")
|
| 1080 |
gr.Markdown("---")
|
| 1081 |
-
gr.Markdown("#### Funcionalidades:")
|
| 1082 |
gr.Markdown("- **Gerador de Posts:** Cria posts completos com texto e imagem.")
|
| 1083 |
gr.Markdown("- **Seleção de Formato:** Permite escolher o formato do texto (Instagram, Twitter, LinkedIn).")
|
| 1084 |
gr.Markdown("- **Controles Avançados:** Permite seleção de Estilo, Qualidade e Filtros para a imagem.")
|
| 1085 |
gr.Markdown("- **Download de Post:** Combina texto e imagem em um único arquivo PNG para download.")
|
| 1086 |
gr.Markdown("- **Chatbot Assistente:** Converse com a IA para ideias e perguntas rápidas.")
|
| 1087 |
gr.Markdown("- **Histórico Persistente:** Salva os *posts gerados* no Firestore.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
|
| 1089 |
-
|
| 1090 |
-
# ABA 4: Sobre
|
| 1091 |
-
with gr.TabItem("ℹ️ Sobre"):
|
| 1092 |
gr.Markdown("""
|
| 1093 |
### Sobre Este Projeto
|
| 1094 |
|
|
@@ -1100,8 +1263,9 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 1100 |
- **Llama 3.1 8B (geração de texto e chatbot)**
|
| 1101 |
- **FLUX.1 & SDXL (geração de imagens)**
|
| 1102 |
- Opus-MT (tradução)
|
| 1103 |
-
- **Firebase Firestore (Banco de Dados)**
|
| 1104 |
- **PIL (Python Imaging Library) (para composição de posts)**
|
|
|
|
| 1105 |
|
| 1106 |
**Como funciona:**
|
| 1107 |
1. **Gerar Post:** Você define o tema, nicho, estilo e **formato** do *texto*.
|
|
@@ -1109,7 +1273,7 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 1109 |
3. O sistema otimiza o prompt, traduz para inglês e usa o sistema de *fallback* de modelos (baseado na *Qualidade*) para gerar a imagem.
|
| 1110 |
4. **Download:** Após a geração, você pode clicar em "Baixar Post Completo" para salvar um PNG com a imagem e o texto formatado.
|
| 1111 |
5. **Chatbot:** Você pode conversar diretamente com a IA na aba 'Chatbot Assistente' para tirar dúvidas.
|
| 1112 |
-
6. **Histórico:** Os posts gerados são salvos no Firestore.
|
| 1113 |
|
| 1114 |
**Desenvolvido por:** Wilder Paz
|
| 1115 |
""")
|
|
@@ -1117,9 +1281,132 @@ with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot") as demo:
|
|
| 1117 |
# Footer
|
| 1118 |
gr.Markdown("""
|
| 1119 |
---
|
| 1120 |
-
**Curso de Python com IA** | 🤖 Powered by Llama 3.1 & FLUX | ⚡ Hugging Face Spaces + Gradio + Firestore
|
| 1121 |
""")
|
| 1122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
# Lançar aplicação
|
| 1124 |
if __name__ == "__main__":
|
| 1125 |
demo.launch()
|
|
|
|
| 1 |
# CHATBOT E GERADOR DE POSTS PARA REDES SOCIAIS
|
| 2 |
+
# VERSÃO COMPLETA (MERGE)
|
| 3 |
+
# Funcionalidades:
|
| 4 |
+
# - Geração Avançada de Imagem (Estilo, Qualidade, Filtro)
|
| 5 |
+
# - Download de Post Composto (Imagem + Texto)
|
| 6 |
+
# - Chatbot Assistente
|
| 7 |
+
# - Sistema de Cache local
|
| 8 |
+
# - Persistência de Histórico e Analytics no Firebase
|
| 9 |
+
# - Aba de Histórico com Busca, Filtros e Favoritos
|
| 10 |
+
# - Carregamento de posts antigos do histórico
|
| 11 |
|
| 12 |
import gradio as gr
|
| 13 |
import requests
|
| 14 |
import os
|
| 15 |
+
import json
|
| 16 |
+
import time
|
| 17 |
from datetime import datetime
|
| 18 |
from zoneinfo import ZoneInfo
|
| 19 |
from PIL import Image, ImageDraw, ImageFont
|
| 20 |
from io import BytesIO
|
| 21 |
from huggingface_hub import InferenceClient
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
import hashlib
|
| 24 |
+
import tempfile
|
| 25 |
+
import textwrap
|
| 26 |
|
| 27 |
# Importar firebase-admin
|
| 28 |
import firebase_admin
|
|
|
|
| 35 |
if not HUGGINGFACE_API_KEY:
|
| 36 |
print("⚠️ API Key do Hugging Face não configurada! Certifique-se de que a variável de ambiente 'Capoeira' está definida.")
|
| 37 |
|
| 38 |
+
# URLs e modelos
|
| 39 |
BASE_URL = "https://router.huggingface.co/v1"
|
| 40 |
MODELO_TEXTO = "meta-llama/Llama-3.1-8B-Instruct"
|
| 41 |
MODELO_TRADUCA = "Helsinki-NLP/opus-mt-pt-en"
|
| 42 |
|
|
|
|
| 43 |
MODELOS_IMAGEM = [
|
| 44 |
{
|
| 45 |
"nome": "FLUX.1-schnell",
|
|
|
|
| 61 |
}
|
| 62 |
]
|
| 63 |
|
|
|
|
| 64 |
# Headers para requisições
|
| 65 |
headers = {
|
| 66 |
"Authorization": f"Bearer {HUGGINGFACE_API_KEY}",
|
|
|
|
| 90 |
"Tutorial/Passo a Passo",
|
| 91 |
]
|
| 92 |
|
|
|
|
| 93 |
ESTILOS_DE_IMAGEM = {
|
| 94 |
"Nenhum (Automático)": "standard photography, high quality, 4k",
|
| 95 |
"Fotografia Vintage": "vintage photography, retro style, film grain, analog",
|
|
|
|
| 108 |
"Minimalista": "minimalist, clean background, simple, elegant",
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
| 111 |
FILTROS_IMAGEM = {
|
| 112 |
"Nenhum": "",
|
| 113 |
"Preto e Branco": "black and white, monochrome, high contrast",
|
|
|
|
| 117 |
"Frio (Moderno)": "cool tones, modern aesthetic, clean, desaturated blues",
|
| 118 |
}
|
| 119 |
|
|
|
|
|
|
|
|
|
|
| 120 |
FORMATO_CONFIGS = {
|
| 121 |
"Instagram (Post)": {"tamanho": "100-150 palavras", "estrutura": "gancho inicial + desenvolvimento + call-to-action", "tom_adicional": "próximo, empático e motivador", "max_tokens": 350, "limite_palavras_ia": "150 palavras", "hashtags": "Incluir 4-5 hashtags relevantes no final. Incluir no máximo 3 emojis relevantes no texto."},
|
| 122 |
"Twitter/X (Curto)": {"tamanho": "Até 280 caracteres", "estrutura": "frase de impacto + link/hashtag", "tom_adicional": "direto e conciso, ideal para tweets", "max_tokens": 150, "limite_palavras_ia": "280 caracteres", "hashtags": "Incluir no máximo 2 hashtags."},
|
| 123 |
+
# CORREÇÃO: A vírgula entre "autoritário" e "focado" foi removida, unindo as duas strings.
|
| 124 |
"LinkedIn (Artigo)": {"tamanho": "250-400 palavras", "estrutura": "título chamativo + desenvolvimento profissional + reflexão", "tom_adicional": "profissional e autoritário, focado em insights", "max_tokens": 700, "limite_palavras_ia": "400 palavras", "hashtags": "Incluir 3-4 hashtags profissionais no final."},
|
| 125 |
}
|
| 126 |
|
| 127 |
+
# Variáveis globais
|
|
|
|
|
|
|
| 128 |
db = None
|
| 129 |
+
post_history = []
|
| 130 |
+
analytics = {}
|
| 131 |
+
CACHE_DIR = Path("post_cache")
|
| 132 |
+
CACHE_DIR.mkdir(exist_ok=True)
|
| 133 |
|
| 134 |
|
| 135 |
# ============================================
|
| 136 |
+
# FUNÇÕES DE PERSISTÊNCIA (FIREBASE)
|
| 137 |
# ============================================
|
| 138 |
|
| 139 |
def _inicializar_firestore():
|
|
|
|
| 141 |
Inicializa o Firebase Admin SDK usando as credenciais
|
| 142 |
armazenadas nos Secrets do Hugging Face Spaces.
|
| 143 |
"""
|
| 144 |
+
global db, analytics
|
| 145 |
|
|
|
|
| 146 |
secret_name = "FIREBASE_SERVICE_ACCOUNT_JSON"
|
| 147 |
secret_json_string = os.environ.get(secret_name)
|
| 148 |
|
|
|
|
| 150 |
print(f"❌ Erro de Configuração do Firebase: Secret '{secret_name}' não encontrado.")
|
| 151 |
print("Usando apenas histórico de sessão (temporário).")
|
| 152 |
db = None
|
| 153 |
+
analytics = {"status": "Não conectado"}
|
| 154 |
return
|
| 155 |
|
| 156 |
if not firebase_admin._apps:
|
| 157 |
try:
|
|
|
|
| 158 |
service_account_info = json.loads(secret_json_string)
|
|
|
|
|
|
|
| 159 |
cred = credentials.Certificate(service_account_info)
|
|
|
|
| 160 |
firebase_admin.initialize_app(cred)
|
| 161 |
db = firestore.client()
|
| 162 |
print("✅ Firestore inicializado com sucesso.")
|
| 163 |
+
# Inicializar/Carregar Analytics do Firestore
|
| 164 |
+
_carregar_analytics_firestore()
|
| 165 |
except Exception as e:
|
| 166 |
print(f"❌ Erro ao inicializar Firestore. Usando histórico de sessão. Detalhe: {e}")
|
| 167 |
db = None
|
| 168 |
+
analytics = {"status": f"Erro de conexão: {e}"}
|
| 169 |
|
| 170 |
def _adicionar_post_firestore(entry):
|
|
|
|
|
|
|
|
|
|
| 171 |
if db:
|
| 172 |
try:
|
|
|
|
| 173 |
db.collection('posts').add(entry)
|
| 174 |
return True
|
| 175 |
except Exception as e:
|
|
|
|
| 178 |
return False
|
| 179 |
|
| 180 |
def _obter_historico_firestore():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
if db:
|
| 182 |
try:
|
| 183 |
+
posts_query = db.collection('posts').order_by('Data/Hora', direction=firestore.Query.DESCENDING).limit(100) # Aumentado limite para busca
|
|
|
|
| 184 |
posts_stream = posts_query.stream()
|
|
|
|
| 185 |
history = [post.to_dict() for post in posts_stream]
|
| 186 |
return history
|
| 187 |
except Exception as e:
|
|
|
|
| 190 |
return []
|
| 191 |
|
| 192 |
def atualizar_historico(entry):
|
| 193 |
+
"""Salva no Firestore e atualiza o cache de sessão local."""
|
|
|
|
|
|
|
| 194 |
global post_history
|
|
|
|
|
|
|
| 195 |
_adicionar_post_firestore(entry)
|
| 196 |
+
# Adiciona no início da lista local
|
|
|
|
| 197 |
post_history.insert(0, entry)
|
| 198 |
+
# Garante que a lista local não cresça indefinidamente
|
| 199 |
+
if len(post_history) > 100:
|
| 200 |
+
post_history = post_history[:100]
|
| 201 |
return post_history
|
| 202 |
|
| 203 |
def carregar_historico_inicial():
|
| 204 |
+
"""Carrega o histórico do Firestore ao iniciar o app."""
|
|
|
|
|
|
|
|
|
|
| 205 |
global post_history
|
|
|
|
|
|
|
| 206 |
historico_db = _obter_historico_firestore()
|
|
|
|
| 207 |
if historico_db:
|
| 208 |
post_history = historico_db
|
|
|
|
|
|
|
| 209 |
return post_history
|
| 210 |
|
| 211 |
+
# ============================================
|
| 212 |
+
# FUNÇÕES DE ANALYTICS
|
| 213 |
+
# ============================================
|
| 214 |
+
|
| 215 |
+
def _carregar_analytics_firestore():
|
| 216 |
+
"""Carrega o documento único de analytics do Firestore."""
|
| 217 |
+
global analytics
|
| 218 |
+
if db:
|
| 219 |
+
try:
|
| 220 |
+
doc_ref = db.collection('analytics').document('summary')
|
| 221 |
+
doc = doc_ref.get()
|
| 222 |
+
if doc.exists:
|
| 223 |
+
analytics = doc.to_dict()
|
| 224 |
+
print("✅ Analytics carregados do Firestore.")
|
| 225 |
+
else:
|
| 226 |
+
# Se não existir, inicializa
|
| 227 |
+
analytics = {
|
| 228 |
+
"total_posts": 0,
|
| 229 |
+
"posts_por_nicho": {},
|
| 230 |
+
"posts_por_estilo": {},
|
| 231 |
+
"total_palavras": 0,
|
| 232 |
+
"total_imagens": 0,
|
| 233 |
+
"cache_hits": 0,
|
| 234 |
+
"cache_misses": 0,
|
| 235 |
+
"total_favoritos": 0 # Adicionado
|
| 236 |
+
}
|
| 237 |
+
doc_ref.set(analytics)
|
| 238 |
+
print("Analytics inicializados no Firestore.")
|
| 239 |
+
except Exception as e:
|
| 240 |
+
print(f"❌ Erro ao carregar Analytics: {e}")
|
| 241 |
+
analytics = {"status": f"Erro: {e}"}
|
| 242 |
+
|
| 243 |
+
def _salvar_analytics_firestore():
|
| 244 |
+
"""Salva o estado atual de analytics no Firestore."""
|
| 245 |
+
if db:
|
| 246 |
+
try:
|
| 247 |
+
db.collection('analytics').document('summary').set(analytics)
|
| 248 |
+
print("Analytics salvos no Firestore.")
|
| 249 |
+
except Exception as e:
|
| 250 |
+
print(f"❌ Erro ao salvar Analytics: {e}")
|
| 251 |
+
|
| 252 |
+
def atualizar_analytics(nicho, estilo, palavras, imagem_gerada, cache_hit, favorito):
|
| 253 |
+
"""Atualiza as métricas de analytics (agora salva no Firestore)."""
|
| 254 |
+
global analytics
|
| 255 |
+
|
| 256 |
+
analytics['total_posts'] = analytics.get('total_posts', 0) + 1
|
| 257 |
+
analytics['total_palavras'] = analytics.get('total_palavras', 0) + palavras
|
| 258 |
+
|
| 259 |
+
if imagem_gerada:
|
| 260 |
+
analytics['total_imagens'] = analytics.get('total_imagens', 0) + 1
|
| 261 |
+
|
| 262 |
+
if cache_hit:
|
| 263 |
+
analytics['cache_hits'] = analytics.get('cache_hits', 0) + 1
|
| 264 |
+
else:
|
| 265 |
+
analytics['cache_misses'] = analytics.get('cache_misses', 0) + 1
|
| 266 |
+
|
| 267 |
+
if favorito:
|
| 268 |
+
analytics['total_favoritos'] = analytics.get('total_favoritos', 0) + 1
|
| 269 |
+
|
| 270 |
+
# Atualizar contadores de nicho e estilo
|
| 271 |
+
nicho_counts = analytics.get('posts_por_nicho', {})
|
| 272 |
+
nicho_counts[nicho] = nicho_counts.get(nicho, 0) + 1
|
| 273 |
+
analytics['posts_por_nicho'] = nicho_counts
|
| 274 |
+
|
| 275 |
+
estilo_counts = analytics.get('posts_por_estilo', {})
|
| 276 |
+
estilo_counts[estilo] = estilo_counts.get(estilo, 0) + 1
|
| 277 |
+
analytics['posts_por_estilo'] = estilo_counts
|
| 278 |
+
|
| 279 |
+
# Salvar no Firestore
|
| 280 |
+
_salvar_analytics_firestore()
|
| 281 |
+
|
| 282 |
+
def gerar_relatorio_analytics():
|
| 283 |
+
"""Formata os dados de analytics para exibição no Gradio como Markdown."""
|
| 284 |
+
global analytics
|
| 285 |
+
if not analytics or 'status' in analytics or analytics.get("total_posts", 0) == 0:
|
| 286 |
+
return "📊 Nenhum post gerado ainda."
|
| 287 |
+
|
| 288 |
+
# Ordenar os dicionários por valor (mais usados primeiro)
|
| 289 |
+
posts_por_nicho_sorted = dict(sorted(analytics.get('posts_por_nicho', {}).items(), key=lambda item: item[1], reverse=True))
|
| 290 |
+
posts_por_estilo_sorted = dict(sorted(analytics.get('posts_por_estilo', {}).items(), key=lambda item: item[1], reverse=True))
|
| 291 |
+
|
| 292 |
+
total_reqs = analytics.get('cache_hits', 0) + analytics.get('cache_misses', 0)
|
| 293 |
+
taxa_cache_hit = (analytics.get('cache_hits', 0) / total_reqs * 100) if total_reqs > 0 else 0
|
| 294 |
+
|
| 295 |
+
nicho_top = max(analytics["posts_por_nicho"].items(), key=lambda x: x[1]) if analytics.get("posts_por_nicho") else ("N/A", 0)
|
| 296 |
+
estilo_top = max(analytics["posts_por_estilo"].items(), key=lambda x: x[1]) if analytics.get("posts_por_estilo") else ("N/A", 0)
|
| 297 |
+
|
| 298 |
+
relatorio = f"""📊 **RELATÓRIO DE ANALYTICS**
|
| 299 |
+
**Geral:**
|
| 300 |
+
• Total de posts: {analytics['total_posts']}
|
| 301 |
+
• Total de palavras: {analytics['total_palavras']:,}
|
| 302 |
+
• Total de imagens: {analytics['total_imagens']}
|
| 303 |
+
• Total de favoritos: {analytics.get('total_favoritos', 0)}
|
| 304 |
+
• Média de palavras/post: {analytics['total_palavras'] // analytics['total_posts'] if analytics['total_posts'] > 0 else 0}
|
| 305 |
+
|
| 306 |
+
**Performance:**
|
| 307 |
+
• Cache hits: {analytics['cache_hits']}
|
| 308 |
+
• Cache misses: {analytics['cache_misses']}
|
| 309 |
+
• Taxa de cache: {taxa_cache_hit:.1f}%
|
| 310 |
+
|
| 311 |
+
**Preferências:**
|
| 312 |
+
• Nicho mais usado: {nicho_top[0]} ({nicho_top[1]} posts)
|
| 313 |
+
• Estilo mais usado: {estilo_top[0]} ({estilo_top[1]} posts)
|
| 314 |
+
"""
|
| 315 |
+
return relatorio
|
| 316 |
+
|
| 317 |
+
def resetar_analytics():
|
| 318 |
+
"""Reseta os dados de analytics no Firestore e localmente."""
|
| 319 |
+
global analytics
|
| 320 |
+
analytics = {
|
| 321 |
+
"total_posts": 0,
|
| 322 |
+
"posts_por_nicho": {},
|
| 323 |
+
"posts_por_estilo": {},
|
| 324 |
+
"total_palavras": 0,
|
| 325 |
+
"total_imagens": 0,
|
| 326 |
+
"cache_hits": 0,
|
| 327 |
+
"cache_misses": 0,
|
| 328 |
+
"total_favoritos": 0
|
| 329 |
+
}
|
| 330 |
+
_salvar_analytics_firestore()
|
| 331 |
+
# Limpar cache local
|
| 332 |
+
for f in CACHE_DIR.glob('*'):
|
| 333 |
+
f.unlink()
|
| 334 |
+
print("Analytics e Cache resetados.")
|
| 335 |
+
return gerar_relatorio_analytics()
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
# ============================================
|
| 339 |
+
# FUNÇÕES DE CACHE
|
| 340 |
+
# ============================================
|
| 341 |
+
|
| 342 |
+
def criar_cache_key(tema, nicho, estilo, formato):
|
| 343 |
+
"""Cria uma chave de hash SHA-256 para os inputs."""
|
| 344 |
+
input_string = f"{tema}-{nicho}-{estilo}-{formato}".encode('utf-8')
|
| 345 |
+
return hashlib.sha256(input_string).hexdigest()
|
| 346 |
+
|
| 347 |
+
def salvar_no_cache(key, data):
|
| 348 |
+
"""Salva os dados (texto e imagem) em cache."""
|
| 349 |
+
cache_file = CACHE_DIR / f"{key}.json"
|
| 350 |
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
| 351 |
+
json.dump({"texto": data["texto"], "imagem_path": data.get("imagem_path")}, f)
|
| 352 |
+
|
| 353 |
+
def buscar_no_cache(key):
|
| 354 |
+
"""Busca dados do cache. Retorna (texto, imagem_path) ou (None, None)."""
|
| 355 |
+
cache_file = CACHE_DIR / f"{key}.json"
|
| 356 |
+
if cache_file.exists():
|
| 357 |
+
try:
|
| 358 |
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
| 359 |
+
data = json.load(f)
|
| 360 |
+
|
| 361 |
+
texto = data.get("texto")
|
| 362 |
+
imagem_path = data.get("imagem_path")
|
| 363 |
+
imagem = None
|
| 364 |
+
|
| 365 |
+
if imagem_path:
|
| 366 |
+
img_cache_file = CACHE_DIR / imagem_path
|
| 367 |
+
if img_cache_file.exists():
|
| 368 |
+
imagem = Image.open(img_cache_file)
|
| 369 |
+
else:
|
| 370 |
+
return None, None
|
| 371 |
+
|
| 372 |
+
return texto, imagem
|
| 373 |
+
except Exception as e:
|
| 374 |
+
print(f"Erro ao ler cache {key}: {e}")
|
| 375 |
+
return None, None
|
| 376 |
+
return None, None
|
| 377 |
+
|
| 378 |
+
def salvar_imagem_cache(key, imagem_pil):
|
| 379 |
+
"""Salva a imagem PIL no diretório de cache e retorna o nome do arquivo."""
|
| 380 |
+
if not imagem_pil:
|
| 381 |
+
return None
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
imagem_path = f"{key}_img.png"
|
| 385 |
+
imagem_pil.save(CACHE_DIR / imagem_path)
|
| 386 |
+
return imagem_path
|
| 387 |
+
except Exception as e:
|
| 388 |
+
print(f"Erro ao salvar imagem no cache: {e}")
|
| 389 |
+
return None
|
| 390 |
|
| 391 |
# ============================================
|
| 392 |
+
# HELPER FUNCTIONS
|
| 393 |
# ============================================
|
| 394 |
+
|
| 395 |
def _formatar_historico_para_df(history_list):
|
| 396 |
+
"""Formata a lista de histórico (dicionários) para o Dataframe (lista de listas)."""
|
|
|
|
|
|
|
|
|
|
| 397 |
formatted_list = []
|
| 398 |
if not history_list:
|
| 399 |
return []
|
| 400 |
|
| 401 |
+
# Colunas: ["⭐", "Data/Hora", "Tema", "Nicho", "Estilo", "Formato", "Texto (Preview)", "Status"]
|
| 402 |
for entry in history_list:
|
| 403 |
+
texto_preview = entry.get("Texto", "")[:100].replace('\n', ' ') + "..."
|
| 404 |
formatted_list.append([
|
| 405 |
+
"⭐" if entry.get("Favorito") else "",
|
| 406 |
entry.get("Data/Hora", ""),
|
| 407 |
entry.get("Tema", ""),
|
| 408 |
entry.get("Nicho", ""),
|
| 409 |
entry.get("Estilo", ""),
|
| 410 |
entry.get("Formato", ""),
|
| 411 |
+
texto_preview,
|
| 412 |
entry.get("Status", "")
|
| 413 |
])
|
| 414 |
return formatted_list
|
| 415 |
|
|
|
|
|
|
|
|
|
|
| 416 |
def criar_alerta(tipo, mensagem):
|
| 417 |
"""Cria alerta HTML colorido"""
|
| 418 |
cores = {
|
|
|
|
| 432 |
</div>
|
| 433 |
"""
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
def copiar_feedback(texto):
|
| 436 |
if texto:
|
| 437 |
return criar_alerta('success', '✅ Texto copiado!')
|
| 438 |
return criar_alerta('warning', '⚠️ Nada para copiar')
|
| 439 |
|
| 440 |
def limpar_tudo():
|
| 441 |
+
"""Limpa todos os inputs da UI, incluindo filtros de histórico."""
|
| 442 |
+
analytics_data = gerar_relatorio_analytics() # Gerar relatório limpo
|
| 443 |
return (
|
| 444 |
+
# Aba Gerador
|
| 445 |
"", # texto_output
|
| 446 |
None, # imagem_output
|
| 447 |
criar_alerta('info', '🧹 Interface limpa!'), # status_output
|
| 448 |
0, # palavras_output
|
| 449 |
0, # caracteres_output
|
| 450 |
0, # hashtags_output
|
| 451 |
+
list(FORMATO_CONFIGS.keys())[0], # formato_input
|
| 452 |
"Nenhum (Automático)", # estilo_img_input
|
| 453 |
"Balanceada", # qualidade_img_input
|
| 454 |
"Nenhum", # filtro_img_input
|
| 455 |
+
None, # download_output
|
| 456 |
+
True, # usar_cache_checkbox
|
| 457 |
+
analytics_data, # analytics_display
|
| 458 |
+
False, # favorito_checkbox
|
| 459 |
+
# Aba Histórico
|
| 460 |
+
"", # busca_query_input
|
| 461 |
+
"Todos", # filtro_nicho_hist
|
| 462 |
+
"Todos", # filtro_estilo_hist
|
| 463 |
+
"Todos", # filtro_formato_hist
|
| 464 |
+
False # filtro_favoritos_hist
|
| 465 |
)
|
| 466 |
|
| 467 |
+
def recarregar_e_formatar_historico(query, nicho, estilo, formato, favoritos_apenas):
|
| 468 |
+
"""
|
| 469 |
+
Chamado após a geração de um post, para atualizar a tabela de histórico
|
| 470 |
+
mantendo os filtros atuais.
|
| 471 |
+
"""
|
| 472 |
+
# A lista global `post_history` já foi atualizada por `atualizar_historico`.
|
| 473 |
+
# Apenas precisamos refiltrar e formatar.
|
| 474 |
+
return filtrar_historico_local(query, nicho, estilo, formato, favoritos_apenas)
|
| 475 |
|
|
|
|
|
|
|
|
|
|
| 476 |
def interpretar_erro_api(erro_str):
|
| 477 |
"""Interpreta erros comuns da API para o usuário em Português."""
|
| 478 |
erro_str_lower = erro_str.lower()
|
|
|
|
| 497 |
if "authorization" in erro_str_lower or "401" in erro_str_lower:
|
| 498 |
return ("Erro 401: Autenticação falhou. A Chave da API (Secret 'Capoeira') pode estar inválida ou ausente.")
|
| 499 |
|
| 500 |
+
return f"Erro inesperado: {erro_str[:200]}..."
|
|
|
|
| 501 |
|
| 502 |
# ============================================
|
| 503 |
+
# NOVAS FUNÇÕES DE FILTRO E CARGA DE HISTÓRICO
|
| 504 |
# ============================================
|
| 505 |
|
| 506 |
+
def filtrar_historico_local(query, nicho, estilo, formato, favoritos_apenas):
|
| 507 |
+
"""Filtra a lista global `post_history` com base nos inputs da UI."""
|
| 508 |
+
global post_history
|
| 509 |
+
|
| 510 |
+
# Começa com a lista completa
|
| 511 |
+
resultados = post_history
|
| 512 |
+
|
| 513 |
+
# Filtro de busca por texto
|
| 514 |
+
if query:
|
| 515 |
+
query_lower = query.lower()
|
| 516 |
+
resultados = [
|
| 517 |
+
post for post in resultados
|
| 518 |
+
if query_lower in post.get("Tema", "").lower() or query_lower in post.get("Texto", "").lower()
|
| 519 |
+
]
|
| 520 |
+
|
| 521 |
+
# Filtro de Nicho
|
| 522 |
+
if nicho != "Todos":
|
| 523 |
+
resultados = [post for post in resultados if post.get("Nicho") == nicho]
|
| 524 |
+
|
| 525 |
+
# Filtro de Estilo
|
| 526 |
+
if estilo != "Todos":
|
| 527 |
+
resultados = [post for post in resultados if post.get("Estilo") == estilo]
|
| 528 |
+
|
| 529 |
+
# Filtro de Formato
|
| 530 |
+
if formato != "Todos":
|
| 531 |
+
resultados = [post for post in resultados if post.get("Formato") == formato]
|
| 532 |
+
|
| 533 |
+
# Filtro de Favoritos
|
| 534 |
+
if favoritos_apenas:
|
| 535 |
+
resultados = [post for post in resultados if post.get("Favorito") == True]
|
| 536 |
+
|
| 537 |
+
# Formata para o Dataframe
|
| 538 |
+
return _formatar_historico_para_df(resultados)
|
| 539 |
+
|
| 540 |
+
def carregar_post_do_historico(evt: gr.SelectData):
|
| 541 |
+
"""
|
| 542 |
+
Chamado quando o usuário clica em uma linha do Dataframe de histórico.
|
| 543 |
+
Carrega os dados do post selecionado na aba "Gerar Post".
|
| 544 |
+
"""
|
| 545 |
+
global post_history
|
| 546 |
+
try:
|
| 547 |
+
# Pega o post correspondente da lista global
|
| 548 |
+
post_selecionado = post_history[evt.index]
|
| 549 |
+
|
| 550 |
+
# Extrai os dados
|
| 551 |
+
tema = post_selecionado.get("Tema", "")
|
| 552 |
+
nicho = post_selecionado.get("Nicho", NICHOS_DISPONIVEIS[0])
|
| 553 |
+
estilo = post_selecionado.get("Estilo", ESTILOS_DISPONIVEIS[0])
|
| 554 |
+
formato = post_selecionado.get("Formato", list(FORMATO_CONFIGS.keys())[0])
|
| 555 |
+
favorito = post_selecionado.get("Favorito", False)
|
| 556 |
+
texto = post_selecionado.get("Texto", "")
|
| 557 |
+
|
| 558 |
+
# Feedback para o usuário
|
| 559 |
+
status_alerta = criar_alerta('info', '✅ Post carregado do histórico! A imagem deve ser gerada novamente se desejado.')
|
| 560 |
+
|
| 561 |
+
# Retorna os valores para a UI, incluindo a mudança de aba
|
| 562 |
+
return (
|
| 563 |
+
gr.Tabs(selected=0), # Muda para a primeira aba (Gerar Post)
|
| 564 |
+
tema,
|
| 565 |
+
nicho,
|
| 566 |
+
estilo,
|
| 567 |
+
formato,
|
| 568 |
+
favorito,
|
| 569 |
+
texto,
|
| 570 |
+
status_alerta
|
| 571 |
+
)
|
| 572 |
+
except Exception as e:
|
| 573 |
+
print(f"Erro ao carregar do histórico: {e}")
|
| 574 |
+
return (
|
| 575 |
+
gr.Tabs(selected=0), # Muda para a primeira aba
|
| 576 |
+
"", "", "", "", False, "", # Limpa os campos
|
| 577 |
+
criar_alerta('error', f'Erro ao carregar post: {e}')
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
# ============================================
|
| 581 |
+
# FUNÇÕES DE GERAÇÃO
|
| 582 |
+
# ============================================
|
| 583 |
+
|
| 584 |
+
def gerar_texto(tema, nicho, estilo, formato):
|
| 585 |
"""
|
| 586 |
Gera texto usando API do Hugging Face com base no formato escolhido.
|
| 587 |
"""
|
| 588 |
|
|
|
|
|
|
|
|
|
|
| 589 |
if not HUGGINGFACE_API_KEY:
|
| 590 |
return "❌ Erro de Configuração: API Key não está definida."
|
| 591 |
|
|
|
|
| 630 |
else:
|
| 631 |
return f"❌ Erro na resposta da API: Resposta vazia ou inesperada.\n{resultado}"
|
| 632 |
else:
|
|
|
|
| 633 |
return f"❌ {interpretar_erro_api(f'Erro {response.status_code}: {response.text}')}"
|
| 634 |
|
| 635 |
except Exception as e:
|
|
|
|
| 636 |
return f"❌ {interpretar_erro_api(str(e))}"
|
| 637 |
|
| 638 |
def traduzir_texto(texto_pt):
|
| 639 |
"""Traduz texto de Português (PT) para Inglês (EN) usando API do Hugging Face.
|
| 640 |
"""
|
|
|
|
| 641 |
if not HUGGINGFACE_API_KEY:
|
| 642 |
return texto_pt
|
| 643 |
|
| 644 |
url = f"https://api-inference.huggingface.co/models/{MODELO_TRADUCA}"
|
| 645 |
+
payload = {"inputs": texto_pt}
|
|
|
|
|
|
|
|
|
|
| 646 |
|
| 647 |
try:
|
| 648 |
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
|
|
|
| 656 |
return texto_pt # Fallback
|
| 657 |
else:
|
| 658 |
return texto_pt # Fallback
|
|
|
|
| 659 |
except Exception as e:
|
| 660 |
print(f"Falha na tradução (fallback para PT): {e}")
|
| 661 |
return texto_pt # Fallback
|
| 662 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
def otimizar_prompt_imagem(descricao_pt, estilo_escolhido, filtro_escolhido):
|
| 664 |
"""Combina as escolhas do usuário em um prompt otimizado (em Português)."""
|
| 665 |
|
|
|
|
|
|
|
| 666 |
estilo = ESTILOS_DE_IMAGEM.get(estilo_escolhido, ESTILOS_DE_IMAGEM["Nenhum (Automático)"])
|
|
|
|
|
|
|
| 667 |
filtro = FILTROS_IMAGEM.get(filtro_escolhido, FILTROS_IMAGEM["Nenhum"])
|
| 668 |
|
|
|
|
|
|
|
| 669 |
prompt_final = f"{descricao_pt}, {estilo}, {filtro}, best quality, 4k"
|
| 670 |
|
|
|
|
| 671 |
prompt_final = prompt_final.replace(", ,", ",").replace(", ,", ",")
|
| 672 |
return prompt_final
|
| 673 |
|
|
|
|
| 715 |
|
| 716 |
client = InferenceClient(api_key=HUGGINGFACE_API_KEY)
|
| 717 |
|
|
|
|
| 718 |
imagem = client.text_to_image(
|
| 719 |
prompt=prompt_final_en,
|
| 720 |
model=modelo_config['id'],
|
| 721 |
negative_prompt=negative_prompt,
|
| 722 |
+
num_inference_steps=config['steps']
|
| 723 |
)
|
| 724 |
|
|
|
|
| 725 |
print(f"✅ Imagem gerada com {modelo_config['nome']}")
|
| 726 |
mensagem = f"✅ Imagem gerada com {modelo_config['nome']}"
|
| 727 |
|
|
|
|
| 730 |
except Exception as e:
|
| 731 |
print(f"❌ Falha com {modelo_config['nome']}: {str(e)}")
|
| 732 |
|
|
|
|
| 733 |
if i < len(config['modelos']) - 1:
|
| 734 |
print(f"⏭️ Tentando próximo modelo...")
|
| 735 |
continue
|
| 736 |
else:
|
|
|
|
|
|
|
| 737 |
mensagem = f"❌ {interpretar_erro_api(str(e))}"
|
| 738 |
return (None, mensagem)
|
| 739 |
|
|
|
|
| 740 |
return (None, "❌ Erro inesperado ao gerar imagem")
|
| 741 |
|
| 742 |
|
| 743 |
# ============================================
|
| 744 |
+
# FUNÇÃO DO CHATBOT
|
| 745 |
# ============================================
|
| 746 |
def responder_chat(message, chat_history):
|
|
|
|
|
|
|
|
|
|
| 747 |
if not HUGGINGFACE_API_KEY:
|
| 748 |
return "❌ Erro de Configuração: API Key não está definida."
|
| 749 |
|
| 750 |
url = f"{BASE_URL}/chat/completions"
|
| 751 |
|
|
|
|
| 752 |
system_prompt = "Você é um assistente virtual prestativo e amigável, especializado em marketing de mídias sociais e criação de conteúdo, mas pode responder sobre qualquer tópico. Seja direto e útil."
|
| 753 |
|
|
|
|
| 754 |
messages = [{"role": "system", "content": system_prompt}]
|
|
|
|
|
|
|
|
|
|
| 755 |
messages.extend(chat_history)
|
|
|
|
|
|
|
| 756 |
messages.append({"role": "user", "content": message})
|
| 757 |
|
| 758 |
payload = {
|
| 759 |
+
"model": MODELO_TEXTO,
|
| 760 |
"messages": messages,
|
| 761 |
+
"max_tokens": 1500,
|
| 762 |
"temperature": 0.7,
|
| 763 |
+
"stream": False
|
| 764 |
}
|
| 765 |
|
| 766 |
try:
|
|
|
|
| 774 |
else:
|
| 775 |
return f"❌ Erro na resposta da API: Resposta vazia ou inesperada.\n{resultado}"
|
| 776 |
else:
|
|
|
|
| 777 |
return f"❌ {interpretar_erro_api(f'Erro {response.status_code}: {response.text}')}"
|
| 778 |
|
| 779 |
except Exception as e:
|
|
|
|
| 780 |
return f"❌ {interpretar_erro_api(str(e))}"
|
| 781 |
|
| 782 |
# ============================================
|
| 783 |
+
# FUNÇÕES DE DOWNLOAD
|
| 784 |
# ============================================
|
| 785 |
|
| 786 |
def criar_post_completo(texto, imagem_pil, tema):
|
|
|
|
| 793 |
return None
|
| 794 |
|
| 795 |
try:
|
|
|
|
| 796 |
LARGURA_POST = 1080
|
| 797 |
ALTURA_IMAGEM = 1080
|
| 798 |
PADDING = 60
|
| 799 |
COR_FUNDO = (255, 255, 255)
|
| 800 |
COR_TEXTO = (0, 0, 0)
|
| 801 |
|
|
|
|
| 802 |
try:
|
|
|
|
| 803 |
fonte_texto = ImageFont.truetype("DejaVuSans.ttf", size=42)
|
| 804 |
fonte_titulo = ImageFont.truetype("DejaVuSans-Bold.ttf", size=55)
|
| 805 |
except IOError:
|
|
|
|
| 807 |
fonte_texto = ImageFont.load_default()
|
| 808 |
fonte_titulo = ImageFont.load_default()
|
| 809 |
|
|
|
|
| 810 |
imagem_quadrada = imagem_pil.resize((LARGURA_POST, ALTURA_IMAGEM), Image.Resampling.LANCZOS)
|
| 811 |
|
|
|
|
|
|
|
| 812 |
linhas_titulo = textwrap.wrap(tema.upper(), width=40)
|
| 813 |
+
linhas_texto = textwrap.wrap(texto, width=50)
|
| 814 |
|
| 815 |
+
altura_titulo = len(linhas_titulo) * 60
|
|
|
|
|
|
|
| 816 |
altura_texto = len(linhas_texto) * 45
|
| 817 |
+
altura_total_texto = altura_titulo + 20 + altura_texto + (PADDING * 2)
|
|
|
|
| 818 |
|
|
|
|
| 819 |
altura_total = ALTURA_IMAGEM + altura_total_texto
|
| 820 |
post_completo = Image.new('RGB', (LARGURA_POST, int(altura_total)), COR_FUNDO)
|
| 821 |
|
|
|
|
| 822 |
post_completo.paste(imagem_quadrada, (0, 0))
|
| 823 |
|
|
|
|
| 824 |
draw = ImageDraw.Draw(post_completo)
|
| 825 |
pos_y = ALTURA_IMAGEM + PADDING
|
| 826 |
|
|
|
|
| 827 |
for linha in linhas_titulo:
|
|
|
|
| 828 |
largura_linha = draw.textlength(linha, font=fonte_titulo)
|
| 829 |
pos_x_titulo = (LARGURA_POST - largura_linha) / 2
|
| 830 |
draw.text((pos_x_titulo, pos_y), linha, font=fonte_titulo, fill=COR_TEXTO)
|
| 831 |
+
pos_y += 60
|
| 832 |
|
| 833 |
+
pos_y += 20
|
| 834 |
|
|
|
|
| 835 |
for linha in linhas_texto:
|
| 836 |
draw.text((PADDING, pos_y), linha, font=fonte_texto, fill=COR_TEXTO)
|
| 837 |
+
pos_y += 45
|
| 838 |
|
|
|
|
|
|
|
| 839 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
|
| 840 |
post_completo.save(f, 'PNG')
|
| 841 |
print(f"Arquivo temporário salvo em: {f.name}")
|
|
|
|
| 847 |
|
| 848 |
def preparar_download(texto, imagem_pil, tema):
|
| 849 |
"""
|
| 850 |
+
Prepara o arquivo PNG para download.
|
| 851 |
Retorna o caminho do arquivo para o gr.File ou None.
|
| 852 |
"""
|
| 853 |
if not texto or not imagem_pil:
|
| 854 |
+
return None
|
| 855 |
|
| 856 |
caminho_arquivo = criar_post_completo(texto, imagem_pil, tema)
|
| 857 |
|
| 858 |
if caminho_arquivo:
|
|
|
|
| 859 |
return caminho_arquivo
|
| 860 |
|
|
|
|
| 861 |
return None
|
| 862 |
|
|
|
|
| 863 |
# ============================================
|
| 864 |
+
# FUNÇÃO PRINCIPAL
|
| 865 |
# ============================================
|
| 866 |
|
| 867 |
+
def gerar_post_interface(tema, nicho, estilo, formato, usar_cache, favorito_checkbox,
|
| 868 |
+
descricao_imagem, gerar_img,
|
| 869 |
+
estilo_img_input, qualidade_img_input, filtro_img_input,
|
| 870 |
progress=gr.Progress()):
|
| 871 |
+
"""
|
| 872 |
+
Função principal unificada, com Cache, Analytics, Favoritos e Geração Avançada.
|
| 873 |
+
Retorna 7 valores para a UI.
|
| 874 |
+
"""
|
| 875 |
+
|
| 876 |
+
analytics_display = gerar_relatorio_analytics() # Carregar estado atual
|
| 877 |
|
|
|
|
| 878 |
progress(0, desc="🚀 Iniciando...")
|
| 879 |
time.sleep(0.3)
|
| 880 |
|
|
|
|
| 881 |
progress(0.1, desc="✅ Validando...")
|
| 882 |
if not tema or len(tema.strip()) < 3:
|
| 883 |
status_final = criar_alerta('error', '⚠️ Digite um tema válido!')
|
| 884 |
+
return ("", None, status_final, 0, 0, 0, analytics_display)
|
|
|
|
| 885 |
time.sleep(0.3)
|
| 886 |
+
|
| 887 |
+
# 1. Lógica de Cache
|
| 888 |
+
cache_key = criar_cache_key(tema, nicho, estilo, formato)
|
| 889 |
+
if usar_cache:
|
| 890 |
+
progress(0.2, desc="🔍 Buscando no cache...")
|
| 891 |
+
texto, imagem = buscar_no_cache(cache_key)
|
| 892 |
+
|
| 893 |
+
if texto:
|
| 894 |
+
print("✅ Cache hit!")
|
| 895 |
+
progress(1.0, desc="🎉 Encontrado no cache!")
|
| 896 |
+
status_final = criar_alerta('success', '🎉 Post carregado do cache!')
|
| 897 |
+
|
| 898 |
+
atualizar_analytics(nicho, estilo, len(texto.split()), (imagem is not None), cache_hit=True, favorito=favorito_checkbox)
|
| 899 |
+
analytics_display = gerar_relatorio_analytics() # Recarregar
|
| 900 |
+
|
| 901 |
+
palavras = len(texto.split())
|
| 902 |
+
caracteres = len(texto)
|
| 903 |
+
hashtags = texto.count('#')
|
| 904 |
+
|
| 905 |
+
history_entry = {
|
| 906 |
+
"Data/Hora": datetime.now(ZoneInfo("America/Bahia")).strftime("%Y-%m-%d %H:%M:%S"),
|
| 907 |
+
"Tema": tema, "Nicho": nicho, "Estilo": estilo, "Formato": formato,
|
| 908 |
+
"Texto": texto,
|
| 909 |
+
"Status": "Carregado do Cache",
|
| 910 |
+
"Favorito": favorito_checkbox
|
| 911 |
+
}
|
| 912 |
+
atualizar_historico(history_entry)
|
| 913 |
+
|
| 914 |
+
return (texto, imagem, status_final, palavras, caracteres, hashtags, analytics_display)
|
| 915 |
|
| 916 |
+
print("Cache miss ou cache desativado.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
progress(0.3, desc="🤖 Gerando texto (Llama 3.1)...")
|
|
|
|
|
|
|
| 918 |
|
| 919 |
+
# 2. Gerar Texto
|
| 920 |
+
texto = gerar_texto(tema, nicho, estilo, formato)
|
| 921 |
+
|
| 922 |
+
if texto.startswith("❌"):
|
| 923 |
status_final = criar_alerta('error', f'{texto}')
|
| 924 |
+
return (texto, None, status_final, 0, 0, 0, analytics_display)
|
| 925 |
|
| 926 |
progress(0.5, desc="✅ Texto pronto!")
|
| 927 |
time.sleep(0.5)
|
| 928 |
|
| 929 |
+
# 3. Gerar Imagem
|
| 930 |
imagem = None
|
| 931 |
status_imagem = ""
|
| 932 |
if gerar_img:
|
|
|
|
| 933 |
descricao_pt = descricao_imagem or f"{tema} imagem"
|
| 934 |
|
|
|
|
| 935 |
(imagem, status_imagem) = gerar_imagem_robusta(
|
| 936 |
+
descricao_pt,
|
| 937 |
+
estilo_img_input,
|
| 938 |
+
qualidade_img_input,
|
| 939 |
+
filtro_img_input,
|
| 940 |
+
progress
|
| 941 |
)
|
| 942 |
|
| 943 |
if imagem:
|
| 944 |
status_final = criar_alerta('success', f'🎉 Post completo gerado! ({status_imagem})')
|
| 945 |
else:
|
|
|
|
|
|
|
| 946 |
status_final = criar_alerta('warning', f'✅ Texto OK, mas imagem falhou: {status_imagem}')
|
| 947 |
else:
|
| 948 |
progress(0.7, desc="⏭️ Pulando geração de imagem...")
|
|
|
|
| 950 |
|
| 951 |
time.sleep(0.5)
|
| 952 |
|
| 953 |
+
# 4. Estatísticas
|
| 954 |
progress(0.9, desc="📊 Calculando estatísticas...")
|
| 955 |
palavras = len(texto.split())
|
| 956 |
caracteres = len(texto)
|
| 957 |
hashtags = texto.count('#')
|
| 958 |
time.sleep(0.3)
|
| 959 |
|
| 960 |
+
# 5. Salvar no Cache
|
| 961 |
+
if usar_cache:
|
| 962 |
+
progress(0.95, desc="💾 Salvando no cache...")
|
| 963 |
+
imagem_path_cache = salvar_imagem_cache(cache_key, imagem)
|
| 964 |
+
cache_data = {
|
| 965 |
+
"texto": texto,
|
| 966 |
+
"imagem_path": imagem_path_cache
|
| 967 |
+
}
|
| 968 |
+
salvar_no_cache(cache_key, cache_data)
|
| 969 |
+
|
| 970 |
+
# 6. Atualizar Histórico (Firestore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 971 |
history_entry = {
|
| 972 |
"Data/Hora": datetime.now(ZoneInfo("America/Bahia")).strftime("%Y-%m-%d %H:%M:%S"),
|
| 973 |
+
"Tema": tema, "Nicho": nicho, "Estilo": estilo, "Formato": formato,
|
| 974 |
+
"Texto": texto, # Salva o texto completo
|
| 975 |
+
"Status": status_imagem or "Texto Gerado",
|
| 976 |
+
"Favorito": favorito_checkbox
|
|
|
|
|
|
|
| 977 |
}
|
|
|
|
|
|
|
| 978 |
atualizar_historico(history_entry)
|
| 979 |
|
| 980 |
+
# 7. Atualizar Analytics (Firestore)
|
| 981 |
+
atualizar_analytics(nicho, estilo, palavras, (imagem is not None), cache_hit=False, favorito=favorito_checkbox)
|
| 982 |
+
analytics_display = gerar_relatorio_analytics() # Recarregar
|
| 983 |
|
| 984 |
+
progress(1.0, desc="🎉 Pronto!")
|
| 985 |
+
|
| 986 |
+
return (texto, imagem, status_final, palavras, caracteres, hashtags, analytics_display)
|
| 987 |
|
| 988 |
|
| 989 |
# ============================================
|
| 990 |
+
# INTERFACE GRADIO
|
| 991 |
# ============================================
|
| 992 |
|
|
|
|
| 993 |
custom_theme = gr.themes.Soft(
|
| 994 |
+
primary_hue="blue",
|
| 995 |
+
secondary_hue="gray",
|
| 996 |
+
neutral_hue="stone",
|
| 997 |
+
font=["Helvetica", "Georgia", "sans-serif"]
|
| 998 |
)
|
| 999 |
|
| 1000 |
+
# Inicializar Firestore e carregar Analytics ANTES de construir a UI
|
| 1001 |
+
_inicializar_firestore()
|
| 1002 |
|
| 1003 |
+
with gr.Blocks(theme=custom_theme, title="Gerador de Posts e Chatbot (Completo)") as demo:
|
| 1004 |
|
| 1005 |
gr.Markdown("""
|
| 1006 |
+
# 🚀 Gerador de Posts e Assistente de Mídias Sociais (Versão Completa)
|
| 1007 |
+
### Powered by Hugging Face, Gradio, Llama 3.1 e Firebase
|
| 1008 |
""")
|
| 1009 |
|
| 1010 |
+
with gr.Tabs() as main_tabs: # Adicionado 'as main_tabs' para controle
|
| 1011 |
+
with gr.TabItem("✨ Gerar Post", id=0):
|
| 1012 |
with gr.Row():
|
| 1013 |
with gr.Column(scale=1):
|
| 1014 |
gr.Markdown("### ⚙️ 1. Configurações do Texto")
|
|
|
|
| 1032 |
placeholder="Ex: Transforme seu corpo, transforme sua vida"
|
| 1033 |
)
|
| 1034 |
|
|
|
|
| 1035 |
formato_input = gr.Radio(
|
| 1036 |
choices=list(FORMATO_CONFIGS.keys()),
|
| 1037 |
label="Escolha o Formato de Saída",
|
| 1038 |
value=list(FORMATO_CONFIGS.keys())[0],
|
| 1039 |
+
interactive=True
|
| 1040 |
+
)
|
| 1041 |
+
|
| 1042 |
+
usar_cache_checkbox = gr.Checkbox(
|
| 1043 |
+
label="Usar Cache? (Acelera posts repetidos)",
|
| 1044 |
+
value=True
|
| 1045 |
)
|
| 1046 |
|
| 1047 |
gr.Markdown("### 🎨 2. Configurações da Imagem (Opcional)")
|
|
|
|
| 1051 |
value=False
|
| 1052 |
)
|
| 1053 |
|
|
|
|
| 1054 |
descricao_img_input = gr.Textbox(
|
| 1055 |
label="Descrição da imagem (em Português)",
|
| 1056 |
placeholder="Ex: Pessoa correndo ao nascer do sol",
|
| 1057 |
visible=False
|
| 1058 |
)
|
| 1059 |
|
|
|
|
| 1060 |
estilo_img_input = gr.Dropdown(
|
| 1061 |
label="Estilo da Imagem",
|
| 1062 |
choices=list(ESTILOS_DE_IMAGEM.keys()),
|
|
|
|
| 1081 |
interactive=True
|
| 1082 |
)
|
| 1083 |
|
|
|
|
| 1084 |
def toggle_descricao_img(gerar):
|
|
|
|
| 1085 |
return (
|
| 1086 |
gr.Textbox(visible=gerar),
|
| 1087 |
gr.Dropdown(visible=gerar),
|
|
|
|
| 1094 |
inputs=[gerar_img_checkbox],
|
| 1095 |
outputs=[descricao_img_input, estilo_img_input, qualidade_img_input, filtro_img_input]
|
| 1096 |
)
|
| 1097 |
+
|
| 1098 |
+
favorito_checkbox = gr.Checkbox(label="⭐ Favoritar este post?", value=False)
|
| 1099 |
|
| 1100 |
gerar_btn = gr.Button("✨ Gerar Post", variant="primary")
|
| 1101 |
|
| 1102 |
with gr.Column(scale=1):
|
| 1103 |
gr.Markdown("### 📋 3. Resultado")
|
| 1104 |
|
|
|
|
| 1105 |
status_output = gr.HTML(
|
| 1106 |
label="Status",
|
| 1107 |
value=criar_alerta('info', 'Pronto para gerar!')
|
|
|
|
| 1110 |
texto_output = gr.Textbox(
|
| 1111 |
label="Texto Gerado",
|
| 1112 |
lines=10,
|
| 1113 |
+
interactive=True, # Alterado para True para carregar do histórico
|
| 1114 |
+
show_copy_button=True
|
| 1115 |
)
|
| 1116 |
|
|
|
|
| 1117 |
with gr.Row():
|
| 1118 |
copiar_btn = gr.Button("📋 Copiar Texto", variant="secondary")
|
| 1119 |
limpar_btn = gr.Button("🧹 Limpar Tudo", variant="stop")
|
| 1120 |
+
|
| 1121 |
imagem_output = gr.Image(
|
| 1122 |
label="Imagem Gerada",
|
| 1123 |
type="pil"
|
| 1124 |
)
|
| 1125 |
|
|
|
|
| 1126 |
gr.Markdown("### 📥 4. Download")
|
| 1127 |
download_btn = gr.Button(
|
| 1128 |
"Baixar Post Completo (Imagem + Texto)",
|
|
|
|
| 1130 |
)
|
| 1131 |
download_output = gr.File(
|
| 1132 |
label="Clique para baixar",
|
| 1133 |
+
visible=True
|
| 1134 |
)
|
|
|
|
| 1135 |
|
|
|
|
| 1136 |
gr.Markdown("### 📊 Estatísticas do Texto")
|
| 1137 |
with gr.Row():
|
| 1138 |
palavras_output = gr.Number(label="Palavras", value=0, interactive=False)
|
| 1139 |
caracteres_output = gr.Number(label="Caracteres", value=0, interactive=False)
|
| 1140 |
hashtags_output = gr.Number(label="Hashtags", value=0, interactive=False)
|
| 1141 |
|
|
|
|
| 1142 |
gr.Markdown("### 💡 Experimente estes exemplos:")
|
| 1143 |
+
|
|
|
|
| 1144 |
gr.Examples(
|
| 1145 |
examples=[
|
| 1146 |
[NICHOS_DISPONIVEIS[2], ESTILOS_DISPONIVEIS[0], "Frases marcantes de pessoas importantes", "Instagram (Post)"],
|
|
|
|
| 1148 |
[NICHOS_DISPONIVEIS[5], ESTILOS_DISPONIVEIS[3], "O futuro da IA em 2025", "LinkedIn (Artigo)"],
|
| 1149 |
[NICHOS_DISPONIVEIS[4], ESTILOS_DISPONIVEIS[1], "Melhores destinos para lua de mel na Europa", "Twitter/X (Curto)"],
|
| 1150 |
],
|
| 1151 |
+
inputs=[nicho_input, estilo_input, tema_input, formato_input],
|
| 1152 |
outputs=[texto_output, imagem_output, status_output]
|
| 1153 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1154 |
|
| 1155 |
+
with gr.TabItem("💬 Chatbot Assistente", id=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1156 |
gr.Markdown("### 🤖 Assistente Virtual")
|
| 1157 |
gr.Markdown("Faça perguntas sobre mídias sociais, IA, peça ideias rápidas ou qualquer outro tópico.")
|
| 1158 |
|
|
|
|
| 1159 |
chatbot_para_interface = gr.Chatbot(
|
| 1160 |
+
height=500,
|
| 1161 |
+
type="messages"
|
| 1162 |
)
|
| 1163 |
|
|
|
|
| 1164 |
gr.ChatInterface(
|
| 1165 |
fn=responder_chat,
|
| 1166 |
title="Assistente Virtual",
|
| 1167 |
description="Converse com o Llama 3.1 para obter ajuda e insights.",
|
| 1168 |
examples=[
|
| 1169 |
+
"O que é um 'gancho' para Instagram?",
|
| 1170 |
+
"Me dê 3 ideias de post para um nicho de 'Fitness'",
|
|
|
|
| 1171 |
"Qual a diferença entre um post para Instagram e um para LinkedIn?"
|
| 1172 |
],
|
| 1173 |
+
chatbot=chatbot_para_interface,
|
| 1174 |
textbox=gr.Textbox(placeholder="Digite sua mensagem aqui...", scale=7),
|
| 1175 |
+
submit_btn="Enviar"
|
| 1176 |
)
|
| 1177 |
|
| 1178 |
+
with gr.TabItem("📚 Histórico de Posts", id=2):
|
| 1179 |
+
gr.Markdown("### 🔍 Buscar e Filtrar Histórico")
|
| 1180 |
+
gr.Markdown("Carregue posts antigos clicando em uma linha da tabela.")
|
| 1181 |
+
|
| 1182 |
+
with gr.Row():
|
| 1183 |
+
busca_query_input = gr.Textbox(
|
| 1184 |
+
label="Buscar por Tema/Texto",
|
| 1185 |
+
placeholder="Digite para buscar...",
|
| 1186 |
+
scale=3,
|
| 1187 |
+
interactive=True
|
| 1188 |
+
)
|
| 1189 |
+
filtro_nicho_hist = gr.Dropdown(
|
| 1190 |
+
label="Nicho",
|
| 1191 |
+
choices=["Todos"] + NICHOS_DISPONIVEIS,
|
| 1192 |
+
value="Todos",
|
| 1193 |
+
interactive=True
|
| 1194 |
+
)
|
| 1195 |
+
with gr.Row():
|
| 1196 |
+
filtro_estilo_hist = gr.Dropdown(
|
| 1197 |
+
label="Estilo",
|
| 1198 |
+
choices=["Todos"] + ESTILOS_DISPONIVEIS,
|
| 1199 |
+
value="Todos",
|
| 1200 |
+
interactive=True
|
| 1201 |
+
)
|
| 1202 |
+
filtro_formato_hist = gr.Dropdown(
|
| 1203 |
+
label="Formato",
|
| 1204 |
+
choices=["Todos"] + list(FORMATO_CONFIGS.keys()),
|
| 1205 |
+
value="Todos",
|
| 1206 |
+
interactive=True
|
| 1207 |
+
)
|
| 1208 |
+
filtro_favoritos_hist = gr.Checkbox(
|
| 1209 |
+
label="⭐ Apenas Favoritos",
|
| 1210 |
+
value=False,
|
| 1211 |
+
interactive=True
|
| 1212 |
+
)
|
| 1213 |
+
|
| 1214 |
+
buscar_hist_btn = gr.Button("Buscar", variant="primary")
|
| 1215 |
|
|
|
|
| 1216 |
historico_display = gr.Dataframe(
|
| 1217 |
+
headers=["⭐", "Data/Hora", "Tema", "Nicho", "Estilo", "Formato", "Texto (Preview)", "Status"],
|
| 1218 |
+
interactive=True, # Habilitado para seleção
|
| 1219 |
+
value=_formatar_historico_para_df(carregar_historico_inicial()),
|
| 1220 |
)
|
| 1221 |
+
|
| 1222 |
+
with gr.TabItem("📊 Analytics", id=3):
|
| 1223 |
+
gr.Markdown("### Análise de Uso da Ferramenta")
|
| 1224 |
+
gr.Markdown("Estes dados são salvos no Firestore e agregam o uso de todos os usuários.")
|
| 1225 |
|
| 1226 |
+
analytics_display = gr.Markdown(
|
| 1227 |
+
value=gerar_relatorio_analytics()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
|
| 1230 |
+
with gr.Row():
|
| 1231 |
+
gerar_relatorio_btn = gr.Button("Atualizar Relatório", variant="secondary")
|
| 1232 |
+
resetar_analytics_btn = gr.Button("Resetar Analytics (CUIDADO)", variant="stop")
|
| 1233 |
|
| 1234 |
+
with gr.TabItem("⚙️ Configurações", id=4):
|
|
|
|
| 1235 |
gr.Markdown("### Configurações do Gerador")
|
| 1236 |
gr.Markdown("**Modelo de Texto (LLM):** Llama 3.1 8B (Usado para Posts e Chatbot)")
|
| 1237 |
gr.Markdown("**Modelos de Imagem:** FLUX.1-schnell, FLUX.1-dev, SDXL 1.0")
|
|
|
|
| 1239 |
gr.Markdown("**API Provider:** Hugging Face Inference")
|
| 1240 |
gr.Markdown("**Database:** Google Firestore (via Firebase Admin)")
|
| 1241 |
gr.Markdown("---")
|
| 1242 |
+
gr.Markdown("#### Funcionalidades (Versão Completa):")
|
| 1243 |
gr.Markdown("- **Gerador de Posts:** Cria posts completos com texto e imagem.")
|
| 1244 |
gr.Markdown("- **Seleção de Formato:** Permite escolher o formato do texto (Instagram, Twitter, LinkedIn).")
|
| 1245 |
gr.Markdown("- **Controles Avançados:** Permite seleção de Estilo, Qualidade e Filtros para a imagem.")
|
| 1246 |
gr.Markdown("- **Download de Post:** Combina texto e imagem em um único arquivo PNG para download.")
|
| 1247 |
gr.Markdown("- **Chatbot Assistente:** Converse com a IA para ideias e perguntas rápidas.")
|
| 1248 |
gr.Markdown("- **Histórico Persistente:** Salva os *posts gerados* no Firestore.")
|
| 1249 |
+
gr.Markdown("- **Busca no Histórico:** Permite buscar e filtrar posts antigos.")
|
| 1250 |
+
gr.Markdown("- **Favoritos:** Permite marcar posts como favoritos.")
|
| 1251 |
+
gr.Markdown("- **Sistema de Cache:** Salva posts localmente para acelerar requisições futuras.")
|
| 1252 |
+
gr.Markdown("- **Sistema de Analytics:** Rastreia o uso (total, por nicho, etc.) no Firestore.")
|
| 1253 |
|
| 1254 |
+
with gr.TabItem("ℹ️ Sobre", id=5):
|
|
|
|
|
|
|
| 1255 |
gr.Markdown("""
|
| 1256 |
### Sobre Este Projeto
|
| 1257 |
|
|
|
|
| 1263 |
- **Llama 3.1 8B (geração de texto e chatbot)**
|
| 1264 |
- **FLUX.1 & SDXL (geração de imagens)**
|
| 1265 |
- Opus-MT (tradução)
|
| 1266 |
+
- **Firebase Firestore (Banco de Dados & Analytics)**
|
| 1267 |
- **PIL (Python Imaging Library) (para composição de posts)**
|
| 1268 |
+
- **Cache local (para performance)**
|
| 1269 |
|
| 1270 |
**Como funciona:**
|
| 1271 |
1. **Gerar Post:** Você define o tema, nicho, estilo e **formato** do *texto*.
|
|
|
|
| 1273 |
3. O sistema otimiza o prompt, traduz para inglês e usa o sistema de *fallback* de modelos (baseado na *Qualidade*) para gerar a imagem.
|
| 1274 |
4. **Download:** Após a geração, você pode clicar em "Baixar Post Completo" para salvar um PNG com a imagem e o texto formatado.
|
| 1275 |
5. **Chatbot:** Você pode conversar diretamente com a IA na aba 'Chatbot Assistente' para tirar dúvidas.
|
| 1276 |
+
6. **Histórico & Analytics:** Os posts gerados são salvos no Firestore e as métricas de uso são atualizadas.
|
| 1277 |
|
| 1278 |
**Desenvolvido por:** Wilder Paz
|
| 1279 |
""")
|
|
|
|
| 1281 |
# Footer
|
| 1282 |
gr.Markdown("""
|
| 1283 |
---
|
| 1284 |
+
**Curso de Python com IA** | 🤖 Powered by Llama 3.1 & FLUX | ⚡ Hugging Face Spaces + Gradio + Firestore + Cache + Analytics
|
| 1285 |
""")
|
| 1286 |
|
| 1287 |
+
# ============================================
|
| 1288 |
+
# CONECTAR EVENTOS
|
| 1289 |
+
# ============================================
|
| 1290 |
+
|
| 1291 |
+
# Lista de inputs para o botão Gerar
|
| 1292 |
+
gerar_inputs = [
|
| 1293 |
+
tema_input, nicho_input, estilo_input,
|
| 1294 |
+
formato_input, usar_cache_checkbox, favorito_checkbox,
|
| 1295 |
+
descricao_img_input, gerar_img_checkbox,
|
| 1296 |
+
estilo_img_input, qualidade_img_input, filtro_img_input
|
| 1297 |
+
]
|
| 1298 |
+
|
| 1299 |
+
# Lista de outputs do botão Gerar
|
| 1300 |
+
gerar_outputs = [
|
| 1301 |
+
texto_output, imagem_output, status_output,
|
| 1302 |
+
palavras_output, caracteres_output, hashtags_output,
|
| 1303 |
+
analytics_display
|
| 1304 |
+
]
|
| 1305 |
+
|
| 1306 |
+
# Botão principal
|
| 1307 |
+
click_event = gerar_btn.click(
|
| 1308 |
+
fn=gerar_post_interface,
|
| 1309 |
+
inputs=gerar_inputs,
|
| 1310 |
+
outputs=gerar_outputs,
|
| 1311 |
+
show_progress="full"
|
| 1312 |
+
)
|
| 1313 |
+
|
| 1314 |
+
# Botão copiar
|
| 1315 |
+
copiar_btn.click(
|
| 1316 |
+
fn=copiar_feedback,
|
| 1317 |
+
inputs=[texto_output],
|
| 1318 |
+
outputs=[status_output]
|
| 1319 |
+
)
|
| 1320 |
+
|
| 1321 |
+
# Lista de outputs para o botão Limpar
|
| 1322 |
+
limpar_outputs = [
|
| 1323 |
+
# Aba Gerador
|
| 1324 |
+
texto_output, imagem_output, status_output,
|
| 1325 |
+
palavras_output, caracteres_output, hashtags_output,
|
| 1326 |
+
formato_input,
|
| 1327 |
+
estilo_img_input, qualidade_img_input, filtro_img_input,
|
| 1328 |
+
download_output,
|
| 1329 |
+
usar_cache_checkbox,
|
| 1330 |
+
analytics_display,
|
| 1331 |
+
favorito_checkbox,
|
| 1332 |
+
# Aba Histórico
|
| 1333 |
+
busca_query_input,
|
| 1334 |
+
filtro_nicho_hist,
|
| 1335 |
+
filtro_estilo_hist,
|
| 1336 |
+
filtro_formato_hist,
|
| 1337 |
+
filtro_favoritos_hist
|
| 1338 |
+
]
|
| 1339 |
+
|
| 1340 |
+
# Botão limpar
|
| 1341 |
+
limpar_btn.click(
|
| 1342 |
+
fn=limpar_tudo,
|
| 1343 |
+
inputs=[],
|
| 1344 |
+
outputs=limpar_outputs
|
| 1345 |
+
)
|
| 1346 |
+
|
| 1347 |
+
# Botão de Download
|
| 1348 |
+
download_btn.click(
|
| 1349 |
+
fn=preparar_download,
|
| 1350 |
+
inputs=[texto_output, imagem_output, tema_input],
|
| 1351 |
+
outputs=[download_output]
|
| 1352 |
+
)
|
| 1353 |
+
|
| 1354 |
+
# --- Eventos da Aba Histórico ---
|
| 1355 |
+
|
| 1356 |
+
# Lista de inputs para os filtros de histórico
|
| 1357 |
+
hist_filter_inputs = [
|
| 1358 |
+
busca_query_input,
|
| 1359 |
+
filtro_nicho_hist,
|
| 1360 |
+
filtro_estilo_hist,
|
| 1361 |
+
filtro_formato_hist,
|
| 1362 |
+
filtro_favoritos_hist
|
| 1363 |
+
]
|
| 1364 |
+
|
| 1365 |
+
# Botão de buscar no histórico
|
| 1366 |
+
buscar_hist_btn.click(
|
| 1367 |
+
fn=filtrar_historico_local,
|
| 1368 |
+
inputs=hist_filter_inputs,
|
| 1369 |
+
outputs=[historico_display]
|
| 1370 |
+
)
|
| 1371 |
+
|
| 1372 |
+
# Atualizar o histórico (mantendo filtros) após gerar um novo post
|
| 1373 |
+
click_event.then(
|
| 1374 |
+
fn=recarregar_e_formatar_historico,
|
| 1375 |
+
inputs=hist_filter_inputs,
|
| 1376 |
+
outputs=[historico_display]
|
| 1377 |
+
)
|
| 1378 |
+
|
| 1379 |
+
# Clicar em uma linha do histórico para carregar
|
| 1380 |
+
historico_display.select(
|
| 1381 |
+
fn=carregar_post_do_historico,
|
| 1382 |
+
inputs=[], # Usa o dado do evento
|
| 1383 |
+
outputs=[
|
| 1384 |
+
main_tabs, # Para mudar de aba
|
| 1385 |
+
tema_input,
|
| 1386 |
+
nicho_input,
|
| 1387 |
+
estilo_input,
|
| 1388 |
+
formato_input,
|
| 1389 |
+
favorito_checkbox,
|
| 1390 |
+
texto_output,
|
| 1391 |
+
status_output
|
| 1392 |
+
],
|
| 1393 |
+
show_progress="minimal"
|
| 1394 |
+
)
|
| 1395 |
+
|
| 1396 |
+
# --- Eventos da Aba Analytics ---
|
| 1397 |
+
|
| 1398 |
+
gerar_relatorio_btn.click(
|
| 1399 |
+
fn=gerar_relatorio_analytics,
|
| 1400 |
+
inputs=None,
|
| 1401 |
+
outputs=[analytics_display]
|
| 1402 |
+
)
|
| 1403 |
+
|
| 1404 |
+
resetar_analytics_btn.click(
|
| 1405 |
+
fn=resetar_analytics,
|
| 1406 |
+
inputs=None,
|
| 1407 |
+
outputs=[analytics_display]
|
| 1408 |
+
)
|
| 1409 |
+
|
| 1410 |
# Lançar aplicação
|
| 1411 |
if __name__ == "__main__":
|
| 1412 |
demo.launch()
|