Commit inicial - upload de todos os arquivos da pasta

This commit is contained in:
2026-02-13 01:31:03 -03:00
commit e388183601
22 changed files with 1382 additions and 0 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
OPENAI_API_KEY=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
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
OPENAI_API_KEY=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
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA

View 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
View 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
View 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
View 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
View 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,
)

BIN
app.zip Normal file

Binary file not shown.

30
github.bat Normal file
View 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

Binary file not shown.

View 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
1 Campaign Ad Group Keyword Match Type Status
2 Campanha LP Grupo 1 [advogado divórcio online] Exact Enabled
3 Campanha LP Grupo 1 "advogado guarda filhos" Phrase Enabled
4 Campanha LP Grupo 1 "advogado pensão alimentícia" Phrase Enabled
5 Campanha LP Grupo 1 [divórcio extrajudicial online] Exact Enabled
6 Campanha LP Grupo 1 "advogado inventário SP" Phrase Enabled
7 Campanha LP Grupo 1 [calcular pensão alimentícia] Exact Enabled
8 Campanha LP Grupo 1 "divórcio amigável online" Phrase Enabled
9 Campanha LP Grupo 1 "advogado união estável SP" Phrase Enabled
10 Campanha LP Grupo 1 [contratar advogado divórcio] Exact Enabled
11 Campanha LP Grupo 1 "advogado especialista família" Phrase Enabled
12 Campanha LP Grupo 1 [consulta advogado divórcio] Exact Enabled
13 Campanha LP Grupo 1 "advogado guarda compartilhada" Phrase Enabled
14 Campanha LP Grupo 1 [honorários advogado divórcio] Exact Enabled
15 Campanha LP Grupo 1 "advogado pensão alimentícia SP" Phrase Enabled
16 Campanha LP Grupo 1 [divórcio rápido online] Exact Enabled
17 Campanha LP Grupo 1 "advogado inventário judicial" Phrase Enabled
18 Campanha LP Grupo 1 [custo advogado divórcio] Exact Enabled
19 Campanha LP Grupo 1 "advogado especialista em família" Phrase Enabled
20 Campanha LP Grupo 1 [falar com advogado divórcio] Exact Enabled
21 Campanha LP Grupo 1 "advogado divórcio preço" Phrase Enabled

16
google_ads_keywords.csv Normal file
View 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
1 Campaign Ad Group Keyword Match Type Status
2 Campanha LP Grupo 1 [advogados trabalhistas] Exact Enabled
3 Campanha LP Grupo 1 "consultoria jurídica online" Phrase Enabled
4 Campanha LP Grupo 1 [direitos do trabalhador] Exact Enabled
5 Campanha LP Grupo 1 "demissão injusta" Phrase Enabled
6 Campanha LP Grupo 1 [horas extras não pagas] Exact Enabled
7 Campanha LP Grupo 1 "assédio moral advogado" Phrase Enabled
8 Campanha LP Grupo 1 [rescisão de contrato] Exact Enabled
9 Campanha LP Grupo 1 "consultoria gratuita advogados" Phrase Enabled
10 Campanha LP Grupo 1 [direito do trabalho] Exact Enabled
11 Campanha LP Grupo 1 "advocacia online" Phrase Enabled
12 Campanha LP Grupo 1 [defesa de direitos trabalhistas] Exact Enabled
13 Campanha LP Grupo 1 "avaliação jurídica gratuita" Phrase Enabled
14 Campanha LP Grupo 1 [advogado demissão] Exact Enabled
15 Campanha LP Grupo 1 "horas extras advogado" Phrase Enabled
16 Campanha LP Grupo 1 [consultar advogado online] Exact Enabled

56
planejamento.md Normal file
View 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
View File

@@ -0,0 +1,10 @@
streamlit
pandas
requests
beautifulsoup4
openai
python-dotenv
openpyxl
google-genai
selenium
webdriver-manager

0
src/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

263
src/ai_generator.py Normal file
View 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
View 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
View 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)