commit e388183601d3b074b997cb4d9a4ba6552459ac82 Author: WanderMotta Date: Fri Feb 13 01:31:03 2026 -0300 Commit inicial - upload de todos os arquivos da pasta diff --git a/.env b/.env new file mode 100644 index 0000000..22a45ac --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +OPENAI_API_KEY=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 +GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a902301 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY=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 +GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA diff --git a/Campanha-LP-Civil-Otimizada.md b/Campanha-LP-Civil-Otimizada.md new file mode 100644 index 0000000..fef5b0f --- /dev/null +++ b/Campanha-LP-Civil-Otimizada.md @@ -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" + ] + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..06de387 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Links Uteis.txt b/Links Uteis.txt new file mode 100644 index 0000000..ac11c16 --- /dev/null +++ b/Links Uteis.txt @@ -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 \ No newline at end of file diff --git a/Prompt.md b/Prompt.md new file mode 100644 index 0000000..4eeaa21 --- /dev/null +++ b/Prompt.md @@ -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. + +--- diff --git a/app.py b/app.py new file mode 100644 index 0000000..8aa38f6 --- /dev/null +++ b/app.py @@ -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(""" + +""", unsafe_allow_html=True) + +# ─── Header ─────────────────────────────────────────────────────── +st.markdown('

Google Ads Generator

', unsafe_allow_html=True) +st.markdown( + '

Gere ativos de campanha automaticamente a partir da sua Landing Page

', + 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, + ) diff --git a/app.zip b/app.zip new file mode 100644 index 0000000..9d5bcf3 Binary files /dev/null and b/app.zip differ diff --git a/github.bat b/github.bat new file mode 100644 index 0000000..8a458f7 --- /dev/null +++ b/github.bat @@ -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 \ No newline at end of file diff --git a/google_ads_assets.xlsx b/google_ads_assets.xlsx new file mode 100644 index 0000000..549d2d7 Binary files /dev/null and b/google_ads_assets.xlsx differ diff --git a/google_ads_keywords (1).csv b/google_ads_keywords (1).csv new file mode 100644 index 0000000..7e175bf --- /dev/null +++ b/google_ads_keywords (1).csv @@ -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 diff --git a/google_ads_keywords.csv b/google_ads_keywords.csv new file mode 100644 index 0000000..71a0f8c --- /dev/null +++ b/google_ads_keywords.csv @@ -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 diff --git a/planejamento.md b/planejamento.md new file mode 100644 index 0000000..3780311 --- /dev/null +++ b/planejamento.md @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..11554c5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +streamlit +pandas +requests +beautifulsoup4 +openai +python-dotenv +openpyxl +google-genai +selenium +webdriver-manager diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d56579c Binary files /dev/null and b/src/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/__pycache__/ai_generator.cpython-310.pyc b/src/__pycache__/ai_generator.cpython-310.pyc new file mode 100644 index 0000000..b9bb40c Binary files /dev/null and b/src/__pycache__/ai_generator.cpython-310.pyc differ diff --git a/src/__pycache__/exporter.cpython-310.pyc b/src/__pycache__/exporter.cpython-310.pyc new file mode 100644 index 0000000..351e60c Binary files /dev/null and b/src/__pycache__/exporter.cpython-310.pyc differ diff --git a/src/__pycache__/scraper.cpython-310.pyc b/src/__pycache__/scraper.cpython-310.pyc new file mode 100644 index 0000000..d8163c5 Binary files /dev/null and b/src/__pycache__/scraper.cpython-310.pyc differ diff --git a/src/ai_generator.py b/src/ai_generator.py new file mode 100644 index 0000000..b957d24 --- /dev/null +++ b/src/ai_generator.py @@ -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 diff --git a/src/exporter.py b/src/exporter.py new file mode 100644 index 0000000..c6ddae4 --- /dev/null +++ b/src/exporter.py @@ -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 diff --git a/src/scraper.py b/src/scraper.py new file mode 100644 index 0000000..9875b8d --- /dev/null +++ b/src/scraper.py @@ -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 () + - 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)