Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
OPENAI_API_KEY=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
|
||||||
|
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA
|
||||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
OPENAI_API_KEY=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
|
||||||
|
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA
|
||||||
120
Campanha-LP-Civil-Otimizada.md
Normal file
120
Campanha-LP-Civil-Otimizada.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
REGRAS OTIMIZADAS PARA GOOGLE ADS:
|
||||||
|
|
||||||
|
1. PALAVRAS-CHAVE (Fundo de Funil): Gere 20 termos com alta intenção de contratação. Use [Exata] e "Frase". Limite os termos a no máximo 25 caracteres para viabilizar o uso nos títulos.
|
||||||
|
2. NEGATIVAS: Gere 20 termos que filtrem estudantes, curiosos e buscas gratuitas (ex: pdf, curso, modelo, jurisprudência, tcc).
|
||||||
|
3. TÍTULOS (Headlines): Gere 15 títulos (máx. 30 caracteres). Regra de Ouro: A palavra-chave da Regra 1 deve aparecer de forma INTEGRAL e IDENTICA no título. Se a keyword for longa, o título será apenas ela.
|
||||||
|
4. DESCRIÇÕES: Gere 4 descrições (máx. 90 caracteres). Devem terminar obrigatoriamente com um ponto final ou exclamação. Inclua uma Proposta Única de Valor (UVP).
|
||||||
|
5. EXTENSÕES (Sitelinks & Callouts):
|
||||||
|
- 4 Sitelinks (Título 25ch / Desc 35ch).
|
||||||
|
- 6 Callouts (Frases de destaque, máx 25ch) focadas em autoridade e agilidade.
|
||||||
|
6. POLÍTICAS EDITORIAIS:
|
||||||
|
- Proibido: "Grátis" e sinônimos.
|
||||||
|
- Proibido: CAIXA ALTA em palavras inteiras (exceto siglas como SP, OAB).
|
||||||
|
- Proibido: Uso excessivo de pontuação (ex: !!!).
|
||||||
|
|
||||||
|
RETORNO: Apenas o JSON estruturado.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
{
|
||||||
|
"campanha_google_ads": {
|
||||||
|
"palavras_chave_fundo_de_funil": [
|
||||||
|
"[advogado divórcio online]",
|
||||||
|
"\"contratar advogado família\"",
|
||||||
|
"[advogado guarda filhos]",
|
||||||
|
"\"advogado pensão alimentícia\"",
|
||||||
|
"[advogado inventário SP]",
|
||||||
|
"\"advogado união estável\"",
|
||||||
|
"[dissolução união estável]",
|
||||||
|
"\"escritório direito civil\"",
|
||||||
|
"[advogado para inventário]",
|
||||||
|
"\"especialista em divórcio\"",
|
||||||
|
"[ação de pensão alimentícia]",
|
||||||
|
"\"advogado guarda de menores\"",
|
||||||
|
"[advogado partilha de bens]",
|
||||||
|
"\"consultoria jurídica família\"",
|
||||||
|
"[divórcio consensual online]",
|
||||||
|
"\"advogado civilista SP\"",
|
||||||
|
"[inventário extrajudicial]",
|
||||||
|
"\"advogado direito de família\"",
|
||||||
|
"[pedir pensão alimentícia]",
|
||||||
|
"\"advogado separação judicial\""
|
||||||
|
],
|
||||||
|
"palavras_chave_negativas": [
|
||||||
|
"grátis (evitar buscas sem orçamento)",
|
||||||
|
"gratuito (evitar defensoria pública)",
|
||||||
|
"estágio (evitar candidatos a vaga)",
|
||||||
|
"vagas (evitar candidatos a vaga)",
|
||||||
|
"curso (evitar estudantes)",
|
||||||
|
"faculdade (evitar estudantes)",
|
||||||
|
"tcc (evitar estudantes)",
|
||||||
|
"modelo de petição (evitar curiosos)",
|
||||||
|
"jurisprudência (evitar estudantes)",
|
||||||
|
"pdf (evitar buscas informativas)",
|
||||||
|
"concurso (evitar concurseiros)",
|
||||||
|
"trabalhista (fora do escopo civil)",
|
||||||
|
"criminal (fora do escopo civil)",
|
||||||
|
"previdenciário (fora do escopo civil)",
|
||||||
|
"defensoria pública (baixa intenção)",
|
||||||
|
"fórum (busca informativa)",
|
||||||
|
"como fazer (busca informativa)",
|
||||||
|
"lei (busca informativa)",
|
||||||
|
"livro (evitar estudantes)",
|
||||||
|
"apostila (evitar estudantes)"
|
||||||
|
],
|
||||||
|
"titulos_headlines": [
|
||||||
|
"Advogado divórcio online",
|
||||||
|
"Contratar advogado família",
|
||||||
|
"Advogado guarda filhos",
|
||||||
|
"Advogado pensão alimentícia",
|
||||||
|
"Advogado inventário SP",
|
||||||
|
"Advogado união estável",
|
||||||
|
"Dissolução união estável",
|
||||||
|
"Escritório direito civil",
|
||||||
|
"Advogado para inventário",
|
||||||
|
"Especialista em divórcio",
|
||||||
|
"Ação de pensão alimentícia",
|
||||||
|
"Advogado guarda de menores",
|
||||||
|
"Advogado partilha de bens",
|
||||||
|
"Consultoria jurídica família",
|
||||||
|
"Divórcio consensual online"
|
||||||
|
],
|
||||||
|
"descricoes": [
|
||||||
|
"Advocacia especializada em Direito Civil e Família. Atendimento 100% online e sigiloso.",
|
||||||
|
"Resolva seu divórcio, guarda ou inventário de forma rápida e segura. Fale com um expert.",
|
||||||
|
"Proteja seus direitos e sua família com especialistas. Soluções ágeis e atendimento humanizado.",
|
||||||
|
"Mais de 10 anos de experiência em Direito de Família. Avalie seu caso com total sigilo."
|
||||||
|
],
|
||||||
|
"sitelinks": [
|
||||||
|
{
|
||||||
|
"titulo": "Divórcio e Separação",
|
||||||
|
"desc_linha_1": "Encerre o caso de forma rápida.",
|
||||||
|
"desc_linha_2": "Consensual ou litigioso online."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Guarda e Pensão",
|
||||||
|
"desc_linha_1": "Priorizando o bem-estar dos filhos.",
|
||||||
|
"desc_linha_2": "Definição e revisão de valores."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Inventário e Herança",
|
||||||
|
"desc_linha_1": "Solução ágil na partilha de bens.",
|
||||||
|
"desc_linha_2": "Via judicial ou extrajudicial."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Fale com Especialista",
|
||||||
|
"desc_linha_1": "Atendimento rápido via WhatsApp.",
|
||||||
|
"desc_linha_2": "Tire suas dúvidas agora mesmo."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"callouts": [
|
||||||
|
"Atendimento 100% Online",
|
||||||
|
"Sigilo Total Garantido",
|
||||||
|
"10 Anos de Experiência",
|
||||||
|
"Resposta Rápida",
|
||||||
|
"Especialista em Família",
|
||||||
|
"Solução Sem Burocracia"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||||
5
Links Uteis.txt
Normal file
5
Links Uteis.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Como criar uma Campanha - adriano gianini
|
||||||
|
https://www.youtube.com/watch?v=4HMDAhl15cA
|
||||||
|
|
||||||
|
#Amanda Agostinho
|
||||||
|
https://www.youtube.com/watch?v=XU1MWVk6HvQ
|
||||||
55
Prompt.md
Normal file
55
Prompt.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
Objetivo: Landing Page (LP) já está bem otimizada para conversão, ela é a melhor fonte de "verdade" para o Google Ads. Usar Python para automatizar isso não só economiza tempo, mas garante que o anúncio seja um reflexo fiel do que o usuário vai encontrar na página (o que aumenta o seu **Índice de Qualidade**).
|
||||||
|
|
||||||
|
Aqui está como eu estruturaria essa aplicação Python para você:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Arquitetura da Aplicação
|
||||||
|
|
||||||
|
Para construir isso "do zero", você pode dividir o script em quatro módulos principais:
|
||||||
|
|
||||||
|
### 1. Scraping (Extração de Conteúdo)
|
||||||
|
|
||||||
|
Use as bibliotecas `BeautifulSoup` ou `Selenium`.
|
||||||
|
|
||||||
|
* **O que pegar:** Títulos (`h1`, `h2`), textos de benefícios, CTAs (chamadas para ação) e a meta-description.
|
||||||
|
* **Dica:** Foque em extrair a **proposta de valor** central.
|
||||||
|
|
||||||
|
### 2. Processamento com IA (O "Cérebro")
|
||||||
|
|
||||||
|
Em vez de tentar criar lógica de palavras-chave na mão, conecte o Python à **API do OpenAI**
|
||||||
|
|
||||||
|
* **Prompt de Ouro:** "Com base no texto desta LP [solicitar URL], gere: 10 variações de palavras-chave (fundo de funil), 3 títulos de 30 caracteres, 2 descrições de 90 caracteres e 4 sitelinks."
|
||||||
|
|
||||||
|
### 3. Estruturação dos Ativos
|
||||||
|
|
||||||
|
Organize os dados extraídos no formato que o Google Ads aceita (planilhas de upload em massa ou via API).
|
||||||
|
|
||||||
|
* **Palavras-chave:** Classifique por tipos de correspondência (Exata e Frase).
|
||||||
|
* **Anúncios Responsivos (RSA):** Garanta que você tenha variações suficientes para o Google testar.
|
||||||
|
|
||||||
|
### 4. Exportação
|
||||||
|
|
||||||
|
Gere um arquivo `.csv` formatado para o **Google Ads Editor**. É muito mais seguro do que subir direto via API se você está começando do zero, pois permite uma revisão final antes de "dar o play".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐍 Sugestão de Stack Tecnológica
|
||||||
|
|
||||||
|
| Componente | Ferramenta Recomendada |
|
||||||
|
| --- | --- |
|
||||||
|
| **Linguagem** | Python 3.10+ |
|
||||||
|
| **Scraping** | `requests` + `BeautifulSoup4` |
|
||||||
|
| **Inteligência** | `api oPENai` |
|
||||||
|
| **Interface** | `Streamlit` (para você colar a URL e ver o resultado na tela) |
|
||||||
|
| **Saída** | `pandas` (para gerar o CSV/Excel) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Meu "Pulo do Gato" para você
|
||||||
|
|
||||||
|
Não peça apenas palavras-chave óbvias. Peça para a IA identificar **"Dores do Cliente"** no texto da sua LP e transformar essas dores em **Extensões de Frase de Destaque (Callouts)**.
|
||||||
|
|
||||||
|
**Exemplo:** Se na sua LP diz "Suporte em 5 minutos", o Python deve extrair isso automaticamente como um diferencial do anúncio.
|
||||||
|
|
||||||
|
---
|
||||||
361
app.py
Normal file
361
app.py
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
"""
|
||||||
|
Google Ads Generator - Interface Streamlit
|
||||||
|
|
||||||
|
Aplicação que extrai conteúdo de Landing Pages e gera automaticamente
|
||||||
|
ativos de campanha para Google Ads usando IA (OpenAI ou Gemini).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import streamlit as st
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from src.scraper import scrape_landing_page
|
||||||
|
from src.ai_generator import generate_google_ads_assets, MODELS
|
||||||
|
from src.exporter import (
|
||||||
|
create_keywords_df,
|
||||||
|
create_negative_keywords_df,
|
||||||
|
create_ads_df,
|
||||||
|
create_sitelinks_df,
|
||||||
|
create_callouts_df,
|
||||||
|
export_all_to_excel,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── Carregar variáveis de ambiente ───────────────────────────────
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def _highlight_keywords(text: str, keywords: list[str]) -> str:
|
||||||
|
"""Destaca em negrito as palavras-chave encontradas no texto."""
|
||||||
|
result = text
|
||||||
|
for kw in keywords:
|
||||||
|
pattern = re.compile(re.escape(kw), re.IGNORECASE)
|
||||||
|
match = pattern.search(result)
|
||||||
|
if match:
|
||||||
|
result = result[:match.start()] + f"**{match.group()}**" + result[match.end():]
|
||||||
|
break # Destacar apenas a primeira keyword encontrada
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ─── Configuração da Página ───────────────────────────────────────
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="Google Ads Generator",
|
||||||
|
page_icon="📊",
|
||||||
|
layout="wide",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── CSS Customizado ──────────────────────────────────────────────
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
.main-header {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a73e8;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.sub-header {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #5f6368;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.metric-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
}
|
||||||
|
.metric-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
.stTabs [data-baseweb="tab-list"] {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.stTabs [data-baseweb="tab"] {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ─── Header ───────────────────────────────────────────────────────
|
||||||
|
st.markdown('<p class="main-header">Google Ads Generator</p>', unsafe_allow_html=True)
|
||||||
|
st.markdown(
|
||||||
|
'<p class="sub-header">Gere ativos de campanha automaticamente a partir da sua Landing Page</p>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── Sidebar ──────────────────────────────────────────────────────
|
||||||
|
with st.sidebar:
|
||||||
|
st.header("Configurações")
|
||||||
|
|
||||||
|
provider = st.selectbox(
|
||||||
|
"Provider de IA",
|
||||||
|
["OpenAI", "Gemini"],
|
||||||
|
index=0,
|
||||||
|
help="Escolha o provider de IA. As chaves devem estar configuradas no arquivo .env",
|
||||||
|
)
|
||||||
|
|
||||||
|
model = st.selectbox(
|
||||||
|
"Modelo",
|
||||||
|
MODELS.get(provider, []),
|
||||||
|
index=0,
|
||||||
|
help="Modelo a ser utilizado para geração dos ativos.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status das chaves configuradas
|
||||||
|
st.divider()
|
||||||
|
openai_ok = bool(os.environ.get("OPENAI_API_KEY"))
|
||||||
|
gemini_ok = bool(os.environ.get("GEMINI_API_KEY"))
|
||||||
|
st.caption("Status das API Keys (.env):")
|
||||||
|
st.markdown(f"- OpenAI: {'✅ Configurada' if openai_ok else '❌ Não encontrada'}")
|
||||||
|
st.markdown(f"- Gemini: {'✅ Configurada' if gemini_ok else '❌ Não encontrada'}")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
campaign_name = st.text_input(
|
||||||
|
"Nome da Campanha",
|
||||||
|
value="Campanha LP",
|
||||||
|
help="Nome que aparecerá na coluna 'Campaign' do CSV.",
|
||||||
|
)
|
||||||
|
|
||||||
|
ad_group = st.text_input(
|
||||||
|
"Nome do Grupo de Anúncios",
|
||||||
|
value="Grupo 1",
|
||||||
|
help="Nome que aparecerá na coluna 'Ad Group' do CSV.",
|
||||||
|
)
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
st.caption("Desenvolvido com Streamlit + OpenAI + Gemini")
|
||||||
|
|
||||||
|
# ─── Área Principal ───────────────────────────────────────────────
|
||||||
|
url = st.text_input(
|
||||||
|
"URL da Landing Page",
|
||||||
|
placeholder="https://www.seusite.com.br/landing-page",
|
||||||
|
help="Cole a URL completa da sua Landing Page aqui.",
|
||||||
|
)
|
||||||
|
|
||||||
|
col_btn, col_status = st.columns([1, 3])
|
||||||
|
|
||||||
|
with col_btn:
|
||||||
|
generate_btn = st.button("Gerar Campanha", type="primary", use_container_width=True)
|
||||||
|
|
||||||
|
# ─── Lógica Principal ────────────────────────────────────────────
|
||||||
|
if generate_btn:
|
||||||
|
# Validações
|
||||||
|
if not url:
|
||||||
|
st.error("Por favor, insira a URL da Landing Page.")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
if not url.startswith(("http://", "https://")):
|
||||||
|
st.error("A URL deve começar com http:// ou https://")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
# Verificar se a chave do provider selecionado está configurada
|
||||||
|
if provider == "OpenAI" and not os.environ.get("OPENAI_API_KEY"):
|
||||||
|
st.error("OPENAI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
|
||||||
|
st.stop()
|
||||||
|
elif provider == "Gemini" and not os.environ.get("GEMINI_API_KEY"):
|
||||||
|
st.error("GEMINI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
# Step 1: Scraping
|
||||||
|
with st.status("Processando...", expanded=True) as status:
|
||||||
|
st.write("Extraindo conteúdo da Landing Page...")
|
||||||
|
try:
|
||||||
|
lp_data = scrape_landing_page(url)
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Erro ao acessar a URL: {e}")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
st.write(f"Conteúdo extraído: {len(lp_data['paragraphs'])} parágrafos, "
|
||||||
|
f"{len(lp_data['h1'])} H1, {len(lp_data['h2'])} H2, "
|
||||||
|
f"{len(lp_data.get('list_items', []))} itens de lista, "
|
||||||
|
f"{len(lp_data['ctas'])} CTAs")
|
||||||
|
|
||||||
|
# Step 2: IA
|
||||||
|
st.write(f"Gerando ativos com {provider} ({model})...")
|
||||||
|
try:
|
||||||
|
assets, prompts = generate_google_ads_assets(
|
||||||
|
lp_content=lp_data["full_text"],
|
||||||
|
provider=provider,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
st.error(str(e))
|
||||||
|
st.stop()
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Erro ao gerar ativos: {e}")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
status.update(label="Concluído!", state="complete", expanded=False)
|
||||||
|
|
||||||
|
# Salvar no session_state
|
||||||
|
st.session_state["assets"] = assets
|
||||||
|
st.session_state["prompts"] = prompts
|
||||||
|
st.session_state["lp_data"] = lp_data
|
||||||
|
st.session_state["campaign_name"] = campaign_name
|
||||||
|
st.session_state["ad_group"] = ad_group
|
||||||
|
st.session_state["provider_used"] = provider
|
||||||
|
st.session_state["model_used"] = model
|
||||||
|
|
||||||
|
# ─── Exibição dos Resultados ─────────────────────────────────────
|
||||||
|
if "assets" in st.session_state:
|
||||||
|
assets = st.session_state["assets"]
|
||||||
|
prompts = st.session_state.get("prompts", {})
|
||||||
|
camp = st.session_state.get("campaign_name", "Campanha LP")
|
||||||
|
adg = st.session_state.get("ad_group", "Grupo 1")
|
||||||
|
prov_used = st.session_state.get("provider_used", "")
|
||||||
|
model_used = st.session_state.get("model_used", "")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# Métricas resumo
|
||||||
|
col1, col2, col3, col4, col5, col6 = st.columns(6)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.metric("Keywords", len(assets.get("keywords", [])))
|
||||||
|
with col2:
|
||||||
|
st.metric("Negativas", len(assets.get("negative_keywords", [])))
|
||||||
|
with col3:
|
||||||
|
st.metric("Títulos", len(assets.get("headlines", [])))
|
||||||
|
with col4:
|
||||||
|
st.metric("Descrições", len(assets.get("descriptions", [])))
|
||||||
|
with col5:
|
||||||
|
st.metric("Sitelinks", len(assets.get("sitelinks", [])))
|
||||||
|
with col6:
|
||||||
|
st.metric("Callouts", len(assets.get("callouts", [])))
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# Abas com resultados
|
||||||
|
tab_kw, tab_neg, tab_ads, tab_sl, tab_co, tab_prompts, tab_raw = st.tabs([
|
||||||
|
"Keywords", "Negativas", "Anúncios RSA", "Sitelinks", "Callouts",
|
||||||
|
"Prompts Utilizados", "Dados da LP",
|
||||||
|
])
|
||||||
|
|
||||||
|
with tab_kw:
|
||||||
|
st.subheader("Palavras-chave")
|
||||||
|
kw_df = create_keywords_df(assets, camp, adg)
|
||||||
|
if not kw_df.empty:
|
||||||
|
st.dataframe(kw_df, use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.info("Nenhuma palavra-chave gerada.")
|
||||||
|
|
||||||
|
with tab_neg:
|
||||||
|
st.subheader("Palavras-chave Negativas")
|
||||||
|
nkw_df = create_negative_keywords_df(assets, camp)
|
||||||
|
if not nkw_df.empty:
|
||||||
|
st.dataframe(nkw_df, use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.info("Nenhuma palavra-chave negativa gerada.")
|
||||||
|
|
||||||
|
with tab_ads:
|
||||||
|
st.subheader("Anúncio Responsivo de Pesquisa (RSA)")
|
||||||
|
|
||||||
|
st.write("**Títulos (Headlines):**")
|
||||||
|
# Coletar palavras-chave para destacar nos títulos
|
||||||
|
kw_list = sorted(
|
||||||
|
[kw["keyword"].lower() for kw in assets.get("keywords", []) if kw.get("keyword")],
|
||||||
|
key=len, reverse=True, # Maior primeiro para evitar match parcial
|
||||||
|
)
|
||||||
|
for i, h in enumerate(assets.get("headlines", []), 1):
|
||||||
|
char_count = len(h)
|
||||||
|
color = "green" if char_count <= 30 else "red"
|
||||||
|
h_highlighted = _highlight_keywords(h, kw_list)
|
||||||
|
st.markdown(f"{i}. {h_highlighted} — :{color}[{char_count} chars]")
|
||||||
|
|
||||||
|
st.write("**Descrições:**")
|
||||||
|
for i, d in enumerate(assets.get("descriptions", []), 1):
|
||||||
|
char_count = len(d)
|
||||||
|
color = "green" if char_count <= 90 else "red"
|
||||||
|
st.markdown(f"{i}. {d} — :{color}[{char_count} chars]")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
ads_df = create_ads_df(assets, camp, adg)
|
||||||
|
if not ads_df.empty:
|
||||||
|
st.dataframe(ads_df, use_container_width=True, hide_index=True)
|
||||||
|
|
||||||
|
with tab_sl:
|
||||||
|
st.subheader("Sitelinks")
|
||||||
|
sl_df = create_sitelinks_df(assets, camp)
|
||||||
|
if not sl_df.empty:
|
||||||
|
st.dataframe(sl_df, use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.info("Nenhum sitelink gerado.")
|
||||||
|
|
||||||
|
with tab_co:
|
||||||
|
st.subheader("Callouts (Frases de Destaque)")
|
||||||
|
co_df = create_callouts_df(assets, camp)
|
||||||
|
if not co_df.empty:
|
||||||
|
st.dataframe(co_df, use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.info("Nenhum callout gerado.")
|
||||||
|
|
||||||
|
with tab_prompts:
|
||||||
|
st.subheader("Prompts Utilizados")
|
||||||
|
st.caption(f"Provider: **{prov_used}** | Modelo: **{model_used}**")
|
||||||
|
|
||||||
|
st.markdown("#### Prompt de Sistema (System Prompt)")
|
||||||
|
st.code(prompts.get("system_prompt", "N/A"), language=None)
|
||||||
|
|
||||||
|
st.markdown("#### Prompt do Usuário (User Prompt)")
|
||||||
|
st.code(prompts.get("user_prompt", "N/A"), language=None)
|
||||||
|
|
||||||
|
with tab_raw:
|
||||||
|
st.subheader("Dados Extraídos da Landing Page")
|
||||||
|
lp_data = st.session_state.get("lp_data", {})
|
||||||
|
if lp_data:
|
||||||
|
st.write(f"**URL:** {lp_data.get('url', '')}")
|
||||||
|
st.write(f"**Título:** {lp_data.get('title', '')}")
|
||||||
|
st.write(f"**Meta Description:** {lp_data.get('meta_description', '')}")
|
||||||
|
|
||||||
|
with st.expander("Headings (H1, H2, H3)"):
|
||||||
|
for h in lp_data.get("h1", []):
|
||||||
|
st.write(f"**H1:** {h}")
|
||||||
|
for h in lp_data.get("h2", []):
|
||||||
|
st.write(f"**H2:** {h}")
|
||||||
|
for h in lp_data.get("h3", []):
|
||||||
|
st.write(f"**H3:** {h}")
|
||||||
|
|
||||||
|
with st.expander("Itens de Lista / Benefícios"):
|
||||||
|
for li in lp_data.get("list_items", []):
|
||||||
|
st.write(f"- {li}")
|
||||||
|
|
||||||
|
with st.expander("CTAs Encontrados"):
|
||||||
|
for c in lp_data.get("ctas", []):
|
||||||
|
st.write(f"- {c}")
|
||||||
|
|
||||||
|
with st.expander("Texto Completo Enviado à IA"):
|
||||||
|
st.code(lp_data.get("full_text", ""), language=None)
|
||||||
|
|
||||||
|
# ─── Botões de Download ───────────────────────────────────────
|
||||||
|
st.divider()
|
||||||
|
st.subheader("Download dos Ativos")
|
||||||
|
|
||||||
|
col_dl1, col_dl2 = st.columns(2)
|
||||||
|
|
||||||
|
with col_dl1:
|
||||||
|
excel_data = export_all_to_excel(assets, camp, adg)
|
||||||
|
st.download_button(
|
||||||
|
label="Baixar Excel (.xlsx)",
|
||||||
|
data=excel_data,
|
||||||
|
file_name="google_ads_assets.xlsx",
|
||||||
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
use_container_width=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with col_dl2:
|
||||||
|
kw_csv = create_keywords_df(assets, camp, adg).to_csv(index=False)
|
||||||
|
st.download_button(
|
||||||
|
label="Baixar Keywords (.csv)",
|
||||||
|
data=kw_csv,
|
||||||
|
file_name="google_ads_keywords.csv",
|
||||||
|
mime="text/csv",
|
||||||
|
use_container_width=True,
|
||||||
|
)
|
||||||
30
github.bat
Normal file
30
github.bat
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
echo === INICIANDO UPLOAD PARA GITHUB ===
|
||||||
|
|
||||||
|
REM Inicializar repositório Git
|
||||||
|
echo Inicializando repositorio Git...
|
||||||
|
git init
|
||||||
|
|
||||||
|
REM Adicionar todos os arquivos
|
||||||
|
echo Adicionando todos os arquivos...
|
||||||
|
git add .
|
||||||
|
|
||||||
|
REM Fazer commit inicial
|
||||||
|
echo Realizando commit inicial...
|
||||||
|
git commit -m "Commit inicial - upload de todos os arquivos da pasta"
|
||||||
|
|
||||||
|
REM Adicionar repositório remoto
|
||||||
|
echo Conectando ao repositorio remoto...
|
||||||
|
git remote add origin https://gitea.aplicativopro.com/wander/Google-Ads.git
|
||||||
|
|
||||||
|
REM Definir branch principal
|
||||||
|
echo Definindo branch principal como 'main'...
|
||||||
|
git branch -M main
|
||||||
|
|
||||||
|
REM Fazer push para o GitHub
|
||||||
|
echo Fazendo upload para o GitHub...
|
||||||
|
git push -u origin main
|
||||||
|
|
||||||
|
echo === UPLOAD CONCLUIDO COM SUCESSO! ===
|
||||||
|
|
||||||
|
pause
|
||||||
BIN
google_ads_assets.xlsx
Normal file
BIN
google_ads_assets.xlsx
Normal file
Binary file not shown.
21
google_ads_keywords (1).csv
Normal file
21
google_ads_keywords (1).csv
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Campaign,Ad Group,Keyword,Match Type,Status
|
||||||
|
Campanha LP,Grupo 1,[advogado divórcio online],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado guarda filhos""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado pensão alimentícia""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[divórcio extrajudicial online],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado inventário SP""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[calcular pensão alimentícia],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""divórcio amigável online""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado união estável SP""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[contratar advogado divórcio],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado especialista família""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[consulta advogado divórcio],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado guarda compartilhada""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[honorários advogado divórcio],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado pensão alimentícia SP""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[divórcio rápido online],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado inventário judicial""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[custo advogado divórcio],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado especialista em família""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[falar com advogado divórcio],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advogado divórcio preço""",Phrase,Enabled
|
||||||
|
16
google_ads_keywords.csv
Normal file
16
google_ads_keywords.csv
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Campaign,Ad Group,Keyword,Match Type,Status
|
||||||
|
Campanha LP,Grupo 1,[advogados trabalhistas],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""consultoria jurídica online""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[direitos do trabalhador],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""demissão injusta""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[horas extras não pagas],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""assédio moral advogado""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[rescisão de contrato],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""consultoria gratuita advogados""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[direito do trabalho],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""advocacia online""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[defesa de direitos trabalhistas],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""avaliação jurídica gratuita""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[advogado demissão],Exact,Enabled
|
||||||
|
Campanha LP,Grupo 1,"""horas extras advogado""",Phrase,Enabled
|
||||||
|
Campanha LP,Grupo 1,[consultar advogado online],Exact,Enabled
|
||||||
|
56
planejamento.md
Normal file
56
planejamento.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Google Ads Generator from LP
|
||||||
|
|
||||||
|
## Arquitetura do Projeto
|
||||||
|
|
||||||
|
O projeto seguirá a estrutura modular sugerida:
|
||||||
|
1. **Interface (Streamlit):** Entrada da URL e Chave da API, exibição dos resultados e botão de download.
|
||||||
|
2. **Scraper (BeautifulSoup):** Extração de texto visível, títulos e meta tags da URL fornecida.
|
||||||
|
3. **IA (OpenAI API):** Geração de palavras-chave, títulos, descrições e extensões com base no conteúdo extraído.
|
||||||
|
4. **Processamento e Exportação (Pandas):** Formatação dos dados para CSV compatível com Google Ads Editor.
|
||||||
|
|
||||||
|
## Passos de Implementação
|
||||||
|
|
||||||
|
### 1. Configuração do Ambiente
|
||||||
|
- Criar arquivo `requirements.txt` com as dependências: `streamlit`, `pandas`, `requests`, `beautifulsoup4`, `openai`, `python-dotenv`.
|
||||||
|
- Configurar estrutura de pastas.
|
||||||
|
|
||||||
|
### 2. Módulo de Scraping (`src/scraper.py`)
|
||||||
|
- Criar função `scrape_landing_page(url)` que retorna um dicionário com:
|
||||||
|
- Título da página (`title`)
|
||||||
|
- Meta description
|
||||||
|
- Headings (`h1`, `h2`, `h3`)
|
||||||
|
- Texto principal (parágrafos relevantes)
|
||||||
|
- Texto de botões/CTAs
|
||||||
|
|
||||||
|
### 3. Módulo de IA (`src/ai_generator.py`)
|
||||||
|
- Configurar cliente OpenAI.
|
||||||
|
- Criar prompts específicos para:
|
||||||
|
- **Palavras-chave:** Fundo de funil, com tipos de correspondência (Exata, Frase).
|
||||||
|
- **Palavras-chave Negativas:** Identificar termos irrelevantes ou que atraiam tráfego desqualificado com base no conteúdo da LP.
|
||||||
|
- **Anúncios Responsivos (RSA):** 15 títulos (30 chars) e 4 descrições (90 chars).
|
||||||
|
- **Sitelinks:** 4 variações com texto e descrição.
|
||||||
|
- **Callouts (Frases de Destaque):** Extração de "Dores do Cliente" e diferenciais (o "Pulo do Gato").
|
||||||
|
|
||||||
|
### 4. Módulo de Exportação (`src/exporter.py`)
|
||||||
|
- Criar função para estruturar os dados em um DataFrame do Pandas.
|
||||||
|
- Mapear colunas para o formato padrão do Google Ads Editor (ex: `Campaign`, `Ad Group`, `Keyword`, `Headline 1`, etc.).
|
||||||
|
- Gerar CSV para download.
|
||||||
|
|
||||||
|
### 5. Interface do Usuário (`app.py`)
|
||||||
|
- Criar layout com Streamlit.
|
||||||
|
- Campo para input da URL.
|
||||||
|
- Campo para input da OpenAI API Key (opcional se usar .env, mas bom para interface).
|
||||||
|
- Botão "Gerar Campanha".
|
||||||
|
- Exibição dos resultados em abas (Keywords, Negative Keywords, Ads, Extensions).
|
||||||
|
- Botão de download do CSV final.
|
||||||
|
|
||||||
|
OpenAI (do mais barato ao mais caro):
|
||||||
|
gpt-4.1-mini -- mais recente e barato
|
||||||
|
gpt-4o-mini -- otimo custo-beneficio
|
||||||
|
gpt-4.1 -- mais capaz, custo moderado
|
||||||
|
gpt-4o -- alta qualidade
|
||||||
|
Gemini (do mais barato ao mais caro):
|
||||||
|
gemini-2.0-flash-lite -- mais barato do Gemini
|
||||||
|
gemini-2.0-flash -- equilibrio custo/qualidade
|
||||||
|
gemini-2.5-flash-lite -- mais recente
|
||||||
|
gemini-1.5-flash -- estavel e confiavel
|
||||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
streamlit
|
||||||
|
pandas
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
openai
|
||||||
|
python-dotenv
|
||||||
|
openpyxl
|
||||||
|
google-genai
|
||||||
|
selenium
|
||||||
|
webdriver-manager
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/ai_generator.cpython-310.pyc
Normal file
BIN
src/__pycache__/ai_generator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/exporter.cpython-310.pyc
Normal file
BIN
src/__pycache__/exporter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/scraper.cpython-310.pyc
Normal file
BIN
src/__pycache__/scraper.cpython-310.pyc
Normal file
Binary file not shown.
263
src/ai_generator.py
Normal file
263
src/ai_generator.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
"""
|
||||||
|
Módulo de IA - Geração de ativos para Google Ads via OpenAI ou Gemini.
|
||||||
|
|
||||||
|
Recebe o conteúdo extraído da LP e gera palavras-chave,
|
||||||
|
anúncios responsivos, sitelinks, callouts e palavras-chave negativas.
|
||||||
|
Suporta múltiplos providers de IA (OpenAI e Google Gemini).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from openai import OpenAI
|
||||||
|
from google import genai
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Modelos disponíveis por provider ─────────────────────────────
|
||||||
|
MODELS = {
|
||||||
|
"OpenAI": ["gpt-4.1-mini", "gpt-4o-mini", "gpt-4.1", "gpt-4o"],
|
||||||
|
"Gemini": ["gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-2.5-flash-lite", "gemini-1.5-flash"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_google_ads_assets(
|
||||||
|
lp_content: str,
|
||||||
|
provider: str = "OpenAI",
|
||||||
|
model: str = "gpt-4o-mini",
|
||||||
|
) -> tuple[dict, dict]:
|
||||||
|
"""
|
||||||
|
Gera todos os ativos de campanha Google Ads a partir do conteúdo da LP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lp_content: Texto completo extraído da LP (full_text do scraper).
|
||||||
|
provider: Provider de IA ("OpenAI" ou "Gemini").
|
||||||
|
model: Modelo a ser usado (deve ser compatível com o provider).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tupla com:
|
||||||
|
- assets: Dicionário com keywords, negative_keywords, headlines, etc.
|
||||||
|
- prompts: Dicionário com system_prompt e user_prompt usados.
|
||||||
|
"""
|
||||||
|
system_prompt = _build_system_prompt()
|
||||||
|
user_prompt = _build_user_prompt(lp_content)
|
||||||
|
|
||||||
|
prompts = {
|
||||||
|
"system_prompt": system_prompt,
|
||||||
|
"user_prompt": user_prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider == "OpenAI":
|
||||||
|
raw_response = _call_openai(system_prompt, user_prompt, model)
|
||||||
|
elif provider == "Gemini":
|
||||||
|
raw_response = _call_gemini(system_prompt, user_prompt, model)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Provider não suportado: {provider}")
|
||||||
|
|
||||||
|
# Limpar possíveis blocos de código markdown
|
||||||
|
raw_response = _clean_json_response(raw_response)
|
||||||
|
|
||||||
|
result = json.loads(raw_response)
|
||||||
|
assets = _validate_and_normalize(result)
|
||||||
|
|
||||||
|
return assets, prompts
|
||||||
|
|
||||||
|
|
||||||
|
def _call_openai(system_prompt: str, user_prompt: str, model: str) -> str:
|
||||||
|
"""Chama a API da OpenAI e retorna a resposta bruta."""
|
||||||
|
api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("OPENAI_API_KEY não encontrada no arquivo .env")
|
||||||
|
|
||||||
|
client = OpenAI(api_key=api_key)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt},
|
||||||
|
],
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=4000,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.choices[0].message.content.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _call_gemini(system_prompt: str, user_prompt: str, model: str, max_retries: int = 2) -> str:
|
||||||
|
"""
|
||||||
|
Chama a API do Google Gemini e retorna a resposta bruta.
|
||||||
|
Faz retry automático em caso de rate limit (429).
|
||||||
|
"""
|
||||||
|
api_key = os.environ.get("GEMINI_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("GEMINI_API_KEY não encontrada no arquivo .env")
|
||||||
|
|
||||||
|
client = genai.Client(api_key=api_key)
|
||||||
|
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
|
try:
|
||||||
|
response = client.models.generate_content(
|
||||||
|
model=model,
|
||||||
|
contents=user_prompt,
|
||||||
|
config=genai.types.GenerateContentConfig(
|
||||||
|
system_instruction=system_prompt,
|
||||||
|
temperature=0.7,
|
||||||
|
max_output_tokens=4000,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return response.text.strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_str = str(e)
|
||||||
|
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str:
|
||||||
|
# Extrair tempo de retry sugerido pela API
|
||||||
|
wait_match = re.search(r"retry.*?(\d+)", error_str, re.IGNORECASE)
|
||||||
|
wait_seconds = int(wait_match.group(1)) if wait_match else 45
|
||||||
|
|
||||||
|
if attempt < max_retries:
|
||||||
|
time.sleep(wait_seconds)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Gemini API: Limite de requisições excedido (Free Tier). "
|
||||||
|
f"Aguarde ~{wait_seconds}s e tente novamente, ou mude para um "
|
||||||
|
f"plano pago em https://ai.google.dev/pricing. "
|
||||||
|
f"Alternativa: use o provider OpenAI."
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_json_response(raw: str) -> str:
|
||||||
|
"""Remove blocos de código markdown e extrai apenas o JSON."""
|
||||||
|
raw = raw.strip()
|
||||||
|
if raw.startswith("```"):
|
||||||
|
raw = raw.split("\n", 1)[1]
|
||||||
|
if raw.endswith("```"):
|
||||||
|
raw = raw[:-3]
|
||||||
|
raw = raw.strip()
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _build_system_prompt() -> str:
|
||||||
|
"""Constrói o prompt de sistema."""
|
||||||
|
return (
|
||||||
|
"Você é um especialista em Google Ads com mais de 10 anos de experiência. "
|
||||||
|
"Seu trabalho é gerar ativos de campanha de alta performance a partir do "
|
||||||
|
"conteúdo de Landing Pages. Responda SEMPRE em formato JSON válido, "
|
||||||
|
"sem markdown, sem blocos de código. Apenas o JSON puro."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_user_prompt(lp_content: str) -> str:
|
||||||
|
"""Constrói o prompt do usuário para geração de ativos."""
|
||||||
|
return f"""Com base no conteúdo desta Landing Page, gere ativos completos para uma campanha de Google Ads.
|
||||||
|
|
||||||
|
=== CONTEÚDO DA LANDING PAGE ===
|
||||||
|
{lp_content}
|
||||||
|
=== FIM DO CONTEÚDO ===
|
||||||
|
|
||||||
|
Gere o seguinte em formato JSON:
|
||||||
|
|
||||||
|
{{
|
||||||
|
"keywords": [
|
||||||
|
{{"keyword": "exemplo de palavra-chave", "match_type": "Exact"}},
|
||||||
|
{{"keyword": "outro exemplo", "match_type": "Phrase"}}
|
||||||
|
],
|
||||||
|
"negative_keywords": [
|
||||||
|
{{"keyword": "grátis", "reason": "Atrai tráfego que não converte"}},
|
||||||
|
{{"keyword": "como fazer", "reason": "Topo de funil, não converte"}}
|
||||||
|
],
|
||||||
|
"headlines": [
|
||||||
|
"Título com até 30 caracteres"
|
||||||
|
],
|
||||||
|
"descriptions": [
|
||||||
|
"Descrição com até 90 caracteres que destaca benefícios e inclui CTA"
|
||||||
|
],
|
||||||
|
"sitelinks": [
|
||||||
|
{{"title": "Título do Sitelink", "description1": "Linha 1", "description2": "Linha 2"}}
|
||||||
|
],
|
||||||
|
"callouts": [
|
||||||
|
"Frase de destaque curta"
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
|
||||||
|
REGRAS OTIMIZADAS PARA GOOGLE ADS:
|
||||||
|
|
||||||
|
1. PALAVRAS-CHAVE (Fundo de Funil): Gere 20 termos com alta intenção de contratação. Use [Exata] e "Frase". Limite os termos a no máximo 25 caracteres para viabilizar o uso nos títulos.
|
||||||
|
2. NEGATIVAS: Gere 20 termos que filtrem estudantes, curiosos e buscas gratuitas (ex: pdf, curso, modelo, jurisprudência, tcc).
|
||||||
|
3. TÍTULOS (Headlines): Gere 15 títulos (máx. 30 caracteres). Regra de Ouro: A palavra-chave da Regra 1 deve aparecer de forma INTEGRAL e IDENTICA no título. Se a keyword for longa, o título será apenas ela.
|
||||||
|
4. DESCRIÇÕES: Gere 4 descrições (máx. 90 caracteres). Devem terminar obrigatoriamente com um ponto final ou exclamação. Inclua uma Proposta Única de Valor (UVP).
|
||||||
|
5. EXTENSÕES (Sitelinks & Callouts):
|
||||||
|
- 4 Sitelinks (Título 25ch / Desc 35ch).
|
||||||
|
- 6 Callouts (Frases de destaque, máx 25ch) focadas em autoridade e agilidade.
|
||||||
|
6. POLÍTICAS EDITORIAIS:
|
||||||
|
- Proibido: "Grátis" e sinônimos.
|
||||||
|
- Proibido: CAIXA ALTA em palavras inteiras (exceto siglas como SP, OAB).
|
||||||
|
- Proibido: Uso excessivo de pontuação (ex: !!!).
|
||||||
|
|
||||||
|
RETORNO: Apenas o JSON estruturado."""
|
||||||
|
|
||||||
|
def _validate_and_normalize(data: dict) -> dict:
|
||||||
|
"""Valida e normaliza os dados retornados pela IA."""
|
||||||
|
result = {
|
||||||
|
"keywords": [],
|
||||||
|
"negative_keywords": [],
|
||||||
|
"headlines": [],
|
||||||
|
"descriptions": [],
|
||||||
|
"sitelinks": [],
|
||||||
|
"callouts": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keywords
|
||||||
|
for kw in data.get("keywords", []):
|
||||||
|
if isinstance(kw, dict) and "keyword" in kw:
|
||||||
|
result["keywords"].append({
|
||||||
|
"keyword": kw["keyword"],
|
||||||
|
"match_type": kw.get("match_type", "Phrase"),
|
||||||
|
})
|
||||||
|
elif isinstance(kw, str):
|
||||||
|
result["keywords"].append({"keyword": kw, "match_type": "Phrase"})
|
||||||
|
|
||||||
|
# Negative Keywords
|
||||||
|
for nkw in data.get("negative_keywords", []):
|
||||||
|
if isinstance(nkw, dict) and "keyword" in nkw:
|
||||||
|
result["negative_keywords"].append({
|
||||||
|
"keyword": nkw["keyword"],
|
||||||
|
"reason": nkw.get("reason", ""),
|
||||||
|
})
|
||||||
|
elif isinstance(nkw, str):
|
||||||
|
result["negative_keywords"].append({"keyword": nkw, "reason": ""})
|
||||||
|
|
||||||
|
# Headlines - garantir limite de 30 chars
|
||||||
|
for h in data.get("headlines", []):
|
||||||
|
if isinstance(h, str) and len(h) <= 30:
|
||||||
|
result["headlines"].append(h)
|
||||||
|
elif isinstance(h, str):
|
||||||
|
result["headlines"].append(h[:30])
|
||||||
|
|
||||||
|
# Descriptions - garantir limite de 90 chars
|
||||||
|
for d in data.get("descriptions", []):
|
||||||
|
if isinstance(d, str) and len(d) <= 90:
|
||||||
|
result["descriptions"].append(d)
|
||||||
|
elif isinstance(d, str):
|
||||||
|
result["descriptions"].append(d[:90])
|
||||||
|
|
||||||
|
# Sitelinks
|
||||||
|
for sl in data.get("sitelinks", []):
|
||||||
|
if isinstance(sl, dict) and "title" in sl:
|
||||||
|
result["sitelinks"].append({
|
||||||
|
"title": sl.get("title", "")[:25],
|
||||||
|
"description1": sl.get("description1", "")[:35],
|
||||||
|
"description2": sl.get("description2", "")[:35],
|
||||||
|
})
|
||||||
|
|
||||||
|
# Callouts
|
||||||
|
for c in data.get("callouts", []):
|
||||||
|
if isinstance(c, str) and len(c) <= 25:
|
||||||
|
result["callouts"].append(c)
|
||||||
|
elif isinstance(c, str):
|
||||||
|
result["callouts"].append(c[:25])
|
||||||
|
|
||||||
|
return result
|
||||||
163
src/exporter.py
Normal file
163
src/exporter.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"""
|
||||||
|
Módulo de Exportação - Gera arquivos CSV compatíveis com Google Ads Editor.
|
||||||
|
|
||||||
|
Recebe os ativos gerados pela IA e os formata em DataFrames do Pandas,
|
||||||
|
prontos para importação no Google Ads Editor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
def create_keywords_df(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> pd.DataFrame:
|
||||||
|
"""Cria DataFrame de palavras-chave no formato Google Ads Editor."""
|
||||||
|
rows = []
|
||||||
|
for kw in assets.get("keywords", []):
|
||||||
|
match_type = kw.get("match_type", "Phrase")
|
||||||
|
keyword = kw.get("keyword", "")
|
||||||
|
|
||||||
|
# Formatar keyword conforme tipo de correspondência
|
||||||
|
if match_type == "Exact":
|
||||||
|
formatted_kw = f"[{keyword}]"
|
||||||
|
elif match_type == "Phrase":
|
||||||
|
formatted_kw = f'"{keyword}"'
|
||||||
|
else:
|
||||||
|
formatted_kw = keyword
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"Campaign": campaign_name,
|
||||||
|
"Ad Group": ad_group,
|
||||||
|
"Keyword": formatted_kw,
|
||||||
|
"Match Type": match_type,
|
||||||
|
"Status": "Enabled",
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def create_negative_keywords_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||||
|
"""Cria DataFrame de palavras-chave negativas no formato Google Ads Editor."""
|
||||||
|
rows = []
|
||||||
|
for nkw in assets.get("negative_keywords", []):
|
||||||
|
rows.append({
|
||||||
|
"Campaign": campaign_name,
|
||||||
|
"Keyword": nkw.get("keyword", ""),
|
||||||
|
"Criterion Type": "Negative",
|
||||||
|
"Reason": nkw.get("reason", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ads_df(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> pd.DataFrame:
|
||||||
|
"""Cria DataFrame do anúncio responsivo (RSA) no formato Google Ads Editor."""
|
||||||
|
headlines = assets.get("headlines", [])
|
||||||
|
descriptions = assets.get("descriptions", [])
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"Campaign": campaign_name,
|
||||||
|
"Ad Group": ad_group,
|
||||||
|
"Ad Type": "Responsive Search Ad",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preencher até 15 headlines
|
||||||
|
for i, h in enumerate(headlines[:15], start=1):
|
||||||
|
row[f"Headline {i}"] = h
|
||||||
|
|
||||||
|
# Preencher até 4 descrições
|
||||||
|
for i, d in enumerate(descriptions[:4], start=1):
|
||||||
|
row[f"Description {i}"] = d
|
||||||
|
|
||||||
|
return pd.DataFrame([row])
|
||||||
|
|
||||||
|
|
||||||
|
def create_sitelinks_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||||
|
"""Cria DataFrame de sitelinks no formato Google Ads Editor."""
|
||||||
|
rows = []
|
||||||
|
for sl in assets.get("sitelinks", []):
|
||||||
|
rows.append({
|
||||||
|
"Campaign": campaign_name,
|
||||||
|
"Sitelink Text": sl.get("title", ""),
|
||||||
|
"Description Line 1": sl.get("description1", ""),
|
||||||
|
"Description Line 2": sl.get("description2", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def create_callouts_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||||
|
"""Cria DataFrame de callouts no formato Google Ads Editor."""
|
||||||
|
rows = []
|
||||||
|
for c in assets.get("callouts", []):
|
||||||
|
rows.append({
|
||||||
|
"Campaign": campaign_name,
|
||||||
|
"Callout Text": c,
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> BytesIO:
|
||||||
|
"""
|
||||||
|
Exporta todos os ativos em um único arquivo Excel com múltiplas abas.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO com o conteúdo do arquivo Excel.
|
||||||
|
"""
|
||||||
|
output = BytesIO()
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
|
kw_df = create_keywords_df(assets, campaign_name, ad_group)
|
||||||
|
if not kw_df.empty:
|
||||||
|
kw_df.to_excel(writer, sheet_name="Keywords", index=False)
|
||||||
|
|
||||||
|
nkw_df = create_negative_keywords_df(assets, campaign_name)
|
||||||
|
if not nkw_df.empty:
|
||||||
|
nkw_df.to_excel(writer, sheet_name="Negative Keywords", index=False)
|
||||||
|
|
||||||
|
ads_df = create_ads_df(assets, campaign_name, ad_group)
|
||||||
|
if not ads_df.empty:
|
||||||
|
ads_df.to_excel(writer, sheet_name="Ads RSA", index=False)
|
||||||
|
|
||||||
|
sl_df = create_sitelinks_df(assets, campaign_name)
|
||||||
|
if not sl_df.empty:
|
||||||
|
sl_df.to_excel(writer, sheet_name="Sitelinks", index=False)
|
||||||
|
|
||||||
|
co_df = create_callouts_df(assets, campaign_name)
|
||||||
|
if not co_df.empty:
|
||||||
|
co_df.to_excel(writer, sheet_name="Callouts", index=False)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Exporta todos os ativos como strings CSV separadas.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dicionário com nome da aba -> conteúdo CSV.
|
||||||
|
"""
|
||||||
|
csvs = {}
|
||||||
|
|
||||||
|
kw_df = create_keywords_df(assets, campaign_name, ad_group)
|
||||||
|
if not kw_df.empty:
|
||||||
|
csvs["keywords"] = kw_df.to_csv(index=False)
|
||||||
|
|
||||||
|
nkw_df = create_negative_keywords_df(assets, campaign_name)
|
||||||
|
if not nkw_df.empty:
|
||||||
|
csvs["negative_keywords"] = nkw_df.to_csv(index=False)
|
||||||
|
|
||||||
|
ads_df = create_ads_df(assets, campaign_name, ad_group)
|
||||||
|
if not ads_df.empty:
|
||||||
|
csvs["ads"] = ads_df.to_csv(index=False)
|
||||||
|
|
||||||
|
sl_df = create_sitelinks_df(assets, campaign_name)
|
||||||
|
if not sl_df.empty:
|
||||||
|
csvs["sitelinks"] = sl_df.to_csv(index=False)
|
||||||
|
|
||||||
|
co_df = create_callouts_df(assets, campaign_name)
|
||||||
|
if not co_df.empty:
|
||||||
|
csvs["callouts"] = co_df.to_csv(index=False)
|
||||||
|
|
||||||
|
return csvs
|
||||||
265
src/scraper.py
Normal file
265
src/scraper.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"""
|
||||||
|
Módulo de Scraping - Extração de conteúdo de Landing Pages.
|
||||||
|
|
||||||
|
Utiliza Selenium (Chrome headless) + BeautifulSoup4 para extrair
|
||||||
|
títulos, textos, CTAs e meta tags de uma Landing Page fornecida via URL.
|
||||||
|
O Selenium renderiza o JavaScript antes da extração, garantindo que
|
||||||
|
todo o conteúdo dinâmico seja capturado.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
|
||||||
|
|
||||||
|
def _create_driver() -> webdriver.Chrome:
|
||||||
|
"""Cria e retorna um driver Chrome headless."""
|
||||||
|
options = Options()
|
||||||
|
options.add_argument("--headless=new")
|
||||||
|
options.add_argument("--no-sandbox")
|
||||||
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
options.add_argument("--disable-gpu")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
|
options.add_argument("--disable-extensions")
|
||||||
|
options.add_argument("--disable-infobars")
|
||||||
|
options.add_argument(
|
||||||
|
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
# Suprimir logs do Chrome
|
||||||
|
options.add_argument("--log-level=3")
|
||||||
|
options.add_experimental_option("excludeSwitches", ["enable-logging"])
|
||||||
|
|
||||||
|
service = Service(ChromeDriverManager().install())
|
||||||
|
driver = webdriver.Chrome(service=service, options=options)
|
||||||
|
return driver
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_landing_page(url: str, wait_seconds: int = 5) -> dict:
|
||||||
|
"""
|
||||||
|
Faz scraping de uma Landing Page usando Selenium (Chrome headless)
|
||||||
|
e retorna um dicionário com os elementos relevantes para geração de anúncios.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: URL completa da Landing Page.
|
||||||
|
wait_seconds: Segundos para aguardar o carregamento do JS.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dicionário com as chaves:
|
||||||
|
- url: URL original
|
||||||
|
- title: Título da página (<title>)
|
||||||
|
- meta_description: Conteúdo da meta description
|
||||||
|
- h1: Lista de textos dos <h1>
|
||||||
|
- h2: Lista de textos dos <h2>
|
||||||
|
- h3: Lista de textos dos <h3>
|
||||||
|
- paragraphs: Lista dos parágrafos principais
|
||||||
|
- ctas: Lista de textos de botões e links de ação
|
||||||
|
- full_text: Texto completo concatenado (para envio à IA)
|
||||||
|
"""
|
||||||
|
driver = _create_driver()
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver.get(url)
|
||||||
|
|
||||||
|
# Aguardar o body estar presente (sinal de que a página carregou)
|
||||||
|
WebDriverWait(driver, 15).until(
|
||||||
|
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Aguardar tempo extra para JavaScript renderizar conteúdo dinâmico
|
||||||
|
time.sleep(wait_seconds)
|
||||||
|
|
||||||
|
# Scroll até o final para disparar lazy-loading
|
||||||
|
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Capturar HTML completo após renderização
|
||||||
|
page_source = driver.page_source
|
||||||
|
|
||||||
|
finally:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
# Parsear com BeautifulSoup
|
||||||
|
soup = BeautifulSoup(page_source, "html.parser")
|
||||||
|
|
||||||
|
# Remover scripts e styles para limpeza do texto
|
||||||
|
for tag in soup(["script", "style", "noscript", "iframe", "svg"]):
|
||||||
|
tag.decompose()
|
||||||
|
|
||||||
|
# Título da página
|
||||||
|
title = soup.title.string.strip() if soup.title and soup.title.string else ""
|
||||||
|
|
||||||
|
# Meta description
|
||||||
|
meta_desc_tag = soup.find("meta", attrs={"name": "description"})
|
||||||
|
meta_description = ""
|
||||||
|
if meta_desc_tag and meta_desc_tag.get("content"):
|
||||||
|
meta_description = meta_desc_tag["content"].strip()
|
||||||
|
|
||||||
|
# Headings
|
||||||
|
h1_tags = [tag.get_text(strip=True) for tag in soup.find_all("h1") if tag.get_text(strip=True)]
|
||||||
|
h2_tags = [tag.get_text(strip=True) for tag in soup.find_all("h2") if tag.get_text(strip=True)]
|
||||||
|
h3_tags = [tag.get_text(strip=True) for tag in soup.find_all("h3") if tag.get_text(strip=True)]
|
||||||
|
|
||||||
|
# Parágrafos relevantes (ignora parágrafos muito curtos)
|
||||||
|
paragraphs = [
|
||||||
|
tag.get_text(strip=True)
|
||||||
|
for tag in soup.find_all("p")
|
||||||
|
if tag.get_text(strip=True) and len(tag.get_text(strip=True)) > 20
|
||||||
|
]
|
||||||
|
|
||||||
|
# Lista items (muitas LPs usam <li> para benefícios)
|
||||||
|
list_items = [
|
||||||
|
tag.get_text(strip=True)
|
||||||
|
for tag in soup.find_all("li")
|
||||||
|
if tag.get_text(strip=True) and 10 < len(tag.get_text(strip=True)) < 200
|
||||||
|
]
|
||||||
|
|
||||||
|
# Spans e divs com texto significativo (para LPs que não usam <p>)
|
||||||
|
extra_texts = _extract_visible_text_blocks(soup)
|
||||||
|
|
||||||
|
# CTAs - botões e links com texto de ação
|
||||||
|
ctas = _extract_ctas(soup)
|
||||||
|
|
||||||
|
# Texto completo para enviar à IA
|
||||||
|
full_text = _build_full_text(
|
||||||
|
title, meta_description, h1_tags, h2_tags, h3_tags,
|
||||||
|
paragraphs, list_items, extra_texts, ctas,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"title": title,
|
||||||
|
"meta_description": meta_description,
|
||||||
|
"h1": h1_tags,
|
||||||
|
"h2": h2_tags,
|
||||||
|
"h3": h3_tags,
|
||||||
|
"paragraphs": paragraphs,
|
||||||
|
"list_items": list_items,
|
||||||
|
"ctas": ctas,
|
||||||
|
"full_text": full_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_visible_text_blocks(soup: BeautifulSoup) -> list[str]:
|
||||||
|
"""
|
||||||
|
Extrai blocos de texto visível de divs e spans que não estão
|
||||||
|
dentro de tags semânticas (p, h1-h6, li, button, a).
|
||||||
|
Útil para LPs que usam divs customizados para conteúdo.
|
||||||
|
"""
|
||||||
|
semantic_tags = {"p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "button", "a", "input", "label"}
|
||||||
|
texts = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
for tag in soup.find_all(["div", "span"]):
|
||||||
|
# Pular se tem filhos que são tags semânticas
|
||||||
|
if tag.find(list(semantic_tags)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = tag.get_text(strip=True)
|
||||||
|
if text and 30 < len(text) < 500 and text not in seen:
|
||||||
|
seen.add(text)
|
||||||
|
texts.append(text)
|
||||||
|
|
||||||
|
return texts[:15] # Limitar para não poluir
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_ctas(soup: BeautifulSoup) -> list[str]:
|
||||||
|
"""Extrai textos de botões e links que parecem ser CTAs."""
|
||||||
|
ctas = []
|
||||||
|
|
||||||
|
# Botões
|
||||||
|
for btn in soup.find_all("button"):
|
||||||
|
text = btn.get_text(strip=True)
|
||||||
|
if text and len(text) < 80:
|
||||||
|
ctas.append(text)
|
||||||
|
|
||||||
|
# Inputs do tipo submit
|
||||||
|
for inp in soup.find_all("input", attrs={"type": "submit"}):
|
||||||
|
value = inp.get("value", "").strip()
|
||||||
|
if value:
|
||||||
|
ctas.append(value)
|
||||||
|
|
||||||
|
# Links com classes comuns de CTA ou texto indicativo
|
||||||
|
cta_keywords = [
|
||||||
|
"btn", "button", "cta", "action", "comprar", "contratar",
|
||||||
|
"saiba", "agendar", "solicitar", "falar", "whatsapp",
|
||||||
|
"contato", "orcamento", "orçamento", "consulta", "agende",
|
||||||
|
]
|
||||||
|
for a_tag in soup.find_all("a"):
|
||||||
|
classes = " ".join(a_tag.get("class", [])).lower()
|
||||||
|
href = (a_tag.get("href") or "").lower()
|
||||||
|
text = a_tag.get_text(strip=True)
|
||||||
|
if text and len(text) < 80:
|
||||||
|
if any(kw in classes for kw in cta_keywords):
|
||||||
|
ctas.append(text)
|
||||||
|
elif any(kw in text.lower() for kw in cta_keywords):
|
||||||
|
ctas.append(text)
|
||||||
|
elif "whatsapp" in href or "wa.me" in href:
|
||||||
|
ctas.append(text)
|
||||||
|
|
||||||
|
# Remover duplicados mantendo ordem
|
||||||
|
seen = set()
|
||||||
|
unique_ctas = []
|
||||||
|
for cta in ctas:
|
||||||
|
if cta not in seen:
|
||||||
|
seen.add(cta)
|
||||||
|
unique_ctas.append(cta)
|
||||||
|
|
||||||
|
return unique_ctas
|
||||||
|
|
||||||
|
|
||||||
|
def _build_full_text(
|
||||||
|
title: str,
|
||||||
|
meta_description: str,
|
||||||
|
h1: list[str],
|
||||||
|
h2: list[str],
|
||||||
|
h3: list[str],
|
||||||
|
paragraphs: list[str],
|
||||||
|
list_items: list[str],
|
||||||
|
extra_texts: list[str],
|
||||||
|
ctas: list[str],
|
||||||
|
) -> str:
|
||||||
|
"""Monta um texto completo e estruturado da LP para envio à IA."""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
if title:
|
||||||
|
parts.append(f"TÍTULO DA PÁGINA: {title}")
|
||||||
|
if meta_description:
|
||||||
|
parts.append(f"META DESCRIPTION: {meta_description}")
|
||||||
|
|
||||||
|
if h1:
|
||||||
|
parts.append("TÍTULOS PRINCIPAIS (H1):")
|
||||||
|
parts.extend(f" - {h}" for h in h1)
|
||||||
|
|
||||||
|
if h2:
|
||||||
|
parts.append("SUBTÍTULOS (H2):")
|
||||||
|
parts.extend(f" - {h}" for h in h2)
|
||||||
|
|
||||||
|
if h3:
|
||||||
|
parts.append("SUBTÍTULOS (H3):")
|
||||||
|
parts.extend(f" - {h}" for h in h3)
|
||||||
|
|
||||||
|
if paragraphs:
|
||||||
|
parts.append("TEXTOS PRINCIPAIS:")
|
||||||
|
parts.extend(f" {p}" for p in paragraphs[:25])
|
||||||
|
|
||||||
|
if list_items:
|
||||||
|
parts.append("ITENS DE LISTA / BENEFÍCIOS:")
|
||||||
|
parts.extend(f" - {li}" for li in list_items[:15])
|
||||||
|
|
||||||
|
if extra_texts:
|
||||||
|
parts.append("OUTROS TEXTOS VISÍVEIS:")
|
||||||
|
parts.extend(f" {t}" for t in extra_texts[:10])
|
||||||
|
|
||||||
|
if ctas:
|
||||||
|
parts.append("CHAMADAS PARA AÇÃO (CTAs):")
|
||||||
|
parts.extend(f" - {c}" for c in ctas)
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
Reference in New Issue
Block a user