wpbcpaz commited on
Commit
647bac7
·
verified ·
1 Parent(s): 4d30e10

Update app.py

Browse files

Atualização com as novas implementações da Aula 12

Files changed (1) hide show
  1. app.py +629 -342
app.py CHANGED
@@ -1,20 +1,28 @@
1
  # CHATBOT E GERADOR DE POSTS PARA REDES SOCIAIS
2
- # Aula 9: Versão Web com Hugging Face Spaces + Gradio
3
- # Versão com persistência de dados via Firebase Admin
4
- # ATUALIZADO: Adicionada funcionalidade de Chatbot e Geração Avançada de Imagens
 
 
 
 
 
 
5
 
6
  import gradio as gr
7
  import requests
8
  import os
9
- import json # Importar para carregar o JSON da Service Account
10
- import time # Adicionado para a nova função de progresso
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
- import tempfile # Para criar arquivos de download
17
- import textwrap # Para quebrar o texto no post final
 
 
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 (SEM símbolos < >)
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
- # Variável global para armazenar o histórico de sessão (agora é um cache)
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 ATIVADO)
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
- # Query para buscar posts, ordenar pelo campo 'Data/Hora' em ordem descendente, limitando a 50
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
- # Inicializar Firestore ao carregar o app
231
- _inicializar_firestore()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  # ============================================
234
- # HELPER FUNCTION PARA GRADIO
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
- # A ordem DEVE bater com os headers do gr.Dataframe
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
- entry.get("Texto (Preview)", ""),
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
- # Retorna 11 valores para limpar todos os campos
 
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 (Solicitação 1)
299
  "Nenhum (Automático)", # estilo_img_input
300
  "Balanceada", # qualidade_img_input
301
  "Nenhum", # filtro_img_input
302
- None # download_output
 
 
 
 
 
 
 
 
 
303
  )
304
 
305
- # NOVA FUNÇÃO AUXILIAR PARA ATUALIZAR A UI DO HISTÓRICO
306
- def recarregar_e_formatar_historico():
307
- """Recarrega o histórico do cache global (que foi atualizado) e formata para o Dataframe."""
308
- # O cache 'post_history' já foi atualizado por 'gerar_post_interface'
309
- # através da chamada a 'atualizar_historico'
310
- return _formatar_historico_para_df(post_history)
 
 
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
- # Fallback para erros genéricos
340
- return f"Erro inesperado: {erro_str[:200]}..." # Trunca o erro
341
 
342
  # ============================================
343
- # FUNÇÕES DE GERAÇÃO (POSTS)
344
  # ============================================
345
 
346
- def gerar_texto(tema, nicho, estilo, formato): # ATUALIZADO: 'formato' re-adicionado
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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'] # Parâmetro de qualidade
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
- # NOVA FUNÇÃO DO CHATBOT (CORRIGIDA)
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, # Reutiliza o mesmo modelo
559
  "messages": messages,
560
- "max_tokens": 1500, # Mais espaço para uma conversa fluida
561
  "temperature": 0.7,
562
- "stream": False # ChatInterface do Gradio lida melhor com respostas completas
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
- # NOVAS FUNÇÕES DE DOWNLOAD
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) # Quebra de linha mais larga para o corpo
621
 
622
- # 3. Calcular altura necessária para o texto
623
- # (altura da linha * num linhas)
624
- altura_titulo = len(linhas_titulo) * 60
625
  altura_texto = len(linhas_texto) * 45
626
- # Altura total = Título + Espaço + Texto + Padding
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 # Avança Y para a próxima linha
647
 
648
- pos_y += 20 # Espaço extra entre título e corpo
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 # Avança Y
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 ZIP para download.
669
  Retorna o caminho do arquivo para o gr.File ou None.
670
  """
671
  if not texto or not imagem_pil:
672
- return None # Retorna None se não houver nada para baixar
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 COM PROGRESSO (ATUALIZADA)
686
  # ============================================
687
 
688
- def gerar_post_interface(tema, nicho, estilo, formato, # ATUALIZADO: 'formato' adicionado
689
- descricao_imagem, gerar_img,
690
- estilo_img_input, qualidade_img_input, filtro_img_input,
691
  progress=gr.Progress()):
692
- """Gera post completo com feedback e lógica de imagem avançada"""
 
 
 
 
 
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
- # Retorna 6 valores
703
- return ("", None, status_final, 0, 0, 0)
704
  time.sleep(0.3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
 
706
- # Preparando
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
- if texto.startswith("❌"): # Erro já vem formatado
 
 
 
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
- # Gerando imagem
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 # Passa o objeto de progresso
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
- # Concluído
758
- progress(1.0, desc="🎉 Pronto!")
759
-
760
- # ==================================================
761
- # INÍCIO DA CORREÇÃO: Re-adicionando salvamento no histórico
762
- # ==================================================
763
-
764
- # ATUALIZADO: Usar o 'formato' vindo do input
765
- formato_usado = formato
766
-
767
- # Usar a variável 'status' que já foi definida
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
- "Nicho": nicho,
777
- "Estilo": estilo,
778
- "Formato": formato_usado, # Atualizado
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
- # FIM DA CORREÇÃO
788
- # ==================================================
789
 
790
- # A função agora retorna 6 valores
791
- return (texto, imagem, status_final, palavras, caracteres, hashtags)
 
792
 
793
 
794
  # ============================================
795
- # INTERFACE GRADIO (Com Chatbot)
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", # Azul como base
801
- secondary_hue="gray", # Contraste frio e sofisticado
802
- neutral_hue="stone", # Tom neutro suave
803
- font=["Helvetica", "Georgia", "sans-serif"] # Fontes clássicas e legíveis
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 e Llama 3.1
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
- visible=True
 
 
 
 
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=False,
918
- show_copy_button=True # Garantir que o botão nativo (ícone) apareça
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 # Visível para que o link apareça
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], # 'formato_input' adicionado
962
  outputs=[texto_output, imagem_output, status_output]
963
  )
964
-
965
- # ============================================
966
- # CONECTAR EVENTOS (Bloco Novo)
967
- # ============================================
968
 
969
- # Botão principal
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" # Alterado conforme solicitação para evitar obsolescência
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, # Passando o componente chatbot customizado
1038
  textbox=gr.Textbox(placeholder="Digite sua mensagem aqui...", scale=7),
1039
- submit_btn="Enviar"
1040
  )
1041
 
1042
- # ABA 2: Histórico (Exibe o Dataframe)
1043
- with gr.TabItem("📚 Histórico de Posts"):
1044
- gr.Markdown("### Posts Gerados Recentemente")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=False,
1050
- value=_formatar_historico_para_df(carregar_historico_inicial()), # Carrega histórico (DB ou vazio) E FORMATA
1051
  )
 
 
 
 
1052
 
1053
- # ============================================
1054
- # CORREÇÃO DA ATUALIZAÇÃO DO HISTÓRICO
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
- Esta aba agora exibe o histórico persistente (dos **posts gerados**) salvo no Google Firestore.
1069
- O histórico é compartilhado entre todas as sessões e usuários.
1070
- """)
1071
 
1072
- # ABA 3: Configurações
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()