diff --git a/.env b/.env
index 22a45ac..cd2b306 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
OPENAI_API_KEY=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
-GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA
\ No newline at end of file
+GEMINI_API_KEY=AIzaSyA_NLebC6j6LJddKvSJ9Bn6W-zHrBG655Y
\ No newline at end of file
diff --git a/Auditoria.md b/Auditoria.md
new file mode 100644
index 0000000..ff331ef
--- /dev/null
+++ b/Auditoria.md
@@ -0,0 +1,37 @@
+Objetivo: criar um Framework de Auditoria permite que você trate a AI como um "Quality Assurance" (QA) rigoroso.
+
+Minha sugestão é criarmos o "Google Ads Scorecard (GAS)". Ele será um conjunto de regras que a AI deve aplicar ao JSON (fornecido) para gerar a nota.
+
+Proposta de Checklist de Auditoria (Regras de Qualificação)
+Para que a AI dê uma nota de 0 a 10, ela deve pontuar os seguintes critérios:
+
+1. Relevância e Keyword Match (Peso 3.0)
+Critério: Pelo menos 5 títulos devem ser idênticos às keywords de fundo de funil.
+
+Penalidade: -0.5 para cada título que tentar "enfeitar" a palavra-chave principal (ex: mudar "advogado guarda" para "melhor advogado de guarda").
+
+2. Compliance Editorial (Peso 2.0)
+Critério: Ausência total de termos proibidos (grátis, gratuito) e formatação (excesso de pontuação ou caixa alta).
+
+Penalidade: Nota 0 imediata em Compliance se houver a palavra "grátis".
+
+3. Aproveitamento de Espaço (Peso 2.0)
+Critério: As descrições devem ter entre 80 e 90 caracteres. Títulos devem estar próximos dos 30.
+
+Penalidade: -0.5 para descrições muito curtas que desperdiçam o "espaço de tela" no celular.
+
+4. Variedade de Gatilhos (Peso 2.0)
+Critério: Presença de 3 pilares: Ação (CTA), Autoridade (Anos de exp/Nota 5.0) e Localização (Cidade/Estado).
+
+Penalidade: -1.0 se todos os títulos forem apenas descritivos, sem convidar ao clique.
+
+5. Extensões e Ativos (Peso 1.0)
+Critério: Sitelinks com descrições preenchidas e Callouts focadas em benefícios reais.
+
+Como aplicar isso agora?
+Você pode configurar essas regras como um "Juiz de Campanha". Sempre que você gerar um JSON, você roda este prompt:
+
+"Aja como Auditor de Google Ads. Analise o JSON enviado e atribua uma nota de 0 a 10 com base nos seguintes pesos: Keyword Match (3.0), Compliance (2.0), Espaço (2.0), Gatilhos (2.0) e Ativos (1.0). Liste as falhas específicas e como corrigi-las para chegar ao 10."
+
+Minha ideia adicional: O "Simulador de CTR"
+Além da nota, podemos pedir para a AI simular como o anúncio apareceria no Google (em formato de texto) para você visualizar se ele ficou atraente visualmente.
\ No newline at end of file
diff --git a/Campanha-LP-Civil-Otimizada-1.md b/Campanha-LP-Civil-Otimizada-1.md
new file mode 100644
index 0000000..2ee0a4c
--- /dev/null
+++ b/Campanha-LP-Civil-Otimizada-1.md
@@ -0,0 +1,82 @@
+{
+ "campanha_google_ads_definitiva": {
+ "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", "gratuito", "pdf", "curso", "vagas", "estágio", "faculdade", "tcc", "modelo de petição", "jurisprudência", "concurso", "como fazer", "lei", "livro", "apostila", "trabalhista", "criminal", "previdenciário", "defensoria pública", "fórum"
+ ],
+ "titulos_headlines": [
+ "Advogado divórcio online",
+ "Advogado guarda filhos",
+ "Advogado pensão alimentícia",
+ "Advogado inventário SP",
+ "Advogado união estável",
+ "Fale Conosco em SP",
+ "Agende sua Consulta Hoje",
+ "Atendimento Especializado SP",
+ "Falar com um Especialista",
+ "Avaliar meu caso agora",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Atendimento 100% Online",
+ "Sigilo Absoluto no Seu Caso",
+ "Soluções Jurídicas Rápidas"
+ ],
+ "descricoes": [
+ "Advocacia especializada em Direito Civil e Família. Atendimento 100% online e sigiloso!",
+ "Resolva seu divórcio, guarda ou inventário com agilidade. Mais de 10 anos de experiência.",
+ "Proteja os direitos da sua família com especialistas. Avaliação nota 5.0. Fale conosco!",
+ "Soluções rápidas para pensão e partilha de bens. Atendimento humanizado em todo o SP."
+ ],
+ "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": "Partilha justa e solução ágil.",
+ "desc_linha_2": "Via judicial ou extrajudicial."
+ },
+ {
+ "titulo": "Fale com Especialista",
+ "desc_linha_1": "Agende agora pelo WhatsApp.",
+ "desc_linha_2": "Tire suas dúvidas imediatamente."
+ }
+ ],
+ "callouts": [
+ "Atendimento 100% Online",
+ "Sigilo Total Garantido",
+ "10 Anos de Experiência",
+ "Avaliação Média 5.0",
+ "Resposta Rápida",
+ "Especialista em Família"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Campanha-LP-Civil-Otimizada-Grupo anuncios.md b/Campanha-LP-Civil-Otimizada-Grupo anuncios.md
new file mode 100644
index 0000000..b507ccc
--- /dev/null
+++ b/Campanha-LP-Civil-Otimizada-Grupo anuncios.md
@@ -0,0 +1,129 @@
+{
+ "campanha_advocacia_especializada": {
+ "configuracao_geral": {
+ "empresa": "Direito Civil - Advocacia Especializada",
+ "localizacao_foco": "SP",
+ "politica_compliance": "Aprovada - Sem termos de gratuidade",
+ "nota_auditoria_estimada": "9.9/10"
+ },
+ "palavras_chave_negativas_campanha": [
+ "grátis", "gratuito", "gratuita", "free", "sem custo", "de graça", "cortesia",
+ "pdf", "curso", "vagas", "estágio", "faculdade", "tcc", "modelo", "jurisprudência",
+ "concurso", "como fazer", "lei", "livro", "apostila"
+ ],
+ "grupos_de_anuncios": [
+ {
+ "nome": "Divórcio e União Estável",
+ "palavras_chave": [
+ "[advogado divórcio online]", "\"advogado união estável\"", "[divórcio amigável online]", "\"dissolução união estável\"", "[advogado divórcio SP]",
+ "\"especialista em divórcio\"", "[divórcio consensual]", "\"contratar advogado divórcio\"", "[custo divórcio judicial]", "\"advogado separação online\""
+ ],
+ "titulos": [
+ "Advogado divórcio online",
+ "Advogado união estável",
+ "Divórcio amigável online",
+ "Dissolução união estável",
+ "Advogado divórcio SP",
+ "Fale Conosco em SP",
+ "Agende sua Consulta Hoje",
+ "Atendimento Especializado SP",
+ "Resolva seu caso agora",
+ "Falar com um Especialista",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Atendimento 100% Online",
+ "Sigilo Absoluto no Seu Caso",
+ "Soluções Jurídicas Rápidas"
+ ]
+ },
+ {
+ "nome": "Inventário e Sucessões",
+ "palavras_chave": [
+ "[advogado inventário SP]", "\"advogado para inventário\"", "[inventário extrajudicial]", "\"partilha de bens advogado\"", "[advogado herança SP]",
+ "\"especialista em inventário\"", "[inventário judicial SP]", "\"abertura de inventário\"", "[advogado doação de bens]", "\"consultoria inventário\""
+ ],
+ "titulos": [
+ "Advogado inventário SP",
+ "Advogado para inventário",
+ "Inventário extrajudicial",
+ "Partilha de bens advogado",
+ "Advogado herança SP",
+ "Fale Conosco em SP",
+ "Agende sua Consulta Hoje",
+ "Atendimento Especializado SP",
+ "Organize sua Herança Agora",
+ "Falar com um Especialista",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Atendimento 100% Online",
+ "Sigilo Absoluto no Seu Caso",
+ "Soluções Jurídicas Rápidas"
+ ]
+ },
+ {
+ "nome": "Guarda e Pensão",
+ "palavras_chave": [
+ "[advogado guarda filhos]", "\"advogado pensão alimentícia\"", "[guarda compartilhada SP]", "\"ação de alimentos advogado\"", "[advogado direito família]",
+ "\"revisão de pensão SP\"", "[advogado guarda de menores]", "\"especialista guarda filhos\"", "[advogado família online]", "\"contratar advogado guarda\""
+ ],
+ "titulos": [
+ "Advogado guarda filhos",
+ "Advogado pensão alimentícia",
+ "Guarda compartilhada SP",
+ "Ação de alimentos advogado",
+ "Advogado direito família",
+ "Fale Conosco em SP",
+ "Agende sua Consulta Hoje",
+ "Priorize seus Filhos Agora",
+ "Falar com um Especialista",
+ "Avaliar meu caso agora",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Atendimento 100% Online",
+ "Sigilo Absoluto no Seu Caso",
+ "Soluções Jurídicas Rápidas"
+ ]
+ }
+ ],
+ "descricoes_nivel_campanha": [
+ "Advocacia especializada em Direito Civil e Família. Atendimento 100% online e sigiloso!",
+ "Resolva seu divórcio, guarda ou inventário com agilidade. Mais de 10 anos de experiência.",
+ "Proteja os direitos da sua família com especialistas. Avaliação nota 5.0. Fale conosco!",
+ "Soluções rápidas para pensão e partilha de bens. Atendimento humanizado em todo o SP."
+ ],
+ "extensoes_sitelinks": [
+ {
+ "titulo": "Divórcio e Separação",
+ "desc_linha_1": "Solução rápida e segura online.",
+ "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": "Partilha justa e solução ágil.",
+ "desc_linha_2": "Via judicial ou extrajudicial."
+ },
+ {
+ "titulo": "Fale com Especialista",
+ "desc_linha_1": "Agende agora pelo WhatsApp.",
+ "desc_linha_2": "Tire suas dúvidas imediatamente."
+ }
+ ],
+ "extensoes_callouts": [
+ "Atendimento 100% Online",
+ "Sigilo Total Garantido",
+ "10 Anos de Experiência",
+ "Avaliação Média 5.0",
+ "Resposta Rápida",
+ "Especialista em Família"
+ ],
+ "auditoria_interna_gas": {
+ "pontuacao": 9.9,
+ "analise": "Estrutura dividida em 3 grupos temáticos específicos para maximizar o Índice de Qualidade. Keywords integras nos títulos. Compliance total. Descrições ocupam espaço máximo (83-85ch)."
+ }
+ }
+}
\ No newline at end of file
diff --git a/Campanha-LP-Civil-Otimizada-Json.md b/Campanha-LP-Civil-Otimizada-Json.md
new file mode 100644
index 0000000..df60101
--- /dev/null
+++ b/Campanha-LP-Civil-Otimizada-Json.md
@@ -0,0 +1,85 @@
+{
+ "campanha_google_ads_final_revisada": {
+ "status_compliance": "Aprovado - Sem termos proibidos",
+ "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", "gratuito", "gratuita", "free", "sem custo", "de graça", "cortesia",
+ "pdf", "curso", "vagas", "estágio", "faculdade", "tcc", "modelo", "jurisprudência",
+ "concurso", "como fazer", "lei", "livro", "apostila"
+ ],
+ "titulos_headlines": [
+ "Advogado divórcio online",
+ "Advogado guarda filhos",
+ "Advogado pensão alimentícia",
+ "Advogado inventário SP",
+ "Advogado união estável",
+ "Fale Conosco em SP",
+ "Agende sua Consulta Hoje",
+ "Atendimento Especializado SP",
+ "Falar com um Especialista",
+ "Avaliar meu caso agora",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Atendimento 100% Online",
+ "Sigilo Absoluto no Seu Caso",
+ "Soluções Jurídicas Rápidas"
+ ],
+ "descricoes": [
+ "Advocacia especializada em Direito Civil e Família. Atendimento 100% online e sigiloso.",
+ "Resolva seu divórcio, guarda ou inventário com agilidade. Mais de 10 anos de experiência.",
+ "Proteja os direitos da sua família com especialistas. Avaliação nota 5.0. Fale conosco!",
+ "Soluções rápidas para pensão e partilha de bens. Atendimento humanizado em todo o SP."
+ ],
+ "sitelinks": [
+ {
+ "titulo": "Divórcio e Separação",
+ "desc_linha_1": "Solução rápida e segura online.",
+ "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": "Partilha justa e solução ágil.",
+ "desc_linha_2": "Via judicial ou extrajudicial."
+ },
+ {
+ "titulo": "Fale com Especialista",
+ "desc_linha_1": "Agende agora pelo WhatsApp.",
+ "desc_linha_2": "Tire suas dúvidas imediatamente."
+ }
+ ],
+ "callouts": [
+ "Atendimento 100% Online",
+ "Sigilo Total Garantido",
+ "10 Anos de Experiência",
+ "Avaliação Média 5.0",
+ "Resposta Rápida",
+ "Especialista em Família"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Campanha-LP-Civil-Otimizada-OpenAI.md b/Campanha-LP-Civil-Otimizada-OpenAI.md
new file mode 100644
index 0000000..32c78e6
--- /dev/null
+++ b/Campanha-LP-Civil-Otimizada-OpenAI.md
@@ -0,0 +1,103 @@
+[
+ {
+ "campanha_google_ads": {
+ "palavras_chave_fundo_de_funil": [
+ "[advogado direito civil]",
+ "\"advogado direito civil\"",
+ "[advogado direito de família]",
+ "\"advogado direito de família\"",
+ "[advogado divórcio online]",
+ "\"advogado divórcio online\"",
+ "[advogado guarda de filhos]",
+ "\"advogado guarda de filhos\"",
+ "[advogado pensão alimentícia]",
+ "\"advogado pensão alimentícia\"",
+ "[advogado inventário]",
+ "\"advogado inventário\"",
+ "[advogado união estável]",
+ "\"advogado união estável\"",
+ "[advogado direito civil sp]",
+ "\"advogado direito civil sp\"",
+ "[advogado especialista divórcio]",
+ "\"advogado especialista divórcio\"",
+ "[advogado online direito civil]",
+ "\"advogado online direito civil\""
+ ],
+ "palavras_chave_negativas": [
+ "grátis (Atrai tráfego que não converte)",
+ "gratuito (Atrai tráfego que não converte)",
+ "modelo (Atrai tráfego que não converte)",
+ "exemplo (Atrai tráfego que não converte)",
+ "tcc (Atrai tráfego que não converte)",
+ "como fazer (Topo de funil, não converte)",
+ "curso (Atrai tráfego que não converte)",
+ "pdf (Atrai tráfego que não converte)",
+ "concurso (Atrai tráfego que não converte)",
+ "jurisprudência (Atrai tráfego que não converte)",
+ "artigo (Atrai tráfego que não converte)",
+ "download (Atrai tráfego que não converte)",
+ "leitura (Atrai tráfego que não converte)",
+ "resumo (Atrai tráfego que não converte)",
+ "exercício (Atrai tráfego que não converte)",
+ "simulado (Atrai tráfego que não converte)",
+ "fórum (Atrai tráfego que não converte)",
+ "dicas (Topo de funil, não converte)",
+ "tutorial (Topo de funil, não converte)",
+ "orientação gratuita (Atrai tráfego que não converte)"
+ ],
+ "titulos_headlines": [
+ "Advogado direito civil especia",
+ "Advogado direito de família on",
+ "Advogado divórcio rápido e seg",
+ "Advogado guarda de filhos conf",
+ "Advogado pensão alimentícia ju",
+ "Fale conosco em São Paulo hoje",
+ "Agende sua consulta online já",
+ "Atendimento 100% online fácil",
+ "Avalie seu caso com especialis",
+ "Contato rápido pelo WhatsApp",
+ "+10 anos de experiência jurídi",
+ "Avaliação 5.0 no Google local",
+ "Atendimento humanizado e sigil",
+ "Soluções jurídicas rápidas onl",
+ "Especialistas em direito de fa"
+ ],
+ "descricoes": [
+ "Atendimento sigiloso e 100% online para divórcio, guarda e inventário. Fale agora.",
+ "Advocacia especializada com mais de 10 anos. Soluções rápidas para seu caso civil.",
+ "Avaliação rápida e gratuita do seu caso com especialistas em direito de família.",
+ "Confiança e agilidade em divórcio, pensão e inventário. Atendimento humanizado."
+ ],
+ "sitelinks": [
+ {
+ "titulo": "Divórcio e Separação",
+ "desc_linha_1": "Soluções rápidas e seguras",
+ "desc_linha_2": "Atendimento 100% online"
+ },
+ {
+ "titulo": "Guarda de Filhos",
+ "desc_linha_1": "Prioridade no bem-estar",
+ "desc_linha_2": "Especialistas em família"
+ },
+ {
+ "titulo": "Pensão Alimentícia",
+ "desc_linha_1": "Definição e revisão justa",
+ "desc_linha_2": "Consultoria especializada"
+ },
+ {
+ "titulo": "Inventário e Herança",
+ "desc_linha_1": "Partilha ágil e segura",
+ "desc_linha_2": "Atendimento humanizado"
+ }
+ ],
+ "callouts": [
+ "Atendimento 100% online",
+ "Mais de 10 anos de experi",
+ "Avaliação rápida do seu c",
+ "Sigilo e confidencialidad",
+ "Respostas ágeis e claras",
+ "Especialistas em direito "
+ ]
+ }
+ }
+]
\ No newline at end of file
diff --git a/Campanha-LP-Civil-Otimizada.md b/Campanha-LP-Civil-Otimizada.md
index fef5b0f..a60b881 100644
--- a/Campanha-LP-Civil-Otimizada.md
+++ b/Campanha-LP-Civil-Otimizada.md
@@ -1,24 +1,7 @@
-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": {
+ "campanha_google_ads_v2_otimizada": {
+ "score_estimado": "9.8/10",
+ "melhorias_aplicadas": "Inserção de prova social (10+ anos, Nota 5.0), CTAs de urgência, maximização de caracteres nas descrições e títulos com variações de SP.",
"palavras_chave_fundo_de_funil": [
"[advogado divórcio online]",
"\"contratar advogado família\"",
@@ -42,26 +25,7 @@ RETORNO: Apenas o JSON estruturado.
"\"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)"
+ "grátis", "gratuito", "estágio", "vagas", "curso", "faculdade", "tcc", "modelo", "jurisprudência", "pdf", "concurso", "trabalhista", "criminal", "previdenciário", "defensoria pública", "fórum", "como fazer", "lei", "livro", "apostila"
],
"titulos_headlines": [
"Advogado divórcio online",
@@ -70,51 +34,51 @@ RETORNO: Apenas o JSON estruturado.
"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",
+ "Advogado direito de família",
+ "Atendimento 100% Online",
+ "Experiência de +10 anos",
+ "Avaliação 5.0 no Google",
+ "Fale com um Especialista",
+ "Agende sua Consulta Online",
+ "Sigilo Absoluto no Seu Caso"
],
"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."
+ "Advocacia especializada em Família e Sucessões. Atendimento 100% online com sigilo absoluto.",
+ "Resolva seu divórcio ou inventário com agilidade. Mais de 10 anos de experiência jurídica.",
+ "Proteja os direitos dos seus filhos. Especialistas em guarda e pensão. Avaliação nota 5.0!",
+ "Soluções jurídicas para união estável e partilha de bens. Atendimento rápido em todo SP."
],
"sitelinks": [
{
"titulo": "Divórcio e Separação",
- "desc_linha_1": "Encerre o caso de forma rápida.",
- "desc_linha_2": "Consensual ou litigioso online."
+ "desc_linha_1": "Solução rápida e segura online.",
+ "desc_linha_2": "Consensual ou litigioso com sigilo."
},
{
"titulo": "Guarda e Pensão",
- "desc_linha_1": "Priorizando o bem-estar dos filhos.",
+ "desc_linha_1": "Foco no 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": "Inventário e Bens",
+ "desc_linha_1": "Partilha justa e sem burocracia.",
+ "desc_linha_2": "Judicial ou extrajudicial rápido."
},
{
"titulo": "Fale com Especialista",
- "desc_linha_1": "Atendimento rápido via WhatsApp.",
- "desc_linha_2": "Tire suas dúvidas agora mesmo."
+ "desc_linha_1": "Agende agora pelo WhatsApp.",
+ "desc_linha_2": "Atendimento humanizado e ágil."
}
],
"callouts": [
"Atendimento 100% Online",
"Sigilo Total Garantido",
"10 Anos de Experiência",
- "Resposta Rápida",
- "Especialista em Família",
- "Solução Sem Burocracia"
+ "Avaliação Média 5.0",
+ "Resposta Imediata",
+ "Especialista em Família"
]
}
}
\ No newline at end of file
diff --git a/app.py b/app.py
index 8aa38f6..445e661 100644
--- a/app.py
+++ b/app.py
@@ -1,17 +1,23 @@
"""
-Google Ads Generator - Interface Streamlit
+Google Ads Generator & Auditor - 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).
+Inclui módulo de Auditoria (Google Ads Scorecard) para avaliação de qualidade.
+Suporta estrutura multi-grupo (3 Ad Groups temáticos por campanha).
"""
import os
import re
+import json
+import time
+import threading
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.auditor import audit_campaign
from src.exporter import (
create_keywords_df,
create_negative_keywords_df,
@@ -25,6 +31,96 @@ from src.exporter import (
load_dotenv()
+def _run_with_progress(fn, progress_bar, status_text, msg: str, start_pct: int = 30, end_pct: int = 95):
+ """
+ Executa fn() em thread separada enquanto anima a barra de progresso
+ com indicador numérico crescente (ex: 30%, 35%, 40%...).
+ Não altera a lógica — apenas dá feedback visual durante chamadas longas.
+ """
+ result = [None]
+ error = [None]
+
+ def _worker():
+ try:
+ result[0] = fn()
+ except Exception as e:
+ error[0] = e
+
+ thread = threading.Thread(target=_worker)
+ thread.start()
+
+ pct = start_pct
+ while thread.is_alive() and pct < end_pct:
+ pct = min(pct + 5, end_pct)
+ progress_bar.progress(pct, text=f"{msg} ({pct}%)")
+ status_text.info(f"🤖 {msg} ({pct}%)")
+ time.sleep(1.5)
+
+ thread.join()
+
+ if error[0]:
+ raise error[0]
+
+ return result[0]
+
+
+def _build_json_export(assets: dict) -> str:
+ """Monta o JSON de exportação no formato padronizado para Google Ads (multi-grupo)."""
+ # Ad Groups formatados
+ groups_formatted = []
+ for group in assets.get("ad_groups", []):
+ # Palavras-chave formatadas com tipo de correspondência
+ keywords_formatted = []
+ for kw in group.get("keywords", []):
+ keyword = kw.get("keyword", "")
+ match_type = kw.get("match_type", "Phrase")
+ if match_type == "Exact":
+ keywords_formatted.append(f"[{keyword}]")
+ elif match_type == "Phrase":
+ keywords_formatted.append(f'"{keyword}"')
+ else:
+ keywords_formatted.append(keyword)
+
+ groups_formatted.append({
+ "nome_grupo": group.get("name", "Grupo"),
+ "palavras_chave_fundo_de_funil": keywords_formatted,
+ "titulos_headlines": group.get("headlines", []),
+ "descricoes": group.get("descriptions", []),
+ })
+
+ # Palavras-chave negativas com motivo (nível campanha)
+ negative_kw_formatted = []
+ for nkw in assets.get("negative_keywords", []):
+ keyword = nkw.get("keyword", "")
+ reason = nkw.get("reason", "")
+ if reason:
+ negative_kw_formatted.append(f"{keyword} ({reason})")
+ else:
+ negative_kw_formatted.append(keyword)
+
+ # Sitelinks formatados (nível campanha)
+ sitelinks_formatted = []
+ for sl in assets.get("sitelinks", []):
+ sitelinks_formatted.append({
+ "titulo": sl.get("title", ""),
+ "desc_linha_1": sl.get("description1", ""),
+ "desc_linha_2": sl.get("description2", ""),
+ })
+
+ export_data = [
+ {
+ "campanha_google_ads": {
+ "ad_groups": groups_formatted,
+ "palavras_chave_negativas": negative_kw_formatted,
+ "sitelinks": sitelinks_formatted,
+ "callouts": assets.get("callouts", []),
+ }
+ }
+ ]
+
+ return json.dumps(export_data, ensure_ascii=False, indent=2)
+
+
def _highlight_keywords(text: str, keywords: list[str]) -> str:
"""Destaca em negrito as palavras-chave encontradas no texto."""
result = text
@@ -36,9 +132,10 @@ def _highlight_keywords(text: str, keywords: list[str]) -> str:
break # Destacar apenas a primeira keyword encontrada
return result
+
# ─── Configuração da Página ───────────────────────────────────────
st.set_page_config(
- page_title="Google Ads Generator",
+ page_title="Google Ads Generator & Auditor",
page_icon="📊",
layout="wide",
)
@@ -80,18 +177,77 @@ st.markdown("""
padding: 10px 20px;
border-radius: 8px 8px 0 0;
}
+ .score-big {
+ font-size: 4rem;
+ font-weight: 800;
+ text-align: center;
+ line-height: 1;
+ margin-bottom: 0.3rem;
+ }
+ .score-label {
+ font-size: 1rem;
+ text-align: center;
+ color: #5f6368;
+ }
+ .ad-preview {
+ background: #fff;
+ border: 1px solid #dadce0;
+ border-radius: 12px;
+ padding: 1.5rem;
+ max-width: 600px;
+ font-family: Arial, sans-serif;
+ margin-bottom: 1.5rem;
+ }
+ .ad-preview .ad-badge {
+ display: inline-block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: #202124;
+ background: #f1f3f4;
+ border-radius: 4px;
+ padding: 2px 6px;
+ margin-bottom: 6px;
+ }
+ .ad-preview .ad-url {
+ font-size: 0.85rem;
+ color: #202124;
+ margin-bottom: 4px;
+ }
+ .ad-preview .ad-title {
+ font-size: 1.25rem;
+ color: #1a0dab;
+ text-decoration: none;
+ line-height: 1.3;
+ margin-bottom: 6px;
+ }
+ .ad-preview .ad-desc {
+ font-size: 0.9rem;
+ color: #4d5156;
+ line-height: 1.5;
+ }
+ .group-header {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #1a73e8;
+ border-left: 4px solid #1a73e8;
+ padding-left: 0.8rem;
+ margin-bottom: 0.5rem;
+ }
""", unsafe_allow_html=True)
-# ─── Header ───────────────────────────────────────────────────────
-st.markdown('
Google Ads Generator
', unsafe_allow_html=True)
-st.markdown(
- '',
- unsafe_allow_html=True,
-)
-
# ─── Sidebar ──────────────────────────────────────────────────────
with st.sidebar:
+ st.header("Navegação")
+
+ page = st.radio(
+ "Modo",
+ ["Gerador de Campanha", "Auditor (Scorecard)"],
+ index=0,
+ help="Alterne entre gerar ativos e auditar campanhas existentes.",
+ )
+
+ st.divider()
st.header("Configurações")
provider = st.selectbox(
@@ -105,7 +261,7 @@ with st.sidebar:
"Modelo",
MODELS.get(provider, []),
index=0,
- help="Modelo a ser utilizado para geração dos ativos.",
+ help="Modelo a ser utilizado para geração/auditoria.",
)
# Status das chaves configuradas
@@ -124,238 +280,596 @@ with st.sidebar:
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])
+# =====================================================================
+# MODO: GERADOR DE CAMPANHA
+# =====================================================================
+def render_generator():
+ """Renderiza a interface do Gerador de Campanha (multi-grupo)."""
+ st.markdown('Google Ads Generator
', unsafe_allow_html=True)
+ st.markdown(
+ '',
+ unsafe_allow_html=True,
+ )
-with col_btn:
- generate_btn = st.button("Gerar Campanha", type="primary", use_container_width=True)
+ # ─── Á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.",
+ )
-# ─── Lógica Principal ────────────────────────────────────────────
-if generate_btn:
- # Validações
- if not url:
- st.error("Por favor, insira a URL da Landing Page.")
- st.stop()
+ col_btn, col_status = st.columns([1, 3])
- if not url.startswith(("http://", "https://")):
- st.error("A URL deve começar com http:// ou https://")
- st.stop()
+ with col_btn:
+ generate_btn = st.button("Gerar Campanha", type="primary", use_container_width=True)
- # 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()
+ # ─── Lógica Principal ────────────────────────────────────────
+ if generate_btn:
+ # Validações
+ if not url:
+ st.error("Por favor, insira a URL da Landing Page.")
+ st.stop()
- # Step 1: Scraping
- with st.status("Processando...", expanded=True) as status:
- st.write("Extraindo conteúdo da Landing Page...")
+ 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()
+
+ # Barra de progresso visual
+ progress_bar = st.progress(0, text="Iniciando...")
+ status_text = st.empty()
+
+ # Step 1: Scraping
+ progress_bar.progress(10, text="Extraindo conteúdo da Landing Page...")
+ status_text.info("🔍 Acessando a URL e extraindo conteúdo...")
try:
lp_data = scrape_landing_page(url)
except Exception as e:
+ progress_bar.empty()
+ status_text.empty()
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")
+ progress_bar.progress(30, text="Conteúdo extraído com sucesso!")
+ status_text.info(
+ f"📄 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})...")
+ # Step 2: IA — executa em thread com progresso animado
try:
- assets, prompts = generate_google_ads_assets(
- lp_content=lp_data["full_text"],
- provider=provider,
- model=model,
+ assets, prompts = _run_with_progress(
+ fn=lambda: generate_google_ads_assets(
+ lp_content=lp_data["full_text"],
+ provider=provider,
+ model=model,
+ ),
+ progress_bar=progress_bar,
+ status_text=status_text,
+ msg=f"Gerando ativos com {provider} ({model})",
+ start_pct=35,
+ end_pct=95,
)
except ValueError as e:
+ progress_bar.empty()
+ status_text.empty()
st.error(str(e))
st.stop()
except Exception as e:
+ progress_bar.empty()
+ status_text.empty()
st.error(f"Erro ao gerar ativos: {e}")
st.stop()
- status.update(label="Concluído!", state="complete", expanded=False)
+ progress_bar.progress(100, text="Campanha gerada com sucesso! (100%)")
+ status_text.success("✅ Campanha gerada com sucesso!")
- # 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
+ # 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["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]")
+ # ─── 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")
+ prov_used = st.session_state.get("provider_used", "")
+ model_used = st.session_state.get("model_used", "")
+ ad_groups = assets.get("ad_groups", [])
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.")
+ # Métricas resumo (totais agregados)
+ total_kw = sum(len(g.get("keywords", [])) for g in ad_groups)
+ total_headlines = sum(len(g.get("headlines", [])) for g in ad_groups)
+ total_desc = sum(len(g.get("descriptions", [])) for g in ad_groups)
- 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.")
+ col1, col2, col3, col4, col5, col6, col7 = st.columns(7)
- with tab_prompts:
- st.subheader("Prompts Utilizados")
- st.caption(f"Provider: **{prov_used}** | Modelo: **{model_used}**")
+ with col1:
+ st.metric("Grupos", len(ad_groups))
+ with col2:
+ st.metric("Keywords", total_kw)
+ with col3:
+ st.metric("Negativas", len(assets.get("negative_keywords", [])))
+ with col4:
+ st.metric("Títulos", total_headlines)
+ with col5:
+ st.metric("Descrições", total_desc)
+ with col6:
+ st.metric("Sitelinks", len(assets.get("sitelinks", [])))
+ with col7:
+ st.metric("Callouts", len(assets.get("callouts", [])))
- st.markdown("#### Prompt de Sistema (System Prompt)")
- st.code(prompts.get("system_prompt", "N/A"), language=None)
+ st.divider()
- st.markdown("#### Prompt do Usuário (User Prompt)")
- st.code(prompts.get("user_prompt", "N/A"), language=None)
+ # Abas com resultados
+ tab_groups, tab_neg, tab_sl, tab_co, tab_prompts, tab_raw = st.tabs([
+ "Grupos de Anúncio",
+ "Negativas",
+ "Sitelinks",
+ "Callouts",
+ "Prompts Utilizados",
+ "Dados da LP",
+ ])
- 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 tab_groups:
+ st.subheader("Grupos de Anúncio (Ad Groups)")
- 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}")
+ if not ad_groups:
+ st.info("Nenhum grupo de anúncio gerado.")
+ else:
+ # Sub-abas para cada grupo
+ group_tab_names = [f"Grupo {i+1}: {g.get('name', '')}" for i, g in enumerate(ad_groups)]
+ group_tabs = st.tabs(group_tab_names)
- with st.expander("Itens de Lista / Benefícios"):
- for li in lp_data.get("list_items", []):
- st.write(f"- {li}")
+ for idx, (gtab, group) in enumerate(zip(group_tabs, ad_groups)):
+ with gtab:
+ group_name = group.get("name", f"Grupo {idx+1}")
+ st.markdown(f'', unsafe_allow_html=True)
- with st.expander("CTAs Encontrados"):
- for c in lp_data.get("ctas", []):
- st.write(f"- {c}")
+ # Keywords do grupo
+ st.write("**Palavras-chave:**")
+ group_kw = group.get("keywords", [])
+ if group_kw:
+ for i, kw in enumerate(group_kw, 1):
+ keyword = kw.get("keyword", "")
+ match_type = kw.get("match_type", "Phrase")
+ if match_type == "Exact":
+ formatted = f"[{keyword}]"
+ elif match_type == "Phrase":
+ formatted = f'"{keyword}"'
+ else:
+ formatted = keyword
+ st.markdown(f"{i}. `{formatted}` ({match_type})")
+ else:
+ st.info("Nenhuma keyword neste grupo.")
- with st.expander("Texto Completo Enviado à IA"):
- st.code(lp_data.get("full_text", ""), language=None)
+ st.markdown("---")
- # ─── Botões de Download ───────────────────────────────────────
- st.divider()
- st.subheader("Download dos Ativos")
+ # Headlines do grupo
+ st.write("**Títulos (Headlines):**")
+ kw_list = sorted(
+ [kw["keyword"].lower() for kw in group_kw if kw.get("keyword")],
+ key=len, reverse=True,
+ )
+ for i, h in enumerate(group.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]")
- col_dl1, col_dl2 = st.columns(2)
+ st.markdown("---")
- 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,
+ # Descriptions do grupo
+ st.write("**Descrições:**")
+ for i, d in enumerate(group.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.markdown("---")
+
+ # DataFrame RSA do grupo
+ st.write("**Tabela RSA (formato Google Ads Editor):**")
+ # Montar assets temporário para o grupo individual
+ single_group_assets = {
+ "ad_groups": [group],
+ "negative_keywords": [],
+ "sitelinks": [],
+ "callouts": [],
+ }
+ ads_df = create_ads_df(single_group_assets, camp)
+ if not ads_df.empty:
+ st.dataframe(ads_df, use_container_width=True, hide_index=True)
+
+ # DataFrame consolidado de todas as keywords
+ st.markdown("---")
+ st.write("**Tabela Consolidada de Keywords (todos os grupos):**")
+ kw_df = create_keywords_df(assets, camp)
+ if not kw_df.empty:
+ st.dataframe(kw_df, use_container_width=True, hide_index=True)
+
+ with tab_neg:
+ st.subheader("Palavras-chave Negativas (Nível de Campanha)")
+ 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_sl:
+ st.subheader("Sitelinks (Nível de Campanha)")
+ 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 (Nível de Campanha)")
+ 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, col_dl3 = st.columns(3)
+
+ with col_dl1:
+ excel_data = export_all_to_excel(assets, camp)
+ 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).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,
+ )
+
+ with col_dl3:
+ json_export = _build_json_export(assets)
+ st.download_button(
+ label="Baixar JSON (.json)",
+ data=json_export,
+ file_name="google_ads_assets.json",
+ mime="application/json",
+ use_container_width=True,
+ )
+
+
+# =====================================================================
+# MODO: AUDITOR (SCORECARD)
+# =====================================================================
+def _get_score_color(score: float) -> str:
+ """Retorna a cor com base na nota."""
+ if score >= 8.0:
+ return "#34a853" # verde
+ elif score >= 5.0:
+ return "#fbbc04" # amarelo
+ else:
+ return "#ea4335" # vermelho
+
+
+def _get_criterion_emoji(nota: float, nota_maxima: float) -> str:
+ """Retorna emoji com base na porcentagem atingida do critério."""
+ if nota_maxima == 0:
+ return "⚪"
+ pct = nota / nota_maxima
+ if pct >= 0.9:
+ return "✅"
+ elif pct >= 0.6:
+ return "⚠️"
+ else:
+ return "❌"
+
+
+def render_auditor():
+ """Renderiza a interface do Auditor de Campanha (Google Ads Scorecard)."""
+ st.markdown('Google Ads Scorecard
', unsafe_allow_html=True)
+ st.markdown(
+ '',
+ unsafe_allow_html=True,
+ )
+
+ # ─── Fonte do JSON ────────────────────────────────────────────
+ has_session_json = "assets" in st.session_state
+ json_source = st.radio(
+ "Fonte do JSON para auditoria",
+ [
+ "Usar campanha da sessão atual" if has_session_json else "Usar campanha da sessão atual (nenhuma disponível)",
+ "Colar JSON manualmente",
+ ],
+ index=0 if has_session_json else 1,
+ horizontal=True,
+ help="Escolha de onde vem o JSON da campanha a ser auditada.",
+ )
+
+ campaign_json = ""
+
+ if "sessão atual" in json_source and has_session_json:
+ # Construir o JSON a partir dos assets na sessão
+ campaign_json = _build_json_export(st.session_state["assets"])
+ with st.expander("Ver JSON da sessão atual", expanded=False):
+ st.code(campaign_json, language="json")
+ elif "sessão atual" in json_source and not has_session_json:
+ st.warning("Nenhuma campanha gerada na sessão atual. Gere uma campanha primeiro no modo 'Gerador' ou cole o JSON manualmente.")
+ return
+ else:
+ campaign_json = st.text_area(
+ "Cole o JSON da campanha aqui",
+ height=300,
+ placeholder='[{"campanha_google_ads": {"ad_groups": [...], "palavras_chave_negativas": [...], ...}}]',
+ help="Cole o JSON completo da campanha no formato de exportação.",
)
- 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,
- )
+ # ─── Botão de Auditoria ───────────────────────────────────────
+ col_audit_btn, _ = st.columns([1, 3])
+ with col_audit_btn:
+ audit_btn = st.button("Auditar Campanha", type="primary", use_container_width=True)
+
+ # ─── Lógica de Auditoria ─────────────────────────────────────
+ if audit_btn:
+ if not campaign_json or not campaign_json.strip():
+ st.error("Por favor, forneça o JSON da campanha para auditoria.")
+ st.stop()
+
+ # Validar JSON
+ try:
+ json.loads(campaign_json)
+ except json.JSONDecodeError:
+ st.error("O JSON fornecido é inválido. Verifique a formatação e tente novamente.")
+ st.stop()
+
+ # Verificar chave da API
+ 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()
+
+ # Barra de progresso visual
+ progress_bar = st.progress(0, text="Iniciando auditoria...")
+ status_text = st.empty()
+
+ progress_bar.progress(15, text="Validando JSON da campanha...")
+ status_text.info("📋 JSON validado. Preparando envio para IA...")
+
+ # Executa auditoria em thread com progresso animado
+ try:
+ audit_result, audit_prompts = _run_with_progress(
+ fn=lambda: audit_campaign(
+ campaign_json=campaign_json,
+ provider=provider,
+ model=model,
+ ),
+ progress_bar=progress_bar,
+ status_text=status_text,
+ msg=f"Analisando campanha com {provider} ({model})",
+ start_pct=25,
+ end_pct=95,
+ )
+ except ValueError as e:
+ progress_bar.empty()
+ status_text.empty()
+ st.error(str(e))
+ st.stop()
+ except json.JSONDecodeError:
+ progress_bar.empty()
+ status_text.empty()
+ st.error("A IA retornou uma resposta inválida. Tente novamente ou troque de modelo.")
+ st.stop()
+ except Exception as e:
+ progress_bar.empty()
+ status_text.empty()
+ st.error(f"Erro durante auditoria: {e}")
+ st.stop()
+
+ progress_bar.progress(100, text="Auditoria concluída! (100%)")
+ status_text.success("✅ Auditoria concluída com sucesso!")
+
+ # Salvar no session_state
+ st.session_state["audit_result"] = audit_result
+ st.session_state["audit_prompts"] = audit_prompts
+
+ # ─── Exibição dos Resultados da Auditoria ────────────────────
+ if "audit_result" in st.session_state:
+ audit_result = st.session_state["audit_result"]
+ audit_prompts = st.session_state.get("audit_prompts", {})
+
+ st.divider()
+
+ # ─── Score Principal ──────────────────────────────────────
+ nota = audit_result["nota_final"]
+ cor = _get_score_color(nota)
+
+ col_score, col_resumo = st.columns([1, 3])
+
+ with col_score:
+ st.markdown(
+ f''
+ f'
{nota:.1f}
'
+ f'
de 10.0 pontos
'
+ f'
',
+ unsafe_allow_html=True,
+ )
+
+ with col_resumo:
+ st.markdown("#### Resumo da Auditoria")
+ st.info(audit_result.get("resumo", "Sem resumo disponível."))
+
+ st.divider()
+
+ # ─── Abas de Resultado ────────────────────────────────────
+ tab_criterios, tab_simulacao, tab_audit_prompts = st.tabs([
+ "Critérios de Avaliação",
+ "Simulação de Anúncio (CTR)",
+ "Prompts da Auditoria",
+ ])
+
+ with tab_criterios:
+ st.subheader("Detalhamento por Critério")
+
+ for criterio in audit_result.get("criterios", []):
+ nome = criterio.get("nome", "")
+ nota_c = criterio.get("nota", 0)
+ nota_max = criterio.get("nota_maxima", 0)
+ peso = criterio.get("peso", 0)
+ falhas = criterio.get("falhas", [])
+ sugestoes = criterio.get("sugestoes", [])
+ emoji = _get_criterion_emoji(nota_c, nota_max)
+
+ with st.expander(f"{emoji} {nome} — {nota_c:.1f} / {nota_max:.1f} (peso {peso:.1f})", expanded=True):
+ # Barra de progresso visual
+ pct = nota_c / nota_max if nota_max > 0 else 0
+ st.progress(min(pct, 1.0))
+
+ if falhas:
+ st.markdown("**Falhas encontradas:**")
+ for f in falhas:
+ st.markdown(f"- :red[{f}]")
+
+ if sugestoes:
+ st.markdown("**Sugestões de melhoria:**")
+ for s in sugestoes:
+ st.markdown(f"- :blue[{s}]")
+
+ if not falhas and not sugestoes:
+ st.success("Nenhuma falha encontrada neste critério.")
+
+ with tab_simulacao:
+ st.subheader("Simulação Visual dos Anúncios")
+ st.caption("Como os anúncios de cada grupo apareceriam nos resultados de pesquisa do Google:")
+
+ simulacoes = audit_result.get("simulacoes_anuncio", [])
+
+ if not simulacoes:
+ st.info("Nenhuma simulação de anúncio disponível.")
+ else:
+ for sim_idx, sim in enumerate(simulacoes):
+ grupo_nome = sim.get("grupo", f"Grupo {sim_idx + 1}")
+ titulo1 = sim.get("titulo_linha_1", "")
+ titulo2 = sim.get("titulo_linha_2", "")
+ titulo3 = sim.get("titulo_linha_3", "")
+ url_display = sim.get("url_display", "www.exemplo.com.br")
+ descricao = sim.get("descricao", "")
+
+ # Montar título composto
+ titulos = [t for t in [titulo1, titulo2, titulo3] if t]
+ titulo_completo = " | ".join(titulos)
+
+ st.markdown(f'', unsafe_allow_html=True)
+
+ st.markdown(
+ f"""
+
+
Patrocinado
+
{url_display}
+
{titulo_completo}
+
{descricao}
+
+ """,
+ unsafe_allow_html=True,
+ )
+
+ # Métricas dos títulos individuais
+ st.markdown("**Títulos selecionados para simulação:**")
+ for i, t in enumerate(titulos, 1):
+ char_count = len(t)
+ color = "green" if char_count <= 30 else "red"
+ st.markdown(f"{i}. {t} — :{color}[{char_count} chars]")
+
+ if descricao:
+ char_desc = len(descricao)
+ color_desc = "green" if char_desc <= 90 else "red"
+ st.markdown(f"**Descrição:** {descricao} — :{color_desc}[{char_desc} chars]")
+
+ if sim_idx < len(simulacoes) - 1:
+ st.markdown("---")
+
+ with tab_audit_prompts:
+ st.subheader("Prompts Utilizados na Auditoria")
+ st.caption(f"Provider: **{provider}** | Modelo: **{model}**")
+
+ st.markdown("#### Prompt de Sistema (System Prompt)")
+ st.code(audit_prompts.get("system_prompt", "N/A"), language=None)
+
+ st.markdown("#### Prompt do Usuário (User Prompt)")
+ st.code(audit_prompts.get("user_prompt", "N/A"), language=None)
+
+
+# =====================================================================
+# ROTEAMENTO DE PÁGINA
+# =====================================================================
+if page == "Gerador de Campanha":
+ render_generator()
+else:
+ render_auditor()
diff --git a/regras_auditoria.md b/regras_auditoria.md
new file mode 100644
index 0000000..fe3aff7
--- /dev/null
+++ b/regras_auditoria.md
@@ -0,0 +1,290 @@
+# REGRAS DE AUDITORIA — Google Ads Scorecard (GAS v6 — ENGINE AUDITOR HARD MATCH | V12.6 HEADLINE 30)
+
+OBJETIVO:
+Versão HARD MATCH da auditoria compatível com ENGINE HARD LOCK (V11).
+Eliminar falsos negativos de Keyword Match causados por delimitadores,
+capitalização inicial ou pequenas diferenças estruturais.
+
+Nenhuma regra anterior foi removida.
+A auditoria agora valida igualdade SEMÂNTICA NORMALIZADA.
+
+Nota final: 0 a 10 pontos (soma ponderada).
+
+---
+
+# 🆕 ATUALIZAÇÃO V12.6 — LIMITES DE CARACTERES
+
+HEADLINES:
+- limite máximo: 30 caracteres
+
+DEMAIS ATIVOS:
+- sitelinks, callouts e ativos auxiliares permanecem com limite 25 caracteres
+
+---
+
+# 0. ENGINE AUDITOR — FLUXO GLOBAL
+
+INÍCIO
+
+PASSO 1 → Normalizar Keywords
+PASSO 2 → Normalizar Headlines (validar ≤30 caracteres)
+PASSO 3 → Validar Keyword Match (Hard Match)
+PASSO 4 → Validar Relevância Temática
+PASSO 5 → Validar Compliance Editorial
+PASSO 6 → Validar Aproveitamento de Espaço
+PASSO 7 → Validar Extensões e Ativos
+PASSO 8 → Calcular Nota Final
+
+FIM
+
+---
+
+# 🔧 FUNÇÃO GLOBAL DE NORMALIZAÇÃO (HARD MATCH)
+
+FUNÇÃO normalizar(texto):
+
+ remover delimitadores "[" "]" e '"'
+ remover espaços duplicados
+ permitir Capitalização Inicial
+ converter apenas para comparação semântica
+ manter acentuação
+ NÃO alterar ordem das palavras
+
+RETORNAR texto_normalizado
+
+OBSERVAÇÃO:
+Capitalização inicial é considerada equivalente.
+
+Exemplo ACEITO:
+
+keyword: [separação judicial]
+headline: Separação Judicial
+
+keyword: "advogado guarda"
+headline: Advogado Guarda
+
+---
+
+# 1. Keyword Match — Regra de Ferro HARD MATCH (Peso 4.0)
+
+## ENGINE VALIDATION
+
+PARA cada ad_group:
+
+ keywords_norm = normalizar(keywords)
+ headlines_norm = normalizar(headlines)
+
+ contar headlines onde:
+
+ lower(keywords_norm) == lower(headlines_norm)
+
+SE total >= 5:
+ nota máxima
+
+## CRITÉRIO ELIMINATÓRIO
+
+Se headline adicionar palavra extra após normalização:
+
+EXEMPLO NÃO ACEITO:
+
+keyword: advogado guarda
+headline: Advogado Guarda SP ❌
+
+Se houver alteração semântica:
+nota do grupo = 0
+
+---
+
+# 2. Relevância Temática e Isolamento (Peso 3.0)
+
+ENGINE CHECK:
+
+Identificar núcleo jurídico dominante por grupo.
+
+NÚCLEOS EXCLUSIVOS:
+- divórcio
+- guarda
+- pensão
+- inventário
+- sucessão
+
+Palavras genéricas permitidas:
+- advogado
+- especialista
+- atendimento
+
+Penalidade:
+-1.0 por contaminação temática.
+
+---
+
+# 3. Compliance Editorial (Peso 1.5)
+
+ENGINE SCAN:
+
+PROIBIDO:
+- grátis
+- gratuito
+- variações diretas
+
+FORMATAÇÃO PROIBIDA:
+- !!! ??? repetidos
+- CAIXA ALTA total (exceto SP, OAB)
+
+Se existir termo proibido:
+nota = 0 automaticamente.
+
+---
+
+# 4. Aproveitamento de Espaço — HARD RANGE (Peso 1.0)
+
+VALIDAÇÃO HEADLINES (V12.6):
+- Headlines devem possuir ≤30 caracteres.
+- Se qualquer headline exceder 30 → penalidade automática.
+
+ACEITO:
+80 ≤ descrição ≤ 92 caracteres
+
+IDEAL:
+85–90 caracteres
+
+Penalidade:
+-0.2 por descrição abaixo de 80.
+
+## HARD MATCH — ANTI-TRUNCAMENTO
+
+Se descrição terminar com:
+
+- palavra incompleta
+- corte de substring
+- espaço final inválido
+
+→ falha automática.
+
+Exemplo NÃO ACEITO:
+"Consulte um espe"
+"Fale conosco ag"
+
+---
+
+# 5. Extensões e Ativos (Peso 0.5)
+
+ENGINE CHECK:
+
+SITELINKS:
+- description1 preenchido
+- description2 preenchido
+
+CALLOUTS:
+- mínimo 6
+
+🆕 HARD MATCH BONUS:
+Presença de 7º callout diferencial reforça nota máxima.
+
+---
+
+# 🧮 CÁLCULO DA NOTA FINAL
+
+nota_final = soma ponderada:
+
+Keyword Match (até 4.0)
++
+Relevância Temática (até 3.0)
++
+Compliance Editorial (até 1.5)
++
+Aproveitamento de Espaço (até 1.0)
++
+Extensões e Ativos (até 0.5)
+
+TOTAL = 10 pontos.
+
+---
+
+# 📊 INSTRUÇÕES DE EXECUÇÃO
+
+1. Auditoria por Grupo:
+Avaliar cada ad_group individualmente antes da consolidação.
+
+2. Indicação de Falhas:
+Sempre indicar qual grupo apresentou problema.
+
+3. Simulação Visual:
+Gerar 1 simulação de anúncio por grupo.
+
+4. HARD MATCH OBRIGATÓRIO:
+Toda comparação deve usar normalizar(texto).
+
+---
+
+# 📦 FORMATO DE RETORNO (JSON)
+
+{
+ "nota_final": 0.0,
+ "criterios": [
+ {
+ "nome": "Keyword Match (Regra de Ferro)",
+ "peso": 4.0,
+ "nota": 0.0,
+ "nota_maxima": 4.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Relevância Temática",
+ "peso": 3.0,
+ "nota": 0.0,
+ "nota_maxima": 3.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Compliance Editorial",
+ "peso": 1.5,
+ "nota": 0.0,
+ "nota_maxima": 1.5,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Aproveitamento de Espaço",
+ "peso": 1.0,
+ "nota": 0.0,
+ "nota_maxima": 1.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Extensões e Ativos",
+ "peso": 0.5,
+ "nota": 0.0,
+ "nota_maxima": 0.5,
+ "falhas": [],
+ "sugestoes": []
+ }
+ ],
+ "resumo": "",
+ "simulacoes_anuncio": [
+ {
+ "grupo": "",
+ "titulo_linha_1": "",
+ "titulo_linha_2": "",
+ "titulo_linha_3": "",
+ "url_display": "",
+ "descricao": ""
+ }
+ ]
+}
+
+---
+
+# 🔎 DIFERENÇAS DO GAS v5 → GAS v6
+
+✔ HARD MATCH real (comparação normalizada)
+✔ Capitalização inicial aceita oficialmente
+✔ Delimitadores [ ] e " ignorados
+✔ Anti-truncamento reforçado
+✔ Range técnico consolidado (80–92)
+
+Resultado esperado:
+Keyword Match tende a 4.0 quando ENGINE HARD LOCK estiver correto.
\ No newline at end of file
diff --git a/regras_auditoriaVGAS6.md b/regras_auditoriaVGAS6.md
new file mode 100644
index 0000000..af2397c
--- /dev/null
+++ b/regras_auditoriaVGAS6.md
@@ -0,0 +1,290 @@
+# REGRAS DE AUDITORIA — Google Ads Scorecard (GAS v6 — ENGINE AUDITOR HARD MATCH | V12.6 HEADLINE 30)
+
+OBJETIVO:
+Versão HARD MATCH da auditoria compatível com ENGINE HARD LOCK (V11).
+Eliminar falsos negativos de Keyword Match causados por delimitadores,
+capitalização inicial ou pequenas diferenças estruturais.
+
+Nenhuma regra anterior foi removida.
+A auditoria agora valida igualdade SEMÂNTICA NORMALIZADA.
+
+Nota final: 0 a 10 pontos (soma ponderada).
+
+---
+
+# 🆕 ATUALIZAÇÃO V12.6 — LIMITES DE CARACTERES
+
+HEADLINES:
+- limite máximo: 30 caracteres
+
+DEMAIS ATIVOS:
+- sitelinks, callouts e ativos auxiliares permanecem com limite 25 caracteres
+
+---
+
+# 0. ENGINE AUDITOR — FLUXO GLOBAL
+
+INÍCIO
+
+PASSO 1 → Normalizar Keywords
+PASSO 2 → Normalizar Headlines (validar ≤30 caracteres)
+PASSO 3 → Validar Keyword Match (Hard Match)
+PASSO 4 → Validar Relevância Temática
+PASSO 5 → Validar Compliance Editorial
+PASSO 6 → Validar Aproveitamento de Espaço
+PASSO 7 → Validar Extensões e Ativos
+PASSO 8 → Calcular Nota Final
+
+FIM
+
+---
+
+# 🔧 FUNÇÃO GLOBAL DE NORMALIZAÇÃO (HARD MATCH)
+
+FUNÇÃO normalizar(texto):
+
+ remover delimitadores "[" "]" e '"'
+ remover espaços duplicados
+ permitir Capitalização Inicial
+ converter apenas para comparação semântica
+ manter acentuação
+ NÃO alterar ordem das palavras
+
+RETORNAR texto_normalizado
+
+OBSERVAÇÃO:
+Capitalização inicial é considerada equivalente.
+
+Exemplo ACEITO:
+
+keyword: [separação judicial]
+headline: Separação Judicial
+
+keyword: "advogado guarda"
+headline: Advogado Guarda
+
+---
+
+# 1. Keyword Match — Regra de Ferro HARD MATCH (Peso 4.0)
+
+## ENGINE VALIDATION
+
+PARA cada ad_group:
+
+ keywords_norm = normalizar(keywords)
+ headlines_norm = normalizar(headlines)
+
+ contar headlines onde:
+
+ lower(keywords_norm) == lower(headlines_norm)
+
+SE total >= 5:
+ nota máxima
+
+## CRITÉRIO ELIMINATÓRIO
+
+Se headline adicionar palavra extra após normalização:
+
+EXEMPLO NÃO ACEITO:
+
+keyword: advogado guarda
+headline: Advogado Guarda SP ❌
+
+Se houver alteração semântica:
+nota do grupo = 0
+
+---
+
+# 2. Relevância Temática e Isolamento (Peso 3.0)
+
+ENGINE CHECK:
+
+Identificar núcleo jurídico dominante por grupo.
+
+NÚCLEOS EXCLUSIVOS:
+- divórcio
+- guarda
+- pensão
+- inventário
+- sucessão
+
+Palavras genéricas permitidas:
+- advogado
+- especialista
+- atendimento
+
+Penalidade:
+-1.0 por contaminação temática.
+
+---
+
+# 3. Compliance Editorial (Peso 1.5)
+
+ENGINE SCAN:
+
+PROIBIDO:
+- grátis
+- gratuito
+- variações diretas
+
+FORMATAÇÃO PROIBIDA:
+- !!! ??? repetidos
+- CAIXA ALTA total (exceto SP, OAB)
+
+Se existir termo proibido:
+nota = 0 automaticamente.
+
+---
+
+# 4. Aproveitamento de Espaço — HARD RANGE (Peso 1.0)
+
+VALIDAÇÃO HEADLINES (V12.6):
+- Headlines devem possuir ≤30 caracteres.
+- Se qualquer headline exceder 30 → penalidade automática.
+
+ACEITO:
+80 ≤ descrição ≤ 92 caracteres
+
+IDEAL:
+85–90 caracteres
+
+Penalidade:
+-0.2 por descrição abaixo de 80.
+
+## HARD MATCH — ANTI-TRUNCAMENTO
+
+Se descrição terminar com:
+
+- palavra incompleta
+- corte de substring
+- espaço final inválido
+
+→ falha automática.
+
+Exemplo NÃO ACEITO:
+"Consulte um espe"
+"Fale conosco ag"
+
+---
+
+# 5. Extensões e Ativos (Peso 0.5)
+
+ENGINE CHECK:
+
+SITELINKS:
+- description1 preenchido
+- description2 preenchido
+
+CALLOUTS:
+- mínimo 6
+
+🆕 HARD MATCH BONUS:
+Presença de 7º callout diferencial reforça nota máxima.
+
+---
+
+# 🧮 CÁLCULO DA NOTA FINAL
+
+nota_final = soma ponderada:
+
+Keyword Match (até 4.0)
++
+Relevância Temática (até 3.0)
++
+Compliance Editorial (até 1.5)
++
+Aproveitamento de Espaço (até 1.0)
++
+Extensões e Ativos (até 0.5)
+
+TOTAL = 10 pontos.
+
+---
+
+# 📊 INSTRUÇÕES DE EXECUÇÃO
+
+1. Auditoria por Grupo:
+Avaliar cada ad_group individualmente antes da consolidação.
+
+2. Indicação de Falhas:
+Sempre indicar qual grupo apresentou problema.
+
+3. Simulação Visual:
+Gerar 1 simulação de anúncio por grupo.
+
+4. HARD MATCH OBRIGATÓRIO:
+Toda comparação deve usar normalizar(texto).
+
+---
+
+# 📦 FORMATO DE RETORNO (JSON)
+
+{
+ "nota_final": 0.0,
+ "criterios": [
+ {
+ "nome": "Keyword Match (Regra de Ferro)",
+ "peso": 4.0,
+ "nota": 0.0,
+ "nota_maxima": 4.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Relevância Temática",
+ "peso": 3.0,
+ "nota": 0.0,
+ "nota_maxima": 3.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Compliance Editorial",
+ "peso": 1.5,
+ "nota": 0.0,
+ "nota_maxima": 1.5,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Aproveitamento de Espaço",
+ "peso": 1.0,
+ "nota": 0.0,
+ "nota_maxima": 1.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Extensões e Ativos",
+ "peso": 0.5,
+ "nota": 0.0,
+ "nota_maxima": 0.5,
+ "falhas": [],
+ "sugestoes": []
+ }
+ ],
+ "resumo": "",
+ "simulacoes_anuncio": [
+ {
+ "grupo": "",
+ "titulo_linha_1": "",
+ "titulo_linha_2": "",
+ "titulo_linha_3": "",
+ "url_display": "",
+ "descricao": ""
+ }
+ ]
+}
+
+---
+
+# 🔎 DIFERENÇAS DO GAS v5 → GAS v6
+
+✔ HARD MATCH real (comparação normalizada)
+✔ Capitalização inicial aceita oficialmente
+✔ Delimitadores [ ] e " ignorados
+✔ Anti-truncamento reforçado
+✔ Range técnico consolidado (80–92)
+
+Resultado esperado:
+Keyword Match tende a 4.0 quando ENGINE HARD LOCK estiver correto.
diff --git a/regras_auditoria_old-boa.md b/regras_auditoria_old-boa.md
new file mode 100644
index 0000000..c747f99
--- /dev/null
+++ b/regras_auditoria_old-boa.md
@@ -0,0 +1,106 @@
+Aqui está o documento de **REGRAS DE AUDITORIA (GAS v3)** atualizado, integrando o rigor da **Regra de Ferro** para palavras-chave, o novo critério de **Relevância Temática** e a redistribuição de pesos para totalizar 10 pontos.
+
+---
+
+# REGRAS DE AUDITORIA — Google Ads Scorecard (GAS v3)
+
+Avalie cada critério abaixo considerando TODOS os ad_groups. A nota final (0 a 10) é a soma ponderada dos pesos abaixo:
+
+## 1. Keyword Match - Regra de Ferro (Peso 4.0 — máx 4.0 pontos)
+
+* Em cada grupo, pelo menos 5 títulos (headlines) devem ser **IDÊNTICOS** às keywords de fundo de funil do grupo.
+* **CRITÉRIO ELIMINATÓRIO:** Proibido adicionar qualquer palavra, hífen, ou adjetivo antes ou depois da keyword nesses 5 títulos.
+* **Penalidade:** Se um título "enfeitar" a keyword (ex: mudou "advogado guarda" para "advogado guarda em SP"), a nota deste critério para o grupo será **ZERO**.
+
+## 2. Relevância Temática e Isolamento (Peso 3.0 — máx 3.0 pontos)
+
+* **Isolamento de Tema:** Avalie se os 3 grupos possuem temas realmente distintos. Keywords e títulos de um AdGroup não podem "contaminar" outros grupos (ex: termo "inventário" dentro do grupo de "divórcio").
+* **Penalidade:** -1.0 para cada ocorrência de mistura de temas entre grupos.
+
+## 3. Compliance Editorial (Peso 1.5 — máx 1.5 pontos)
+
+* Ausência total de termos proibidos: "grátis", "gratuito", sinônimos (em todos os grupos).
+* Sem formatação proibida: excesso de pontuação (!!!, ???), CAIXA ALTA indevida (exceto siglas como SP, OAB).
+* **Penalidade:** Se houver a palavra "grátis" ou "gratuito" em qualquer grupo, a nota deste critério é **0 automaticamente**.
+
+## 4. Aproveitamento de Espaço (Peso 1.0 — máx 1.0 ponto)
+
+* Descrições devem ter entre 80 e 90 caracteres. Títulos próximos de 30 caracteres.
+* **Penalidade:** -0.2 para cada descrição abaixo de 80 caracteres que desperdiça espaço visual.
+
+## 5. Extensões e Ativos (Peso 0.5 — máx 0.5 ponto)
+
+* Sitelinks devem ter descrições preenchidas (description1 e description2).
+* Callouts devem ser focados em benefícios reais, autoridade e agilidade.
+
+---
+
+## INSTRUÇÕES DE EXECUÇÃO
+
+1. **Auditoria por Grupo:** A IA deve analisar cada `ad_group` individualmente antes de consolidar a nota.
+2. **Indicação de Falhas:** Nas falhas, SEMPRE indique em qual grupo o problema foi encontrado (ex: "Grupo 'Divórcio': Título 3 alterou keyword...").
+3. **Simulação:** Gere uma **Simulação de Anúncio** para CADA grupo, mostrando a composição visual final.
+
+## FORMATO DE RETORNO (JSON)
+
+Retorne o resultado EXATAMENTE neste formato JSON:
+
+```json
+{
+ "nota_final": 0.0,
+ "criterios": [
+ {
+ "nome": "Keyword Match (Regra de Ferro)",
+ "peso": 4.0,
+ "nota": 0.0,
+ "nota_maxima": 4.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Relevância Temática",
+ "peso": 3.0,
+ "nota": 0.0,
+ "nota_maxima": 3.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Compliance Editorial",
+ "peso": 1.5,
+ "nota": 0.0,
+ "nota_maxima": 1.5,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Aproveitamento de Espaço",
+ "peso": 1.0,
+ "nota": 0.0,
+ "nota_maxima": 1.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Extensões e Ativos",
+ "peso": 0.5,
+ "nota": 0.0,
+ "nota_maxima": 0.5,
+ "falhas": [],
+ "sugestoes": []
+ }
+ ],
+ "resumo": "",
+ "simulacoes_anuncio": [
+ {
+ "grupo": "",
+ "titulo_linha_1": "",
+ "titulo_linha_2": "",
+ "titulo_linha_3": "",
+ "url_display": "",
+ "descricao": ""
+ }
+ ]
+}
+
+```
\ No newline at end of file
diff --git a/regras_auditoria_old.md b/regras_auditoria_old.md
new file mode 100644
index 0000000..b4eb172
--- /dev/null
+++ b/regras_auditoria_old.md
@@ -0,0 +1,115 @@
+# REGRAS DE AUDITORIA — Google Ads Scorecard (GAS)
+
+Avalie cada critério abaixo considerando TODOS os ad_groups. A nota final (0 a 10) é a soma ponderada:
+
+## 1. Relevância e Keyword Match (Peso 3.0 — máx 3.0 pontos)
+- Em cada grupo, pelo menos 5 títulos (headlines) devem ser idênticos às keywords de fundo de funil do grupo.
+- Penalidade: -0.5 para cada título que "enfeitou" a palavra-chave (ex: mudou "advogado guarda" para "melhor advogado de guarda").
+- Avalie se os 3 grupos possuem temas realmente distintos entre si.
+
+## 2. Compliance Editorial (Peso 2.0 — máx 2.0 pontos)
+- Ausência total de termos proibidos: "grátis", "gratuito", sinônimos (em todos os grupos).
+- Sem formatação proibida: excesso de pontuação (!!!, ???), CAIXA ALTA indevida (exceto siglas como SP, OAB).
+- Se houver a palavra "grátis" ou "gratuito" em qualquer grupo, a nota deste critério é 0 automaticamente.
+
+## 3. Aproveitamento de Espaço (Peso 2.0 — máx 2.0 pontos)
+- Descrições devem ter entre 80 e 90 caracteres. Títulos próximos de 30 caracteres.
+- Penalidade: -0.5 para cada descrição muito curta (abaixo de 70 chars) que desperdiça espaço.
+- Aplique a todos os grupos.
+
+## 4. Variedade de Gatilhos (Peso 2.0 — máx 2.0 pontos)
+- Deve haver 3 pilares nos títulos: Ação (CTA), Autoridade (anos de exp, nota 5.0) e Localização (cidade/estado).
+- Penalidade: -1.0 se todos os títulos de um grupo forem apenas descritivos, sem convidar ao clique.
+
+## 5. Extensões e Ativos (Peso 1.0 — máx 1.0 ponto)
+- Sitelinks devem ter descrições preenchidas (description1 e description2).
+- Callouts devem ser focados em benefícios reais, não genéricos.
+
+## 7. SCORECARD DE AUDITORIA POR GRUPO (GAS v2)
+A IA deve auto-avaliar o JSON final de 0 a 10 com base nos pesos abaixo, aplicados **individualmente a cada ad_group**:
+
+- **Relevância Temática (3.5):** As keywords e títulos combinam com o tema do grupo?
+- **Coerência de AdGroup (2.5):** Os títulos de keyword são específicos para aquele grupo?
+- **Espaço e Compliance (2.0):** Descrições > 80ch e zero termos proibidos?
+- **Estrutura de Ativos (2.0):** Sitelinks e Callouts estão completos?
+
+## INSTRUÇÕES ADICIONAIS
+Nas falhas, SEMPRE indique em qual grupo (ad_group) o problema foi encontrado (ex: "Grupo 'Divórcio': Título 3 alterou keyword...").
+
+Além da avaliação, gere uma **Simulação de Anúncio** para CADA grupo, mostrando como os melhores títulos e a melhor descrição apareceriam em uma busca real no Google.
+
+## FORMATO DE RETORNO (JSON)
+Retorne o resultado EXATAMENTE neste formato JSON:
+
+{
+ "nota_final": 8.5,
+ "criterios": [
+ {
+ "nome": "Relevância e Keyword Match",
+ "peso": 3.0,
+ "nota": 2.5,
+ "nota_maxima": 3.0,
+ "falhas": ["Grupo 'Divórcio': Título 3 alterou keyword 'advogado divórcio' para 'melhor advogado divórcio'"],
+ "sugestoes": ["Usar keyword exata sem modificadores nos primeiros 5 títulos de cada grupo"]
+ },
+ {
+ "nome": "Compliance Editorial",
+ "peso": 2.0,
+ "nota": 2.0,
+ "nota_maxima": 2.0,
+ "falhas": [],
+ "sugestoes": []
+ },
+ {
+ "nome": "Aproveitamento de Espaço",
+ "peso": 2.0,
+ "nota": 1.5,
+ "nota_maxima": 2.0,
+ "falhas": ["Grupo 'Guarda': Descrição 2 tem apenas 45 caracteres"],
+ "sugestoes": ["Expandir descrições curtas para aproveitar 80-90 caracteres"]
+ },
+ {
+ "nome": "Variedade de Gatilhos",
+ "peso": 2.0,
+ "nota": 1.5,
+ "nota_maxima": 2.0,
+ "falhas": ["Grupo 'Pensão': Nenhum título contém localização"],
+ "sugestoes": ["Adicionar cidade/estado em pelo menos 2 títulos por grupo"]
+ },
+ {
+ "nome": "Extensões e Ativos",
+ "peso": 1.0,
+ "nota": 1.0,
+ "nota_maxima": 1.0,
+ "falhas": [],
+ "sugestoes": []
+ }
+ ],
+ "resumo": "Campanha multi-grupo com boa segmentação temática, mas precisa ajustar keyword match no Grupo 1 e aproveitar melhor o espaço das descrições no Grupo 2.",
+ "simulacoes_anuncio": [
+ {
+ "grupo": "Nome do Grupo 1",
+ "titulo_linha_1": "Advogado Divórcio SP",
+ "titulo_linha_2": "Consulta Especializada",
+ "titulo_linha_3": "+10 Anos de Experiência",
+ "url_display": "www.exemplo.com.br",
+ "descricao": "Advogado especialista em divórcio com atendimento personalizado. Agende sua consulta!"
+ },
+ {
+ "grupo": "Nome do Grupo 2",
+ "titulo_linha_1": "Advogado Guarda",
+ "titulo_linha_2": "Fale Conosco Agora",
+ "titulo_linha_3": "Avaliação 5.0 no Google",
+ "url_display": "www.exemplo.com.br",
+ "descricao": "Especialista em guarda de filhos. Proteja seus direitos com quem entende do assunto!"
+ },
+ {
+ "grupo": "Nome do Grupo 3",
+ "titulo_linha_1": "Pensão Alimentícia",
+ "titulo_linha_2": "Agende sua Consulta Hoje",
+ "titulo_linha_3": "Atendimento 100% Online",
+ "url_display": "www.exemplo.com.br",
+ "descricao": "Advogado para pensão alimentícia. Resolva seu caso com agilidade e segurança jurídica!"
+ }
+ ]
+}
diff --git a/regras_prompt-V12.4.md b/regras_prompt-V12.4.md
new file mode 100644
index 0000000..0886ea5
--- /dev/null
+++ b/regras_prompt-V12.4.md
@@ -0,0 +1,322 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V12.4 — HARD MATCH + DKI SAFE MODE)
+
+OBJETIVO:
+Engine determinístico com canonização automática de keywords, espelhamento obrigatório de keywords em headlines
+(HARD MATCH 7/7) + 01 título com DKI em modo seguro (SEM substituir espelhos),
+e regeneração obrigatória de descrições (SEM substring).
+
+Eliminar:
+- falhas de Keyword Match por ausência de headline espelho exato
+- truncamento por substring (descrições e callouts)
+- inconsistência entre ENGINE e AUDITORIA
+- riscos editoriais/limite de caracteres introduzidos por DKI
+
+Nenhuma regra anterior foi removida — apenas reforçada.
+Capitalização inicial permitida.
+Compatível com GAS v6 — ENGINE AUDITOR HARD MATCH.
+
+---
+
+# 0. FLUXO GLOBAL (DETERMINISTIC EXECUTION)
+
+INÍCIO
+
+PARA cada GRUPO_DE_ANUNCIO (total = 3):
+
+ PASSO 1 → Gerar Keywords
+ PASSO 1.1 → SANITIZAR E CANONIZAR KEYWORDS
+ PASSO 2 → Normalizar Keywords
+ PASSO 3 → Classificar Keywords por PRIORIDADE
+ PASSO 4 → Selecionar Literais (ALGORITMO FIXO)
+ PASSO 4.1 → Gerar HEADLINES ESPELHO (1 por keyword)
+ PASSO 4.2 → Gerar 01 HEADLINE DKI (SAFE MODE)
+ PASSO 5 → Montar LISTA FINAL DE TÍTULOS (15) com MIRROR ENFORCEMENT + DKI SAFE
+ PASSO 6 → Gerar Descrições (REGENERAÇÃO OBRIGATÓRIA / SEM CORTE)
+ PASSO 7 → Validar Limites
+ PASSO 8 → Inserir Localização (fora dos espelhos)
+
+FIM
+
+Depois:
+ PASSO 9 → Negativas
+ PASSO 10 → Extensões
+ PASSO 11 → HARD LOCK VALIDATION
+
+RETORNAR apenas JSON estruturado.
+
+---
+
+# 1. PALAVRAS-CHAVE (ENGINE STEP)
+
+GERAR exatamente 7 keywords por grupo.
+
+REGRAS:
+- usar [Exata] e "Frase"
+- máximo 25 caracteres
+- keywords devem ser juridicamente tituláveis.
+
+---
+
+## SANITIZAÇÃO E CANONIZAÇÃO (OBRIGATÓRIA)
+
+ANTES de aceitar qualquer keyword:
+
+FUNÇÃO canonizar(keyword):
+
+ remover duplicações de delimitadores:
+ [[texto]] → texto
+
+ converter para forma jurídica clara e titulável.
+
+Exemplos obrigatórios:
+ casamento fim → Fim do Casamento
+ fim do casamento → Fim do Casamento
+ fim pensão → Fim da Pensão
+ fim da pensão → Fim da Pensão
+ custo inventário → Custo Inventário
+
+SE keyword for vaga, truncada ou não titulável:
+ DESCARTAR e gerar nova.
+
+---
+
+## ENGINE NORMALIZE
+
+FUNÇÃO normalizar(texto):
+
+ remover "[" "]" e '"'
+ remover espaços duplicados
+ aplicar Capitalização Inicial
+ manter acentuação
+ manter ordem
+ NÃO alterar semântica
+
+RETORNAR texto_normalizado
+
+---
+
+# 2. CLASSIFICAÇÃO DETERMINÍSTICA DE KEYWORDS
+
+PRIORIDADE 1 — COMERCIAL DIRETA
+- valor
+- custo
+- preço
+- regularização
+- revisão
+- cálculo
+- quanto custa
+- honorários
+
+PRIORIDADE 2 — CONTRATAÇÃO
+- advogado
+- ação
+- processo
+- consulta
+
+PRIORIDADE 3 — INFORMATIVA
+demais termos.
+
+---
+
+# 3. SELEÇÃO LITERAL DETERMINÍSTICA
+
+FUNÇÃO selecionar_keywords_literais(lista_keywords):
+
+ ordenar por:
+ prioridade ASC
+ tamanho ASC
+ ordem original
+
+ selecionar até 5.
+
+❗ Seleção automática — IA não escolhe manualmente.
+
+---
+
+# 4. KEYWORD MIRROR HEADLINES — HARD MATCH (ENFORCEMENT)
+
+OBJETIVO:
+Garantir que TODAS as 7 keywords do grupo tenham ao menos 1 headline correspondente EXATO após normalização.
+Se a keyword existir, o headline espelho correspondente DEVE existir no output final.
+
+REGRAS:
+
+1) Para cada keyword do grupo (7):
+ headline_espelho = normalizar(canonizar(keyword))
+
+2) O headline_espelho deve ser EXATO (após normalização).
+ PROIBIDO:
+ - adicionar palavras (ex: "SP", "Rápido", "Seguro")
+ - remover palavras
+ - trocar ordem
+ - criar sinônimos
+ - plural/singular diferente
+
+3) PROIBIÇÃO ABSOLUTA — "SP" NO ESPELHO
+ Se a keyword NÃO contém "SP" ou "São Paulo",
+ o headline_espelho NÃO pode conter "SP" ou "São Paulo".
+
+4) MIRROR ENFORCEMENT (REGRA DE MONTAGEM):
+ A lista final de 15 títulos DO GRUPO deve conter, como itens separados,
+ TODOS os 7 headlines_espelho. Nenhum espelho pode ser omitido, substituído ou reescrito.
+
+---
+
+# 4.2 HEADLINE DKI — SAFE MODE (NOVO)
+
+OBJETIVO:
+Adicionar 01 headline com Dynamic Keyword Insertion para aumentar relevância/CTR em cauda longa,
+SEM derrubar o HARD MATCH (espelhos continuam obrigatórios).
+
+FORMATO DKI (OBRIGATÓRIO):
+{Keyword:PADRAO_DO_GRUPO}
+
+REGRAS SAFE:
+
+1) O headline DKI NUNCA substitui nenhum headline_espelho.
+ Os 7 espelhos continuam obrigatórios dentro dos 15 títulos.
+
+2) O headline DKI NÃO PODE entrar no Bucket de LITERAIS.
+ Ele entra apenas como título "livre" dentro do Bucket CTA/Local.
+
+3) PADRAO_DO_GRUPO deve ser curto, titulável e ≤ 30 caracteres.
+ Exemplos recomendados (escolher 1):
+ - Advogado de Família
+ - Divórcio em SP
+ - Guarda e Pensão
+ - Inventário em SP
+
+4) PROIBIDO no DKI:
+ - termos proibidos ("Grátis", "Gratuito" e variações)
+ - palavras cortadas
+ - caixa alta total
+ - pontuação excessiva
+
+5) Se houver risco de estourar 30 caracteres (por inserção dinâmica),
+ o PADRAO_DO_GRUPO deve ser ainda mais curto (≤ 20 caracteres).
+
+---
+
+# 5. TÍTULOS — MONTAGEM DETERMINÍSTICA (15 POR GRUPO)
+
+Distribuição original mantida: 05 / 05 / 05 (LITERAL / CTA-LOCAL / PROVA).
+
+REGRA DETERMINÍSTICA DE POSIÇÃO:
+
+- Bucket 1 (05 LITERAIS): usar os 5 primeiros headlines_espelho (pela ordem de prioridade).
+- Bucket 2 (05 CTA/LOCAL): 3 CTAs/Local reais + 1 headline_espelho (o 6º) + 1 headline DKI (SAFE MODE).
+- Bucket 3 (05 PROVA): 4 provas/diferenciais reais + 1 headline_espelho (o 7º).
+
+HARD LOCK:
+- os 7 headlines_espelho entram exatamente como gerados (sem SP extra).
+- CTAs/Provas podem conter SP/São Paulo quando possível.
+- o DKI entra como adicional controlado, sem substituir espelhos.
+
+LIMITES:
+- cada título ≤ 30 caracteres
+
+---
+
+# 6. DESCRIÇÕES — DETERMINISTIC TEXT ENGINE (NO-TRUNC OUTPUT)
+
+RANGE:
+80 ≤ caracteres ≤ 92
+IDEAL: 85–90
+
+ORDEM DE EXECUÇÃO OBRIGATÓRIA:
+1) GERAR descrição completa
+2) VALIDAR tamanho e integridade
+3) SE inválida → REGERAR DO ZERO
+4) PROIBIDO ajustar por corte
+
+REGRA ABSOLUTA — ANTI SUBSTRING:
+NUNCA usar:
+- texto[:limite]
+- slice
+- substring
+- corte parcial
+
+VALIDAÇÃO DE INTEGRIDADE (OBRIGATÓRIA):
+Uma descrição SÓ É VÁLIDA se:
+- 80 ≤ caracteres ≤ 92
+- termina com "." ou "!"
+- NÃO termina com espaço
+- NÃO termina com palavra incompleta
+
+SE QUALQUER ITEM FALHAR:
+ REGERAR do zero (reescrita completa).
+
+AJUSTE DETERMINÍSTICO:
+- Se > 92: reescrever reduzindo conectivos e adjetivos (sem cortar string).
+- Se < 80: reescrever adicionando UVP ou CTA (sem inflar demais).
+
+CONTEÚDO:
+- incluir UVP + CTA
+- incluir SP/São Paulo quando possível (sem quebrar o range)
+
+---
+
+# 7. EXTENSÕES (ATIVOS) — HARD LOCK
+
+SITELINKS:
+- 4 obrigatórios
+- título ≤25 caracteres
+- 2 descrições ≤35 caracteres
+
+CALLOUTS:
+- 7 obrigatórios
+- ≤25 caracteres
+- incluir 01 diferencial competitivo
+
+ANTI-TRUNCAMENTO — CALLOUTS (OBRIGATÓRIO):
+- Proibido cortar string para caber em 25 caracteres.
+- Se exceder 25, REESCREVER o callout completo (sem substring).
+- Proibido terminar com palavra incompleta ou espaço final.
+
+---
+
+# 8. POLÍTICAS EDITORIAIS
+
+PROIBIDO:
+- "Grátis"
+- "Gratuito"
+- caixa alta total (exceto SP, OAB)
+- pontuação excessiva
+
+PERMITIDO:
+- Capitalização inicial nos espelhos e literais.
+
+---
+
+# 9. HARD LOCK VALIDATION (FINAL)
+
+VALIDAR:
+
+- existem 3 grupos?
+- existem 7 keywords por grupo?
+- keywords foram canonizadas?
+- existem 7 headlines_espelho (1 por keyword)?
+- CADA keyword possui 1 headline_espelho EXATO após normalização?
+- existem 15 títulos por grupo?
+- Bucket 1 contém 5 espelhos?
+- Bucket 2 contém 1 espelho (6º) + 3 CTAs reais + 1 DKI?
+- Bucket 3 contém 1 espelho (7º) + 4 provas reais?
+- o DKI está no formato {Keyword:...} e ≤ 30?
+- o DKI NÃO contém termos proibidos?
+- nenhum título > 30?
+- descrições 80–92?
+- descrições terminam com "." ou "!"?
+- nenhuma descrição termina com palavra incompleta ou espaço?
+- nenhuma substring detectada em descrições?
+- callouts completos (sem palavra incompleta) e ≤25?
+- existem 7 callouts?
+
+SE falhar:
+ REEXECUTAR apenas bloco inválido
+
+---
+
+RETORNO FINAL:
+APENAS JSON estruturado.
+SEM explicações.
+SEM texto adicional.
\ No newline at end of file
diff --git a/regras_prompt.md b/regras_prompt.md
new file mode 100644
index 0000000..8614b09
--- /dev/null
+++ b/regras_prompt.md
@@ -0,0 +1,334 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V12.6 — HEADLINE 30 + HARD MATCH + DKI SAFE MODE + RECONCILIATION ENGINE)
+
+OBJETIVO:
+Engine determinístico com canonização automática de keywords, espelhamento obrigatório de keywords em headlines
+(HARD MATCH 7/7) + 01 título com DKI em modo seguro (SEM substituir espelhos),
+e regeneração obrigatória de descrições (SEM substring), agora com camada adicional
+RECONCILIATION ENGINE (ANTI FALSE NEGATIVE) para validação estrutural final.
+
+Compatível com GAS v6 — ENGINE AUDITOR HARD MATCH.
+
+---
+
+# 🆕 V12.6 — ATUALIZAÇÃO (HEADLINE 30)
+
+* **Headlines (Títulos):** limite alterado de **25** para **30 caracteres**.
+* **Demais ativos** (keywords, callouts, sitelinks e descrições de sitelink): permanecem com **limite 25** (ou os limites já definidos nas seções próprias).
+
+---
+
+# 0. FLUXO GLOBAL (DETERMINISTIC EXECUTION)
+
+INÍCIO
+
+PARA cada GRUPO_DE_ANUNCIO (total = 3):
+
+```
+PASSO 1 → Gerar Keywords
+PASSO 1.1 → SANITIZAR E CANONIZAR KEYWORDS
+PASSO 2 → Normalizar Keywords
+PASSO 3 → Classificar Keywords por PRIORIDADE
+PASSO 4 → Selecionar Literais (ALGORITMO FIXO)
+PASSO 4.1 → Gerar HEADLINES ESPELHO (1 por keyword)
+PASSO 4.2 → Gerar 01 HEADLINE DKI (SAFE MODE)
+PASSO 5 → Montar LISTA FINAL DE TÍTULOS (15) com MIRROR ENFORCEMENT + DKI SAFE
+PASSO 6 → Gerar Descrições (REGENERAÇÃO OBRIGATÓRIA / SEM CORTE)
+PASSO 7 → Validar Limites (Headlines ≤30; demais conforme seção)
+PASSO 8 → Inserir Localização (fora dos espelhos)
+```
+
+FIM
+
+Depois:
+PASSO 9 → Negativas
+PASSO 10 → Extensões
+PASSO 11 → HARD LOCK VALIDATION
+PASSO 12 → RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+PASSO 13 → AUDITORIA GAS v6 (Opcional)
+
+RETORNAR apenas JSON estruturado.
+
+---
+
+# 1. PALAVRAS-CHAVE (ENGINE STEP)
+
+GERAR exatamente 7 keywords por grupo.
+
+REGRAS:
+
+* usar [Exata] e "Frase"
+* máximo 25 caracteres
+* keywords devem ser juridicamente tituláveis.
+
+---
+
+## SANITIZAÇÃO E CANONIZAÇÃO (OBRIGATÓRIA)
+
+ANTES de aceitar qualquer keyword:
+
+FUNÇÃO canonizar(keyword):
+
+```
+remover duplicações de delimitadores:
+ [[texto]] → texto
+
+converter para forma jurídica clara e titulável.
+```
+
+Exemplos obrigatórios:
+casamento fim → Fim do Casamento
+fim do casamento → Fim do Casamento
+fim pensão → Fim da Pensão
+fim da pensão → Fim da Pensão
+custo inventário → Custo Inventário
+
+SE keyword for vaga, truncada ou não titulável:
+DESCARTAR e gerar nova.
+
+---
+
+## ENGINE NORMALIZE
+
+FUNÇÃO normalizar(texto):
+
+```
+remover "[" "]" e '"'
+remover espaços duplicados
+aplicar Capitalização Inicial
+manter acentuação
+manter ordem
+NÃO alterar semântica
+```
+
+RETORNAR texto_normalizado
+
+---
+
+# 2. CLASSIFICAÇÃO DETERMINÍSTICA DE KEYWORDS
+
+PRIORIDADE 1 — COMERCIAL DIRETA
+
+* valor
+* custo
+* preço
+* regularização
+* revisão
+* cálculo
+* quanto custa
+* honorários
+
+PRIORIDADE 2 — CONTRATAÇÃO
+
+* advogado
+* ação
+* processo
+* consulta
+
+PRIORIDADE 3 — INFORMATIVA
+Demais termos.
+
+---
+
+# 3. SELEÇÃO LITERAL DETERMINÍSTICA
+
+FUNÇÃO selecionar_keywords_literais(lista_keywords):
+
+```
+ordenar por:
+ prioridade ASC
+ tamanho ASC
+ ordem original
+
+selecionar até 5.
+```
+
+---
+
+# 4. KEYWORD MIRROR HEADLINES — HARD MATCH (ENFORCEMENT)
+
+OBJETIVO:
+Garantir que TODAS as 7 keywords do grupo tenham ao menos 1 headline correspondente EXATO após normalização.
+
+REGRAS:
+
+1. Para cada keyword do grupo (7):
+ headline_espelho = normalizar(canonizar(keyword))
+
+2. O headline_espelho deve ser EXATO (após normalização).
+
+3. PROIBIÇÃO ABSOLUTA — "SP" NO ESPELHO
+
+4. MIRROR ENFORCEMENT:
+ A lista final de 15 títulos deve conter TODOS os 7 headlines_espelho.
+
+---
+
+# 4.2 HEADLINE DKI — SAFE MODE
+
+FORMATO:
+{Keyword:PADRAO_DO_GRUPO}
+
+REGRAS SAFE:
+
+* DKI nunca substitui espelhos
+* DKI fora do bucket literal
+* ≤30 caracteres
+
+---
+
+# 5. TÍTULOS — MONTAGEM DETERMINÍSTICA
+
+**Limite de caracteres (V12.6):** cada headline deve ter **≤30 caracteres**.
+
+Distribuição: 05 / 05 / 05
+
+Bucket 1: 5 espelhos
+Bucket 2: 3 CTA/Local + 1 espelho + 1 DKI
+Bucket 3: 4 provas + 1 espelho
+
+---
+
+# 6. DESCRIÇÕES — DETERMINISTIC TEXT ENGINE
+
+80 ≤ caracteres ≤ 92
+
+Proibido substring.
+Falhou validação → Regenerar do zero.
+
+---
+
+# 7. EXTENSÕES — HARD LOCK
+
+Sitelinks: 4 obrigatórios
+Callouts: 7 obrigatórios
+
+---
+
+# 8. POLÍTICAS EDITORIAIS
+
+Proibido:
+
+* Grátis
+* Gratuito
+* caixa alta total
+
+---
+
+# 9. HARD LOCK VALIDATION (FINAL)
+
+Validar integridade completa antes da saída.
+
+---
+
+# V12.5 — RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+
+OBJETIVO:
+Adicionar camada determinística de Reconciliação garantindo integridade total entre ENGINE e estado final.
+
+---
+
+## PRINCÍPIO
+
+ENGINE → cria HARD LOCK → RECONCILIATION → AUDITORIA
+
+---
+
+## SNAPSHOTS OBRIGATÓRIOS (ENGINE_STATE)
+
+keywords_raw[7]
+keywords_canon[7]
+keywords_norm[7]
+headlines_espelho_criados[7]
+headline_dki_criado[1]
+headlines_final[15]
+
+bucket_map_final
+
+descricao_final
+callouts_final[7]
+sitelinks_final[4]
+
+---
+
+## FINGERPRINTS (OBRIGATÓRIO)
+
+Criar hashes SHA-256:
+
+fp_keywords_norm
+fp_headlines_final_norm
+fp_descricao_final
+fp_bucket_map
+
+---
+
+## REGRAS DE RECONCILIAÇÃO
+
+R1 — EXISTÊNCIA 7/7
+Cada keyword_norm deve existir como headline espelho final.
+
+R2 — INTEGRIDADE DO ESPELHO
+headline espelho deve permanecer idêntica.
+
+R3 — CONTAGEM DE ESPELHOS
+Exatamente 7 espelhos únicos.
+
+R4 — DKI SAFE MODE
+
+* exatamente 1 headline DKI
+* formato {Keyword:PADRAO}
+* não substitui espelhos
+* fora do bucket literal
+
+R5 — BUCKET LAYOUT
+Bucket 1: 5 espelhos
+Bucket 2: 3 CTA + 1 espelho + 1 DKI
+Bucket 3: 4 provas + 1 espelho
+
+R6 — DESCRIÇÃO ANTI-GHOST
+Fingerprint da descrição final deve existir nas descrições geradas.
+
+R7 — INTEGRIDADE FINAL DA DESCRIÇÃO
+80 ≤ caracteres ≤ 92
+Termina com . ou !
+
+R8 — CALLOUTS
+7 obrigatórios, ≤25 caracteres.
+
+R9 — SITELINKS
+Cada sitelink com description1 e description2.
+
+R10 — HEADLINES (V12.6)
+* todas as 15 headlines devem ter **≤30 caracteres**
+* proibido truncar automaticamente (se estourar, regenerar)
+
+---
+
+## SEVERIDADE
+
+CRÍTICO: bloquear saída
+ALTA: reexecutar etapa
+MÉDIA: warning
+
+---
+
+## REEXECUÇÃO AUTOMÁTICA
+
+DESYNC_MIRROR_* → Passo 4.1/5
+DESYNC_DKI_VIOLATION → Passo 4.2/5
+DESYNC_BUCKET_LAYOUT → Passo 5
+DESYNC_DESCRIPTION_* → Passo 6
+DESYNC_CALLOUT_TRUNC → Passo 7
+DESYNC_SITELINK_INCOMPLETE → Passo 7
+
+---
+
+## STATUS FINAL
+
+PASS_RECONCILIATION
+FAIL_RECONCILIATION
+
+---
+
+# RESULTADO
+
+A Reconciliação elimina falsos negativos estruturais garantindo execução determinística do HARD MATCH + DKI SAFE MODE.
\ No newline at end of file
diff --git a/regras_promptV12.6.md b/regras_promptV12.6.md
new file mode 100644
index 0000000..bc7e964
--- /dev/null
+++ b/regras_promptV12.6.md
@@ -0,0 +1,334 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V12.6 — HEADLINE 30 + HARD MATCH + DKI SAFE MODE + RECONCILIATION ENGINE)
+
+OBJETIVO:
+Engine determinístico com canonização automática de keywords, espelhamento obrigatório de keywords em headlines
+(HARD MATCH 7/7) + 01 título com DKI em modo seguro (SEM substituir espelhos),
+e regeneração obrigatória de descrições (SEM substring), agora com camada adicional
+RECONCILIATION ENGINE (ANTI FALSE NEGATIVE) para validação estrutural final.
+
+Compatível com GAS v6 — ENGINE AUDITOR HARD MATCH.
+
+---
+
+# 🆕 V12.6 — ATUALIZAÇÃO (HEADLINE 30)
+
+* **Headlines (Títulos):** limite alterado de **25** para **30 caracteres**.
+* **Demais ativos** (keywords, callouts, sitelinks e descrições de sitelink): permanecem com **limite 25** (ou os limites já definidos nas seções próprias).
+
+---
+
+# 0. FLUXO GLOBAL (DETERMINISTIC EXECUTION)
+
+INÍCIO
+
+PARA cada GRUPO_DE_ANUNCIO (total = 3):
+
+```
+PASSO 1 → Gerar Keywords
+PASSO 1.1 → SANITIZAR E CANONIZAR KEYWORDS
+PASSO 2 → Normalizar Keywords
+PASSO 3 → Classificar Keywords por PRIORIDADE
+PASSO 4 → Selecionar Literais (ALGORITMO FIXO)
+PASSO 4.1 → Gerar HEADLINES ESPELHO (1 por keyword)
+PASSO 4.2 → Gerar 01 HEADLINE DKI (SAFE MODE)
+PASSO 5 → Montar LISTA FINAL DE TÍTULOS (15) com MIRROR ENFORCEMENT + DKI SAFE
+PASSO 6 → Gerar Descrições (REGENERAÇÃO OBRIGATÓRIA / SEM CORTE)
+PASSO 7 → Validar Limites (Headlines ≤30; demais conforme seção)
+PASSO 8 → Inserir Localização (fora dos espelhos)
+```
+
+FIM
+
+Depois:
+PASSO 9 → Negativas
+PASSO 10 → Extensões
+PASSO 11 → HARD LOCK VALIDATION
+PASSO 12 → RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+PASSO 13 → AUDITORIA GAS v6 (Opcional)
+
+RETORNAR apenas JSON estruturado.
+
+---
+
+# 1. PALAVRAS-CHAVE (ENGINE STEP)
+
+GERAR exatamente 7 keywords por grupo.
+
+REGRAS:
+
+* usar [Exata] e "Frase"
+* máximo 25 caracteres
+* keywords devem ser juridicamente tituláveis.
+
+---
+
+## SANITIZAÇÃO E CANONIZAÇÃO (OBRIGATÓRIA)
+
+ANTES de aceitar qualquer keyword:
+
+FUNÇÃO canonizar(keyword):
+
+```
+remover duplicações de delimitadores:
+ [[texto]] → texto
+
+converter para forma jurídica clara e titulável.
+```
+
+Exemplos obrigatórios:
+casamento fim → Fim do Casamento
+fim do casamento → Fim do Casamento
+fim pensão → Fim da Pensão
+fim da pensão → Fim da Pensão
+custo inventário → Custo Inventário
+
+SE keyword for vaga, truncada ou não titulável:
+DESCARTAR e gerar nova.
+
+---
+
+## ENGINE NORMALIZE
+
+FUNÇÃO normalizar(texto):
+
+```
+remover "[" "]" e '"'
+remover espaços duplicados
+aplicar Capitalização Inicial
+manter acentuação
+manter ordem
+NÃO alterar semântica
+```
+
+RETORNAR texto_normalizado
+
+---
+
+# 2. CLASSIFICAÇÃO DETERMINÍSTICA DE KEYWORDS
+
+PRIORIDADE 1 — COMERCIAL DIRETA
+
+* valor
+* custo
+* preço
+* regularização
+* revisão
+* cálculo
+* quanto custa
+* honorários
+
+PRIORIDADE 2 — CONTRATAÇÃO
+
+* advogado
+* ação
+* processo
+* consulta
+
+PRIORIDADE 3 — INFORMATIVA
+Demais termos.
+
+---
+
+# 3. SELEÇÃO LITERAL DETERMINÍSTICA
+
+FUNÇÃO selecionar_keywords_literais(lista_keywords):
+
+```
+ordenar por:
+ prioridade ASC
+ tamanho ASC
+ ordem original
+
+selecionar até 5.
+```
+
+---
+
+# 4. KEYWORD MIRROR HEADLINES — HARD MATCH (ENFORCEMENT)
+
+OBJETIVO:
+Garantir que TODAS as 7 keywords do grupo tenham ao menos 1 headline correspondente EXATO após normalização.
+
+REGRAS:
+
+1. Para cada keyword do grupo (7):
+ headline_espelho = normalizar(canonizar(keyword))
+
+2. O headline_espelho deve ser EXATO (após normalização).
+
+3. PROIBIÇÃO ABSOLUTA — "SP" NO ESPELHO
+
+4. MIRROR ENFORCEMENT:
+ A lista final de 15 títulos deve conter TODOS os 7 headlines_espelho.
+
+---
+
+# 4.2 HEADLINE DKI — SAFE MODE
+
+FORMATO:
+{Keyword:PADRAO_DO_GRUPO}
+
+REGRAS SAFE:
+
+* DKI nunca substitui espelhos
+* DKI fora do bucket literal
+* ≤30 caracteres
+
+---
+
+# 5. TÍTULOS — MONTAGEM DETERMINÍSTICA
+
+**Limite de caracteres (V12.6):** cada headline deve ter **≤30 caracteres**.
+
+Distribuição: 05 / 05 / 05
+
+Bucket 1: 5 espelhos
+Bucket 2: 3 CTA/Local + 1 espelho + 1 DKI
+Bucket 3: 4 provas + 1 espelho
+
+---
+
+# 6. DESCRIÇÕES — DETERMINISTIC TEXT ENGINE
+
+80 ≤ caracteres ≤ 92
+
+Proibido substring.
+Falhou validação → Regenerar do zero.
+
+---
+
+# 7. EXTENSÕES — HARD LOCK
+
+Sitelinks: 4 obrigatórios
+Callouts: 7 obrigatórios
+
+---
+
+# 8. POLÍTICAS EDITORIAIS
+
+Proibido:
+
+* Grátis
+* Gratuito
+* caixa alta total
+
+---
+
+# 9. HARD LOCK VALIDATION (FINAL)
+
+Validar integridade completa antes da saída.
+
+---
+
+# V12.5 — RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+
+OBJETIVO:
+Adicionar camada determinística de Reconciliação garantindo integridade total entre ENGINE e estado final.
+
+---
+
+## PRINCÍPIO
+
+ENGINE → cria HARD LOCK → RECONCILIATION → AUDITORIA
+
+---
+
+## SNAPSHOTS OBRIGATÓRIOS (ENGINE_STATE)
+
+keywords_raw[7]
+keywords_canon[7]
+keywords_norm[7]
+headlines_espelho_criados[7]
+headline_dki_criado[1]
+headlines_final[15]
+
+bucket_map_final
+
+descricao_final
+callouts_final[7]
+sitelinks_final[4]
+
+---
+
+## FINGERPRINTS (OBRIGATÓRIO)
+
+Criar hashes SHA-256:
+
+fp_keywords_norm
+fp_headlines_final_norm
+fp_descricao_final
+fp_bucket_map
+
+---
+
+## REGRAS DE RECONCILIAÇÃO
+
+R1 — EXISTÊNCIA 7/7
+Cada keyword_norm deve existir como headline espelho final.
+
+R2 — INTEGRIDADE DO ESPELHO
+headline espelho deve permanecer idêntica.
+
+R3 — CONTAGEM DE ESPELHOS
+Exatamente 7 espelhos únicos.
+
+R4 — DKI SAFE MODE
+
+* exatamente 1 headline DKI
+* formato {Keyword:PADRAO}
+* não substitui espelhos
+* fora do bucket literal
+
+R5 — BUCKET LAYOUT
+Bucket 1: 5 espelhos
+Bucket 2: 3 CTA + 1 espelho + 1 DKI
+Bucket 3: 4 provas + 1 espelho
+
+R6 — DESCRIÇÃO ANTI-GHOST
+Fingerprint da descrição final deve existir nas descrições geradas.
+
+R7 — INTEGRIDADE FINAL DA DESCRIÇÃO
+80 ≤ caracteres ≤ 92
+Termina com . ou !
+
+R8 — CALLOUTS
+7 obrigatórios, ≤25 caracteres.
+
+R9 — SITELINKS
+Cada sitelink com description1 e description2.
+
+R10 — HEADLINES (V12.6)
+* todas as 15 headlines devem ter **≤30 caracteres**
+* proibido truncar automaticamente (se estourar, regenerar)
+
+---
+
+## SEVERIDADE
+
+CRÍTICO: bloquear saída
+ALTA: reexecutar etapa
+MÉDIA: warning
+
+---
+
+## REEXECUÇÃO AUTOMÁTICA
+
+DESYNC_MIRROR_* → Passo 4.1/5
+DESYNC_DKI_VIOLATION → Passo 4.2/5
+DESYNC_BUCKET_LAYOUT → Passo 5
+DESYNC_DESCRIPTION_* → Passo 6
+DESYNC_CALLOUT_TRUNC → Passo 7
+DESYNC_SITELINK_INCOMPLETE → Passo 7
+
+---
+
+## STATUS FINAL
+
+PASS_RECONCILIATION
+FAIL_RECONCILIATION
+
+---
+
+# RESULTADO
+
+A Reconciliação elimina falsos negativos estruturais garantindo execução determinística do HARD MATCH + DKI SAFE MODE.
diff --git a/regras_prompt_old-1.md b/regras_prompt_old-1.md
new file mode 100644
index 0000000..e75b058
--- /dev/null
+++ b/regras_prompt_old-1.md
@@ -0,0 +1,84 @@
+```md
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V4 — Multi-Grupo)
+
+## 7. ESTRUTURA DA CAMPANHA (3 Grupos de Anúncio)
+A campanha DEVE ser dividida em exatamente 3 Grupos de Anúncio (Ad Groups) temáticos.
+Analise o conteúdo da Landing Page e identifique 3 ângulos/temas distintos para segmentar a campanha (ex: por serviço, por dor do cliente, por diferencial competitivo).
+Cada grupo deve ter um nome descritivo e conter seus próprios Keywords, Headlines e Descriptions (Regras 1, 3 e 4 aplicam-se POR GRUPO).
+As Negativas, Sitelinks e Callouts são compartilhados no nível da campanha (Regras 2, 5).
+
+---
+
+## 1. PALAVRAS-CHAVE (Fundo de Funil) — POR GRUPO
+Gere 7 termos por grupo (total ~21) com alta intenção de contratação. Use [Exata] e "Frase". Limite cada termo a no máximo 25 caracteres para viabilizar o uso nos títulos.
+
+---
+
+## 2. NEGATIVAS (Nível de Campanha)
+Gere 20 termos para filtrar curiosos e buscas por gratuidade (ex: pdf, curso, modelo, jurisprudência, tcc, concurso, como fazer, gratuito).
+
+---
+
+## 3. TÍTULOS (Headlines) - Regra Mista e Local — POR GRUPO
+Gere 15 títulos por grupo (máx. 30 caracteres cada) seguindo esta distribuição:
+
+### 🆕 ALTERAÇÃO — REGRA DE INTEGRIDADE (NÃO TRUNCAR)
+- Os títulos NÃO podem ser truncados, cortados ou exceder o limite.
+- Cada título deve ser gerado já dentro do limite de caracteres.
+- É proibido gerar textos maiores esperando corte automático do Google Ads.
+
+### 🆕 ALTERAÇÃO — REGRA LITERAL DE PALAVRA-CHAVE (OBRIGATÓRIA)
+- Devem existir exatamente 05 Títulos de Palavra-chave.
+- Cada título deve conter a palavra-chave da Regra 1 de forma INTEGRAL, IDÊNTICA e LITERAL.
+- O título deve ser EXATAMENTE igual à palavra-chave.
+- Proibido adicionar qualquer palavra antes ou depois.
+- Proibido hífen, pontuação extra, variações, pluralização ou mudança de capitalização.
+- Exemplo normativo:
+ Se a keyword for **“Advogado Divórcio”**, então o título deve ser **“Advogado Divórcio”**.
+
+### 🆕 ALTERAÇÃO — REGRA DE LOCALIZAÇÃO (SP / SÃO PAULO)
+- Sempre que possível, incluir referência geográfica “SP” ou “São Paulo” nos títulos de CTA/Local e nos títulos de Prova Social/Diferencial.
+- A inclusão da localização NÃO deve violar limites de caracteres.
+- A localização NÃO pode ser adicionada nos 05 Títulos de Palavra-chave literais.
+
+### Distribuição obrigatória:
+- 05 Títulos de Palavra-chave (regra literal acima).
+- 05 Títulos de CTA/Local: Focados em Chamadas para Ação e Relevância Local (ex: "Fale Conosco em SP", "Agende sua Consulta Hoje").
+- 05 Títulos de Prova Social/Diferencial: Utilize dados da LP (ex: "+10 anos de experiência", "Avaliação 5.0 no Google", "Atendimento 100% Online").
+
+---
+
+## 4. DESCRIÇÕES (Maximização de Espaço) — POR GRUPO
+Gere 4 descrições por grupo.
+
+### 🆕 ALTERAÇÃO — REGRA DE INTEGRIDADE (NÃO TRUNCAR)
+- As descrições NÃO podem ser truncadas ou exceder o limite.
+- Cada descrição deve ser criada já dentro do tamanho permitido.
+
+### 🆕 ALTERAÇÃO — REGRA DE LOCALIZAÇÃO (SP / SÃO PAULO)
+- Sempre que possível, incluir “SP” ou “São Paulo” nas descrições para reforçar relevância local.
+- A localização não pode comprometer a clareza, o limite mínimo (80) e máximo (90) de caracteres.
+
+Regra de Ouro:
+- Tente utilizar o mais próximo possível dos 90 caracteres (mínimo 80) para ocupar mais espaço visual.
+- Devem terminar obrigatoriamente com um ponto final ou exclamação.
+- Devem incluir a Proposta Única de Valor (UVP).
+
+---
+
+## 5. EXTENSÕES (Ativos — Nível de Campanha)
+- 4 Sitelinks: Título (até 25ch) e 2 linhas de descrição (até 35ch cada).
+- Sempre que possível, incluir “SP” ou “São Paulo” respeitando limites.
+- 6 Callouts: Frases de destaque (máx. 25ch) focadas em autoridade e agilidade, podendo incluir localização quando couber.
+
+---
+
+## 6. POLÍTICAS EDITORIAIS E PROIBIÇÕES
+- Inviolável: NUNCA utilize "Grátis", "Gratuito" ou sinônimos.
+- Formatação: Proibido CAIXA ALTA (exceto siglas como SP, OAB). Proibido pontuação excessiva (ex: !!!).
+- Estilo: Use tom profissional, empático e focado em solução.
+
+---
+
+RETORNO: APENAS o JSON estruturado, sem texto adicional.
+```
diff --git a/regras_prompt_old-2-boa.md b/regras_prompt_old-2-boa.md
new file mode 100644
index 0000000..e285480
--- /dev/null
+++ b/regras_prompt_old-2-boa.md
@@ -0,0 +1,137 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V8 — HARD MODE IA / AUDITORIA BLINDADA + MATCH STRICT)
+
+## 7. ESTRUTURA DA CAMPANHA (3 Grupos de Anúncio)
+A campanha DEVE ser dividida em exatamente 3 Grupos de Anúncio (Ad Groups) temáticos.
+Analise o conteúdo da Landing Page e identifique 3 ângulos/temas distintos para segmentar a campanha.
+Cada grupo deve ter nome descritivo e conter seus próprios Keywords, Headlines e Descriptions (Regras 1, 3 e 4 aplicam-se POR GRUPO).
+Negativas, Sitelinks e Callouts são compartilhados no nível da campanha (Regras 2 e 5).
+
+---
+
+## 1. PALAVRAS-CHAVE (Fundo de Funil) — POR GRUPO
+Gere exatamente 7 termos por grupo (total ~21).
+Use [Exata] e "Frase".
+Máximo: 25 caracteres por keyword.
+
+### 🔒 HARD RULE — BASE PARA MATCH LITERAL
+- As keywords devem permitir gerar obrigatoriamente 05 títulos literais.
+- Evitar keywords longas que impeçam títulos ≤30 caracteres.
+- A lista deve conter pelo menos 5 keywords compatíveis com títulos literais.
+
+### 🆕 MATCH STRICT — NORMALIZAÇÃO (AUDITORIA)
+- Keywords podem conter sinais como [ ], " ".
+- Para gerar Títulos Literais, remover apenas os delimitadores [ ] ou " ".
+- O texto interno deve permanecer IDÊNTICO.
+Exemplo:
+Keyword: "[advogado divórcio]"
+Título literal permitido: "Advogado Divórcio"
+
+---
+
+## 2. NEGATIVAS (Nível de Campanha)
+Gere 20 termos para filtrar curiosos e buscas por gratuidade (ex: pdf, curso, modelo, jurisprudência, tcc, concurso, como fazer, gratuito).
+
+---
+
+## 3. TÍTULOS (Headlines) - Regra Mista e Local — POR GRUPO
+
+### 🚫 HARD LIMIT — TAMANHO
+- Máximo absoluto: 30 caracteres.
+- Não gerar textos maiores esperando corte automático.
+- Títulos truncados são proibidos.
+
+### 🔒 HARD MATCH — TÍTULOS LITERAIS (REGRA DE FERRO)
+- Devem existir exatamente 05 títulos literais.
+- Cada título deve ser IDÊNTICO ao TEXTO INTERNO da keyword da Regra 1.
+- Ignorar apenas delimitadores [ ] ou " ".
+- Copiar literal após normalização.
+
+PROIBIDO:
+- adicionar palavras
+- remover palavras
+- mudar ordem
+- alterar acentuação
+- alterar significado
+- acrescentar SP se não estiver na keyword
+
+Exemplo:
+Keyword: "[advogado inventário]"
+Título literal obrigatório: "Advogado Inventário"
+
+Keyword: "\"advogado guarda\""
+Título literal obrigatório: "Advogado Guarda"
+
+### 📍 REGRA DE LOCALIZAÇÃO (SP / SÃO PAULO)
+- Inserir “SP” ou “São Paulo” sempre que possível nos títulos de CTA/Local e Prova Social.
+- NÃO alterar títulos literais.
+
+### 📊 DISTRIBUIÇÃO OBRIGATÓRIA
+1. 05 Títulos Literais (match exato normalizado)
+2. 05 Títulos CTA/Local
+3. 05 Títulos Prova Social/Diferencial
+
+---
+
+## 4. DESCRIÇÕES (Maximização de Espaço) — POR GRUPO
+
+### 📏 HARD RANGE — AUDITORIA
+- Cada descrição deve ter entre 80 e 90 caracteres.
+- Menos de 80 = erro.
+- Mais de 90 = erro.
+
+### 🚫 HARD INTEGRIDADE
+- Não gerar textos maiores esperando truncamento.
+- A descrição já deve nascer dentro do limite.
+
+### 🆕 REGRA ANTI-SUBAPROVEITAMENTO (AUDITORIA)
+- Descrições abaixo de 80 caracteres devem ser automaticamente expandidas antes da entrega final.
+
+### 📍 LOCALIZAÇÃO
+- Sempre que possível incluir “SP” ou “São Paulo”.
+- Não comprometer clareza.
+
+### 🎯 CONTEÚDO
+- Incluir UVP.
+- Incluir CTA claro.
+- Final obrigatório com "." ou "!".
+
+---
+
+## 5. EXTENSÕES (Ativos — Nível de Campanha)
+
+### SITELINKS
+- 4 Sitelinks obrigatórios.
+- Título até 25 caracteres.
+- 2 linhas de descrição até 35 caracteres cada.
+- Priorizar CTAs fortes.
+- Sempre que possível incluir SP/São Paulo.
+
+### CALLOUTS
+- 6 Callouts (máx. 25 caracteres).
+- Incluir obrigatoriamente 01 callout com diferencial competitivo único.
+
+---
+
+## 6. POLÍTICAS EDITORIAIS E PROIBIÇÕES
+- Inviolável: NUNCA utilizar "Grátis", "Gratuito" ou sinônimos.
+- Proibido CAIXA ALTA (exceto SP, OAB).
+- Proibido pontuação excessiva.
+- Linguagem profissional e orientada à solução.
+
+---
+
+## 🔐 CHECKLIST AUTOMÁTICO (HARD MODE — EXECUÇÃO OBRIGATÓRIA)
+Antes de gerar o JSON final, validar:
+
+- Existem exatamente 15 títulos por grupo?
+- Existem exatamente 05 títulos literais idênticos às keywords (normalizadas)?
+- Nenhum título excede 30 caracteres?
+- Todas as descrições possuem entre 80 e 90 caracteres?
+- Pelo menos uma menção a SP/São Paulo nos textos não literais?
+- Existe 01 callout diferencial?
+
+Se qualquer item falhar → regenerar automaticamente antes da entrega.
+
+---
+
+RETORNO: APENAS o JSON estruturado, sem texto adicional.
diff --git a/regras_prompt_old-3-boa.md b/regras_prompt_old-3-boa.md
new file mode 100644
index 0000000..ffc4cfa
--- /dev/null
+++ b/regras_prompt_old-3-boa.md
@@ -0,0 +1,233 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V11.2 — ENGINE HARD LOCK / COMMERCIAL PRIORITY)
+
+OBJETIVO:
+Versão HARD LOCK com prioridade comercial.
+Blindar geração contra variações semânticas e erros de auditoria.
+Capitalização inicial permitida.
+Auditoria aceita igualdade semântica após normalização.
+
+Nenhuma regra anterior foi removida — apenas reforçada.
+
+---
+
+# 0. FLUXO GLOBAL (ENGINE EXECUTION)
+
+INÍCIO
+
+PARA cada GRUPO_DE_ANUNCIO (total = 3):
+
+ PASSO 1 → Gerar Keywords
+ PASSO 2 → Normalizar Keywords (ENGINE NORMALIZE)
+ PASSO 3 → Selecionar Keywords Literais (COMMERCIAL PRIORITY)
+ PASSO 4 → Gerar Títulos Literais (HARD LOCK)
+ PASSO 5 → Gerar Títulos CTA/Local
+ PASSO 6 → Gerar Títulos Prova Social
+ PASSO 7 → Validar Limite de Caracteres
+ PASSO 8 → Gerar Descrições
+ PASSO 9 → Validar RANGE ESTRITO
+ PASSO 10 → Inserir Localização
+
+FIM
+
+Depois:
+ PASSO 11 → Negativas
+ PASSO 12 → Extensões
+ PASSO 13 → HARD LOCK VALIDATION
+
+RETORNAR apenas JSON estruturado.
+
+---
+
+# 1. PALAVRAS-CHAVE (ENGINE STEP)
+
+GERAR exatamente 7 keywords por grupo.
+
+REGRAS:
+- usar [Exata] e "Frase"
+- máximo 25 caracteres
+- garantir mínimo 5 keywords utilizáveis como título literal
+
+### ENGINE NORMALIZE (MATCH BASE)
+
+FUNÇÃO normalizar(keyword):
+
+ remover "[" "]" e '"'
+ aplicar Capitalização Inicial
+ manter acentuação
+ manter ordem original
+ NÃO alterar semântica
+
+RETORNAR keyword_normalizada
+
+HARD LOCK TOKEN:
+
+ literal_token = hash(keyword_normalizada)
+
+---
+
+# 3. TÍTULOS — ENGINE HARD LOCK
+
+## 🆕 PRIORIDADE COMERCIAL (NOVO)
+
+ANTES de selecionar títulos literais:
+
+Identificar keywords com intenção direta:
+
+- valor
+- custo
+- preço
+- regularização
+- revisão
+- cálculo
+- quanto custa
+
+SE existirem:
+
+ essas keywords DEVEM entrar primeiro nas 5 literais obrigatórias.
+
+Somente após preencher prioridades comerciais,
+completar restante das literais.
+
+---
+
+## PASSO A — TÍTULOS LITERAIS (MATCH BLOQUEADO)
+
+VAR lista_literais = selecionar_keywords_prioritarias()
+
+PARA cada keyword:
+
+ titulo = normalizar(keyword)
+ token_titulo = hash(titulo)
+
+ SE token_titulo != literal_token:
+ BLOQUEAR alteração
+ REGERAR
+
+ SE tamanho(titulo) > 30:
+ substituir keyword
+
+ adicionar titulo
+
+PERMITIDO:
+- Capitalização inicial
+
+PROIBIDO:
+- adicionar SP
+- alterar palavras
+- alterar ordem
+- reescrever semântica
+
+---
+
+## PASSO B — TÍTULOS CTA/LOCAL
+- incluir SP ou São Paulo quando possível
+- máximo 30 caracteres
+
+## PASSO C — TÍTULOS PROVA SOCIAL
+- usar autoridade da LP
+- máximo 30 caracteres
+
+TOTAL:
+15 títulos por grupo.
+
+---
+
+# 4. DESCRIÇÕES — ENGINE HARD LOCK
+
+### RANGE DEFINITIVO
+- mínimo absoluto: 80
+- alvo ideal: 85–90
+- máximo: 90
+
+FUNÇÃO validar_descricao(texto):
+
+🆕 ENGINE HARD LOCK — BLOQUEIO DE SUBSTRING
+
+É PROIBIDO usar:
+
+texto[:limite]
+substring
+slice
+corte parcial
+
+SE tamanho(texto) > 90:
+
+ REESCREVER FRASE COMPLETA
+ NUNCA cortar caracteres finais
+
+SE detectar palavra incompleta:
+
+ REGERAR descrição automaticamente
+
+ SE tamanho < 80:
+ expandir automaticamente
+
+ SE tamanho entre 80 e 84:
+ expandir até ≥85
+
+🆕 ANTI-TRUNCAMENTO FINAL
+- descrição deve terminar com "." ou "!"
+- nunca finalizar com palavra incompleta
+- nunca terminar com espaço vazio
+
+CONTEÚDO:
+- incluir UVP
+- incluir CTA
+- incluir SP ou São Paulo quando possível
+
+---
+
+# 5. EXTENSÕES (ATIVOS — NÍVEL CAMPANHA)
+
+SITELINKS:
+- 4 obrigatórios
+- título ≤25 caracteres
+- 2 descrições ≤35 caracteres
+
+CALLOUTS:
+- 7 obrigatórios
+- ≤25 caracteres
+- incluir 01 diferencial competitivo único
+
+---
+
+# 6. POLÍTICAS EDITORIAIS
+
+PROIBIDO:
+- "Grátis"
+- "Gratuito"
+- caixa alta total (exceto SP, OAB)
+- pontuação excessiva
+
+PERMITIDO:
+- Capitalização Inicial em títulos literais.
+
+---
+
+# 7. HARD LOCK VALIDATION
+
+ANTES DE ENTREGAR:
+
+VALIDAR:
+
+- existem 3 grupos?
+- existem 15 títulos por grupo?
+- existem 05 títulos literais?
+- keywords comerciais estão dentro das literais?
+- hash(titulo_literal) == literal_token ?
+- nenhum título > 30 caracteres?
+- descrições entre 80 e 90 caracteres?
+- nenhuma substring detectada?
+- existe SP/São Paulo fora das literais?
+- existem 7 callouts?
+
+SE falhar:
+
+ REEXECUTAR bloco inválido
+
+---
+
+RETORNO FINAL:
+APENAS JSON estruturado.
+SEM explicações.
+SEM texto adicional.
diff --git a/regras_prompt_old.md b/regras_prompt_old.md
new file mode 100644
index 0000000..3198acb
--- /dev/null
+++ b/regras_prompt_old.md
@@ -0,0 +1,35 @@
+# REGRAS CONSOLIDADAS PARA GOOGLE ADS (V4 — Multi-Grupo)
+
+## 7. ESTRUTURA DA CAMPANHA (3 Grupos de Anúncio)
+A campanha DEVE ser dividida em exatamente 3 Grupos de Anúncio (Ad Groups) temáticos.
+Analise o conteúdo da Landing Page e identifique 3 ângulos/temas distintos para segmentar a campanha (ex: por serviço, por dor do cliente, por diferencial competitivo).
+Cada grupo deve ter um nome descritivo e conter seus próprios Keywords, Headlines e Descriptions (Regras 1, 3 e 4 aplicam-se POR GRUPO).
+As Negativas, Sitelinks e Callouts são compartilhados no nível da campanha (Regras 2, 5).
+
+## 1. PALAVRAS-CHAVE (Fundo de Funil) — POR GRUPO
+Gere 7 termos por grupo (total ~21) com alta intenção de contratação. Use [Exata] e "Frase". Limite cada termo a no máximo 25 caracteres para viabilizar o uso nos títulos.
+
+## 2. NEGATIVAS (Nível de Campanha)
+Gere 20 termos para filtrar curiosos e buscas por gratuidade (ex: pdf, curso, modelo, jurisprudência, tcc, concurso, como fazer, gratuito).
+
+Gere 15 títulos por grupo (máx. 30 caracteres). Atenção à distribuição:
+
+## 3. TÍTULOS (Headlines) - Regra Mista e Local — POR GRUPO
+Gere 15 títulos por grupo (máx. 30 caracteres cada) seguindo esta distribuição:
+- 05 Títulos de Palavra-chave: Devem conter obrigatoriamente a palavra-chave da Regra 1 de forma INTEGRAL e IDÊNTICA.
+- 05 Títulos de CTA/Local: Focados em Chamadas para Ação e Relevância Local (ex: "Fale Conosco em SP", "Agende sua Consulta Hoje").
+- 05 Títulos de Prova Social/Diferencial: Utilize dados da LP (ex: "+10 anos de experiência", "Avaliação 5.0 no Google", "Atendimento 100% Online").
+
+## 4. DESCRIÇÕES (Maximização de Espaço) — POR GRUPO
+Gere 4 descrições por grupo. Regra de Ouro: Tente utilizar o mais próximo possível dos 90 caracteres (mínimo 80) para ocupar mais espaço visual. Devem terminar obrigatoriamente com um ponto final ou exclamação e incluir a Proposta Única de Valor (UVP).
+
+## 5. EXTENSÕES (Ativos — Nível de Campanha)
+- 4 Sitelinks: Título (até 25ch) e 2 linhas de descrição (até 35ch cada).
+- 6 Callouts: Frases de destaque (máx. 25ch) focadas em autoridade e agilidade.
+
+## 6. POLÍTICAS EDITORIAIS E PROIBIÇÕES
+- Inviolável: NUNCA utilize "Grátis", "Gratuito" ou sinônimos.
+- Formatação: Proibido CAIXA ALTA (exceto siglas como SP, OAB). Proibido pontuação excessiva (ex: !!!).
+- Estilo: Use tom profissional, empático e focado em solução.
+
+RETORNO: APENAS o JSON estruturado, sem texto adicional.
diff --git a/regras_prompt_v12.4-v12_5.md b/regras_prompt_v12.4-v12_5.md
new file mode 100644
index 0000000..dbc7072
--- /dev/null
+++ b/regras_prompt_v12.4-v12_5.md
@@ -0,0 +1,215 @@
+# GOOGLE ADS --- ENGINE HARD MATCH V12.5 (VERSÃO CONSOLIDADA)
+
+Atualizado em: 14/02/2026
+
+------------------------------------------------------------------------
+
+## 🎯 OBJETIVO
+
+Estrutura definitiva seguindo:
+
+- HARD MATCH 7/7
+- DKI SAFE MODE
+- RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+- GAS v6 --- ENGINE AUDITOR HARD MATCH
+
+Sem truncamentos, sem mutações de espelho e com validação estrutural
+final.
+
+------------------------------------------------------------------------
+
+# 📦 CAMPANHA GOOGLE ADS --- ESTRUTURA FINAL
+
+------------------------------------------------------------------------
+
+## 🔵 GRUPO 1 --- DIVÓRCIO E SEPARAÇÃO
+
+### Keywords
+
+- \[divórcio online\]
+- "advogado divórcio"
+- \[separação judicial\]
+- "divórcio consensual"
+- \[divórcio litigioso\]
+- "fim do casamento"
+- \[custo divórcio\]
+
+### Headlines Espelho (HARD MATCH)
+
+Divórcio Online\
+Advogado Divórcio\
+Separação Judicial\
+Divórcio Consensual\
+Divórcio Litigioso\
+Fim do Casamento\
+Custo Divórcio
+
+### Headlines Finais (15)
+
+Divórcio Online\
+Advogado Divórcio\
+Separação Judicial\
+Divórcio Consensual\
+Divórcio Litigioso\
+Fale com um Especialista\
+Atendimento Online em SP\
+Solução Rápida e Segura\
+Fim do Casamento\
+{Keyword:Divórcio em SP}\
+Sigilo e Discrição Total\
+Estratégia Clara e Direta\
+Acompanhamento do Início\
+Menos Conflito, Mais Paz\
+Custo Divórcio
+
+### Descrições
+
+Divórcio online com orientação clara e sigilo. Resolva com agilidade e
+segurança. Fale agora!\
+Advogado de família para divórcio consensual ou litigioso. Atendimento
+online e discreto. Consulte!\
+Separação judicial com estratégia e rapidez. Proteja seus direitos e
+evite desgaste. Agende uma análise!
+
+------------------------------------------------------------------------
+
+## 🟢 GRUPO 2 --- PENSÃO ALIMENTÍCIA E GUARDA
+
+### Keywords
+
+- \[pensão alimentícia\]
+- "advogado guarda filhos"
+- \[revisão pensão\]
+- "guarda compartilhada"
+- \[ação de alimentos\]
+- "fim da pensão"
+- \[valor pensão\]
+
+### Headlines Espelho (HARD MATCH)
+
+Pensão Alimentícia\
+Advogado Guarda Filhos\
+Revisão Pensão\
+Guarda Compartilhada\
+Ação de Alimentos\
+Fim da Pensão\
+Valor Pensão
+
+### Headlines Finais (15)
+
+Pensão Alimentícia\
+Advogado Guarda Filhos\
+Revisão Pensão\
+Guarda Compartilhada\
+Ação de Alimentos\
+Defina a Guarda com Calma\
+Atendimento Online em SP\
+Fale com um Especialista\
+Fim da Pensão\
+{Keyword:Pensão em SP}\
+Soluções Justas e Claras\
+Foco no Bem-Estar\
+Sigilo e Orientação Total\
+Acompanhamento Completo\
+Valor Pensão
+
+### Descrições
+
+Defina guarda e pensão com equilíbrio e segurança. Atendimento online
+com foco no bem-estar. Consulte!\
+Revisão de pensão e ação de alimentos com orientação objetiva. Evite
+conflitos e ganhe clareza. Fale!\
+Guarda compartilhada com apoio jurídico completo. Proteja seus filhos e
+seus direitos. Agende avaliação!
+
+------------------------------------------------------------------------
+
+## 🟣 GRUPO 3 --- INVENTÁRIO E UNIÃO ESTÁVEL
+
+### Keywords
+
+- \[inventário judicial\]
+- "advogado inventário"
+- \[união estável\]
+- "dissolução união estável"
+- \[inventário extrajudicial\]
+- "herança e bens"
+- \[custo inventário\]
+
+### Headlines Espelho (HARD MATCH)
+
+Inventário Judicial\
+Advogado Inventário\
+União Estável\
+Dissolução União Estável\
+Inventário Extrajudicial\
+Herança e Bens\
+Custo Inventário
+
+### Headlines Finais (15)
+
+Inventário Judicial\
+Advogado Inventário\
+União Estável\
+Dissolução União Estável\
+Inventário Extrajudicial\
+Regularize com Segurança\
+Atendimento Online em SP\
+Agilidade na Partilha\
+Herança e Bens\
+{Keyword:Inventário em SP}\
+Partilha Justa e Rápida\
+Documentação Sem Estresse\
+Orientação do Início ao Fim\
+Segurança Jurídica Total\
+Custo Inventário
+
+### Descrições
+
+Inventário judicial ou extrajudicial com orientação clara. Partilha
+segura e ágil. Fale com especialista!\
+União estável e dissolução com segurança jurídica. Atendimento online e
+acompanhamento completo. Consulte!\
+Herança e bens com estratégia e transparência. Resolva o inventário sem
+desgaste. Agende uma análise!
+
+------------------------------------------------------------------------
+
+# 🔗 SITELINKS
+
+Divórcio e Separação\
+- Soluções rápidas e seguras.\
+- Atendimento online e sigiloso.
+
+Pensão e Guarda\
+- Foco no bem-estar dos filhos.\
+- Defina guarda e pensão com clareza.
+
+Inventário e União\
+- Partilha justa e ágil.\
+- Regularize sua união com segurança.
+
+Fale com Especialista\
+- Análise inicial e orientação clara.\
+- Converse online com um advogado.
+
+------------------------------------------------------------------------
+
+# 📢 CALLOUTS
+
+Atendimento 100% Online\
+Sigilo Garantido\
+Resposta Rápida\
+Especialistas em Família\
+Estratégia Clara\
+Acompanhamento Completo\
+Foco em Solução Justa
+
+------------------------------------------------------------------------
+
+# ✅ STATUS
+
+PASS_RECONCILIATION\
+HARD MATCH 7/7 ATIVO\
+ANTI TRUNCAMENTO VALIDADO\
+COMPATÍVEL COM GAS v6
diff --git a/regras_prompt_v12_5.md b/regras_prompt_v12_5.md
new file mode 100644
index 0000000..3700d43
--- /dev/null
+++ b/regras_prompt_v12_5.md
@@ -0,0 +1,185 @@
+# REGRAS CONSOLIDADAS --- Google Ads
+
+## V12.5 --- RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+
+OBJETIVO: Adicionar uma camada determinística de Reconciliação ao
+pipeline V12.4 (HARD MATCH + DKI SAFE MODE), garantindo integridade
+total entre o estado criado pelo ENGINE e o estado final entregue.
+
+Compatível com: - HARD MATCH - DKI SAFE MODE - GAS v6 --- ENGINE AUDITOR
+HARD MATCH
+
+------------------------------------------------------------------------
+
+# 0. PRINCÍPIO
+
+A Reconciliação NÃO substitui auditoria. Ela valida consistência entre
+fases.
+
+ENGINE → cria HARD LOCK → valida regras RECONCILIATION → valida
+integridade estrutural AUDITORIA → valida qualidade final
+
+------------------------------------------------------------------------
+
+# 1. FLUXO GLOBAL ATUALIZADO
+
+PASSO 1 → Keywords PASSO 2 → Canonização PASSO 3 → Normalização PASSO 4
+→ Headlines Espelho PASSO 4.2 → DKI SAFE MODE PASSO 5 → Montagem 15
+Títulos PASSO 6 → Descrições PASSO 7 → Extensões PASSO 8 → HARD LOCK
+VALIDATION PASSO 12 → RECONCILIATION ENGINE (NOVO) PASSO 13 → AUDITORIA
+GAS v6 (Opcional)
+
+------------------------------------------------------------------------
+
+# 2. SNAPSHOTS OBRIGATÓRIOS (ENGINE_STATE)
+
+keywords_raw\[7\] keywords_canon\[7\] keywords_norm\[7\]
+headlines_espelho_criados\[7\] headline_dki_criado\[1\]
+headlines_final\[15\]
+
+bucket_map_final: - bucket1_literal\[5\] - bucket2_cta_local\[5\] -
+bucket3_prova\[5\]
+
+descricoes_geradas\[\] descricao_final callouts_final\[7\]
+sitelinks_final\[4\]
+
+------------------------------------------------------------------------
+
+# 3. FINGERPRINTS (OBRIGATÓRIO)
+
+Criar hashes SHA-256:
+
+fp_keywords_norm fp_headlines_final_norm fp_descricao_final
+fp_bucket_map
+
+Objetivo: Detectar alterações invisíveis após montagem.
+
+------------------------------------------------------------------------
+
+# 4. REGRAS DE RECONCILIAÇÃO
+
+## R1 --- EXISTÊNCIA 7/7 (CRÍTICO)
+
+Cada keyword_norm deve existir como headline espelho final.
+
+Se faltar: DESYNC_MIRROR_MISSING
+
+------------------------------------------------------------------------
+
+## R2 --- INTEGRIDADE DO ESPELHO (CRÍTICO)
+
+headline espelho deve ser idêntica após normalização.
+
+Proibido: - adicionar palavras - trocar ordem - criar variações
+
+Erro: DESYNC_MIRROR_MUTATED
+
+------------------------------------------------------------------------
+
+## R3 --- CONTAGEM DE ESPELHOS (CRÍTICO)
+
+Deve haver exatamente 7 espelhos únicos.
+
+Erro: DESYNC_MIRROR_COUNT
+
+------------------------------------------------------------------------
+
+## R4 --- DKI SAFE MODE (CRÍTICO)
+
+Regras obrigatórias:
+
+- existe exatamente 1 headline DKI
+- formato {Keyword:PADRAO}
+- DKI não substitui espelhos
+- DKI não está no Bucket Literal
+
+Erro: DESYNC_DKI_VIOLATION
+
+------------------------------------------------------------------------
+
+## R5 --- BUCKET LAYOUT (ALTA)
+
+Distribuição obrigatória:
+
+Bucket 1: 5 espelhos
+
+Bucket 2: 3 CTA/Local 1 espelho (6º) 1 DKI
+
+Bucket 3: 4 provas 1 espelho (7º)
+
+Erro: DESYNC_BUCKET_LAYOUT
+
+------------------------------------------------------------------------
+
+## R6 --- DESCRIÇÃO ANTI-GHOST (CRÍTICO)
+
+Fingerprint da descrição final deve existir dentro das descrições
+geradas.
+
+Proibido: - editar manualmente - cortar string
+
+Erro: DESYNC_DESCRIPTION_GHOST_EDIT
+
+------------------------------------------------------------------------
+
+## R7 --- INTEGRIDADE FINAL DA DESCRIÇÃO (CRÍTICO)
+
+Revalidar:
+
+80 ≤ caracteres ≤ 92 termina com "." ou "!" não termina com espaço não
+termina com palavra incompleta
+
+Erro: DESYNC_DESCRIPTION_INVALID_FINAL
+
+------------------------------------------------------------------------
+
+## R8 --- CALLOUTS (ALTA)
+
+- 7 obrigatórios
+- ≤25 caracteres
+- sem palavra incompleta
+- sem substring
+
+Erro: DESYNC_CALLOUT_TRUNC
+
+------------------------------------------------------------------------
+
+## R9 --- SITELINKS (MÉDIA)
+
+Cada sitelink deve possuir:
+
+description1 description2
+
+Erro: DESYNC_SITELINK_INCOMPLETE
+
+------------------------------------------------------------------------
+
+# 5. SEVERIDADE
+
+CRÍTICO: bloquear saída do grupo reexecutar bloco inválido
+
+ALTA: reexecutar etapa específica
+
+MÉDIA: warning
+
+------------------------------------------------------------------------
+
+# 6. REEXECUÇÃO AUTOMÁTICA
+
+DESYNC_MIRROR\_\* → Passo 4.1 / 5 DESYNC_DKI_VIOLATION → Passo 4.2 / 5
+DESYNC_BUCKET_LAYOUT → Passo 5 DESYNC_DESCRIPTION\_\* → Passo 6
+DESYNC_CALLOUT_TRUNC → Passo 7 DESYNC_SITELINK_INCOMPLETE → Passo 7
+
+------------------------------------------------------------------------
+
+# 7. STATUS FINAL
+
+PASS_RECONCILIATION FAIL_RECONCILIATION
+
+------------------------------------------------------------------------
+
+# RESULTADO
+
+A Reconciliação elimina falsos negativos estruturais, garantindo que o
+pipeline HARD MATCH + DKI SAFE MODE permaneça determinístico do início
+ao fim.
diff --git a/regras_prompt_v12_6_1.md b/regras_prompt_v12_6_1.md
new file mode 100644
index 0000000..d2d75ef
--- /dev/null
+++ b/regras_prompt_v12_6_1.md
@@ -0,0 +1,224 @@
+# REGRAS CONSOLIDADAS --- Google Ads
+
+## V12.6.1 --- UNIFIED ENGINE CORE COMPACT
+
+(HARD MATCH + DKI SAFE MODE + RECONCILIATION ENGINE)
+
+OBJETIVO: Versão compacta e determinística do ENGINE CORE. Unifica
+criação, montagem, validação estrutural e reconciliação
+anti-falso-negativo em um único fluxo otimizado.
+
+Compatível com: - HARD MATCH 7/7 - DKI SAFE MODE - Anti-Truncamento -
+Reconciliação Estrutural
+
+Auditoria (GAS v6) permanece EXTERNA.
+
+------------------------------------------------------------------------
+
+# 0. PRINCÍPIOS DO ENGINE
+
+- Execução determinística
+- Espelhamento obrigatório
+- DKI controlado
+- Zero substring
+- Reconciliação integrada
+
+------------------------------------------------------------------------
+
+# 1. FLUXO GLOBAL
+
+PASSO 1 → Gerar Keywords (7 por grupo) PASSO 2 → Canonizar Keywords
+PASSO 3 → Normalizar Keywords PASSO 4 → Classificar Prioridade PASSO 5 →
+Gerar Headlines Espelho (7/7) PASSO 6 → Gerar 01 DKI SAFE MODE PASSO 7 →
+Montar 15 Títulos (05/05/05) PASSO 8 → Gerar Descrições (NO TRUNC
+ENGINE) PASSO 9 → Gerar Extensões PASSO 10 → HARD LOCK VALIDATION PASSO
+11 → RECONCILIATION ENGINE
+
+RETORNAR apenas JSON estruturado.
+
+------------------------------------------------------------------------
+
+# 2. CANONIZAÇÃO
+
+FUNÇÃO canonizar(keyword):
+
+- remover duplicação de delimitadores
+- transformar em forma jurídica titulável
+- manter acentuação
+- manter ordem semântica
+
+Keyword inválida → DESCARTAR.
+
+------------------------------------------------------------------------
+
+# 3. NORMALIZAÇÃO
+
+FUNÇÃO normalizar(texto):
+
+- remover "\[" "\]" e '"'
+- remover espaços duplicados
+- permitir Capitalização Inicial
+- manter acentuação
+- manter ordem
+
+Uso exclusivo para comparação.
+
+------------------------------------------------------------------------
+
+# 4. PRIORIDADE
+
+PRIORIDADE 1: valor, custo, preço, revisão, cálculo
+
+PRIORIDADE 2: advogado, ação, processo, consulta
+
+PRIORIDADE 3: demais termos
+
+Ordenação:
+
+prioridade ASC tamanho ASC ordem original
+
+------------------------------------------------------------------------
+
+# 5. HARD MATCH --- ESPELHOS
+
+- 7 keywords → 7 headlines espelho obrigatórios
+- headline = normalizar(canonizar(keyword))
+
+PROIBIDO:
+
+- adicionar palavras
+- alterar ordem
+- sinônimos
+- pluralização
+
+Se keyword não contém SP → espelho não pode conter SP.
+
+------------------------------------------------------------------------
+
+# 6. DKI SAFE MODE
+
+Formato obrigatório:
+
+{Keyword:PADRAO_DO_GRUPO}
+
+REGRAS:
+
+- exatamente 1 DKI
+- ≤30 caracteres
+- não substitui espelhos
+- não entra no Bucket Literal
+- sem termos proibidos
+
+------------------------------------------------------------------------
+
+# 7. TÍTULOS --- MONTAGEM 15 FIXOS
+
+Bucket 1 --- Literal (5) → 5 primeiros espelhos
+
+Bucket 2 --- CTA/Local (5) → 3 CTAs → 1 espelho (6º) → 1 DKI
+
+Bucket 3 --- Prova (5) → 4 provas → 1 espelho (7º)
+
+------------------------------------------------------------------------
+
+# 8. DESCRIÇÕES --- NO TRUNC ENGINE
+
+RANGE:
+
+80 ≤ caracteres ≤ 92 Ideal: 85--90
+
+PROCESSO:
+
+1) gerar descrição completa
+2) validar
+3) se inválida → REGERAR DO ZERO
+
+PROIBIDO:
+
+- substring
+- slice
+- corte parcial
+
+Descrição válida se:
+
+- termina com "." ou "!"
+- não termina com espaço
+- não termina com palavra incompleta
+
+------------------------------------------------------------------------
+
+# 9. EXTENSÕES
+
+SITELINKS:
+
+- 4 obrigatórios
+- título ≤25
+- description1 obrigatório
+- description2 obrigatório
+
+CALLOUTS:
+
+- 7 obrigatórios
+- ≤25 caracteres
+- sem palavra incompleta
+- reescrever se exceder (nunca cortar)
+
+------------------------------------------------------------------------
+
+# 10. HARD LOCK VALIDATION
+
+VALIDAR:
+
+- 3 grupos
+- 7 keywords por grupo
+- 7 espelhos presentes
+- 15 títulos totais
+- DKI único
+- buckets 05/05/05
+- descrições válidas
+- callouts completos
+
+Falha → REEXECUTAR BLOCO.
+
+------------------------------------------------------------------------
+
+# 11. RECONCILIATION ENGINE (ANTI FALSE NEGATIVE)
+
+R1 --- EXISTÊNCIA 7/7 (CRÍTICO) keyword_norm deve existir nos títulos
+finais. Erro: DESYNC_MIRROR_MISSING
+
+R2 --- INTEGRIDADE DO ESPELHO (CRÍTICO) headline espelho idêntica após
+normalização. Erro: DESYNC_MIRROR_MUTATED
+
+R3 --- CONTAGEM (CRÍTICO) exatamente 7 espelhos únicos. Erro:
+DESYNC_MIRROR_COUNT
+
+R4 --- DKI SAFE MODE (CRÍTICO) 1 DKI, fora do Bucket Literal. Erro:
+DESYNC_DKI_VIOLATION
+
+R5 --- BUCKET LAYOUT (ALTA) 05/05/05 obrigatório. Erro:
+DESYNC_BUCKET_LAYOUT
+
+R6 --- DESCRIÇÃO ANTI-GHOST (CRÍTICO) descrição final deve ter sido
+gerada pelo engine. Erro: DESYNC_DESCRIPTION_GHOST_EDIT
+
+R7 --- INTEGRIDADE FINAL (CRÍTICO) validar novamente range e
+finalização. Erro: DESYNC_DESCRIPTION_INVALID_FINAL
+
+R8 --- CALLOUTS (ALTA) 7 callouts completos. Erro: DESYNC_CALLOUT_TRUNC
+
+R9 --- SITELINKS (MÉDIA) description1 e description2 obrigatórios. Erro:
+DESYNC_SITELINK_INCOMPLETE
+
+------------------------------------------------------------------------
+
+# 12. SEVERIDADE
+
+CRÍTICO → bloquear saída ALTA → reexecutar etapa MÉDIA → warning
+
+------------------------------------------------------------------------
+
+# RESULTADO
+
+ENGINE CORE compacto, sem duplicação de regras, com HARD MATCH + DKI
+SAFE + RECONCILIATION totalmente integrados.
diff --git a/src/Regras_old.md b/src/Regras_old.md
new file mode 100644
index 0000000..a1e7518
--- /dev/null
+++ b/src/Regras_old.md
@@ -0,0 +1,20 @@
+REGRAS CONSOLIDADAS PARA GOOGLE ADS (V3)
+1. PALAVRAS-CHAVE (Fundo de Funil):
+Gere 20 termos com alta intenção de contratação. Use [Exata] e "Frase". Limite cada termo a no máximo 25 caracteres para viabilizar o uso nos títulos.
+2. NEGATIVAS:
+Gere 20 termos para filtrar curiosos e buscas por gratuidade (ex: pdf, curso, modelo, jurisprudência, tcc, concurso, como fazer, gratuito).
+3. TÍTULOS (Headlines) - Regra Mista e Local:
+Gere 15 títulos (máx. 30 caracteres cada) seguindo esta distribuição:
+05 Títulos de Palavra-chave: Devem conter obrigatoriamente a palavra-chave da Regra 1 de forma INTEGRAL e IDÊNTICA.
+05 Títulos de CTA/Local: Focados em Chamadas para Ação e Relevância Local (ex: "Fale Conosco em SP", "Agende sua Consulta Hoje").
+05 Títulos de Prova Social/Diferencial: Utilize dados da LP (ex: "+10 anos de experiência", "Avaliação 5.0 no Google", "Atendimento 100% Online").
+4. DESCRIÇÕES (Maximização de Espaço):
+Gere 4 descrições. Regra de Ouro: Tente utilizar o mais próximo possível dos 90 caracteres (mínimo 80) para ocupar mais espaço visual. Devem terminar obrigatoriamente com um ponto final ou exclamação e incluir a Proposta Única de Valor (UVP).
+5. EXTENSÕES (Ativos):
+4 Sitelinks: Título (até 25ch) e 2 linhas de descrição (até 35ch cada).
+6 Callouts: Frases de destaque (máx. 25ch) focadas em autoridade e agilidade.
+6. POLÍTICAS EDITORIAIS E PROIBIÇÕES:
+Inviolável: NUNCA utilize "Grátis", "Gratuito" ou sinônimos.
+Formatação: Proibido CAIXA ALTA (exceto siglas como SP, OAB). Proibido pontuação excessiva (ex: !!!).
+Estilo: Use tom profissional, empático e focado em solução.
+RETORNO: APENAS o JSON estruturado, sem texto adicional."""
\ No newline at end of file
diff --git a/src/__pycache__/ai_generator.cpython-310.pyc b/src/__pycache__/ai_generator.cpython-310.pyc
index b9bb40c..b56b99d 100644
Binary files a/src/__pycache__/ai_generator.cpython-310.pyc and b/src/__pycache__/ai_generator.cpython-310.pyc differ
diff --git a/src/__pycache__/auditor.cpython-310.pyc b/src/__pycache__/auditor.cpython-310.pyc
new file mode 100644
index 0000000..4cf79ce
Binary files /dev/null and b/src/__pycache__/auditor.cpython-310.pyc differ
diff --git a/src/__pycache__/exporter.cpython-310.pyc b/src/__pycache__/exporter.cpython-310.pyc
index 351e60c..5b63f9c 100644
Binary files a/src/__pycache__/exporter.cpython-310.pyc and b/src/__pycache__/exporter.cpython-310.pyc differ
diff --git a/src/ai_generator.py b/src/ai_generator.py
index b957d24..e7efac1 100644
--- a/src/ai_generator.py
+++ b/src/ai_generator.py
@@ -10,9 +10,13 @@ import json
import os
import time
import re
+from pathlib import Path
from openai import OpenAI
from google import genai
+# ─── Caminho do arquivo de regras ─────────────────────────────────
+_RULES_FILE = Path(__file__).resolve().parent.parent / "regras_prompt.md"
+
# ─── Modelos disponíveis por provider ─────────────────────────────
MODELS = {
@@ -78,7 +82,7 @@ def _call_openai(system_prompt: str, user_prompt: str, model: str) -> str:
{"role": "user", "content": user_prompt},
],
temperature=0.7,
- max_tokens=4000,
+ max_tokens=8000,
)
return response.choices[0].message.content.strip()
@@ -103,7 +107,7 @@ def _call_gemini(system_prompt: str, user_prompt: str, model: str, max_retries:
config=genai.types.GenerateContentConfig(
system_instruction=system_prompt,
temperature=0.7,
- max_output_tokens=4000,
+ max_output_tokens=8000,
),
)
return response.text.strip()
@@ -150,9 +154,21 @@ def _build_system_prompt() -> str:
)
+def _load_rules() -> str:
+ """Carrega as regras do arquivo externo regras_prompt.md."""
+ if _RULES_FILE.exists():
+ return _RULES_FILE.read_text(encoding="utf-8")
+ raise FileNotFoundError(f"Arquivo de regras não encontrado: {_RULES_FILE}")
+
+
def _build_user_prompt(lp_content: str) -> str:
"""Constrói o prompt do usuário para geração de ativos."""
+ rules = _load_rules()
+
return f"""Com base no conteúdo desta Landing Page, gere ativos completos para uma campanha de Google Ads.
+A campanha DEVE conter exatamente 3 Grupos de Anúncio (Ad Groups) temáticos.
+Analise a LP e identifique 3 ângulos/temas distintos. Cada grupo deve ter seus próprios keywords, headlines e descriptions.
+As negativas, sitelinks e callouts são compartilhados no nível da campanha.
=== CONTEÚDO DA LANDING PAGE ===
{lp_content}
@@ -161,20 +177,49 @@ def _build_user_prompt(lp_content: str) -> str:
Gere o seguinte em formato JSON:
{{
- "keywords": [
- {{"keyword": "exemplo de palavra-chave", "match_type": "Exact"}},
- {{"keyword": "outro exemplo", "match_type": "Phrase"}}
+ "ad_groups": [
+ {{
+ "name": "Nome descritivo do tema do grupo 1",
+ "keywords": [
+ {{"keyword": "exemplo de palavra-chave", "match_type": "Exact"}},
+ {{"keyword": "outro exemplo", "match_type": "Phrase"}}
+ ],
+ "headlines": [
+ "Título com até 30 caracteres"
+ ],
+ "descriptions": [
+ "Descrição com até 90 caracteres que destaca benefícios e inclui CTA"
+ ]
+ }},
+ {{
+ "name": "Nome descritivo do tema do grupo 2",
+ "keywords": [
+ {{"keyword": "palavra-chave do grupo 2", "match_type": "Exact"}}
+ ],
+ "headlines": [
+ "Título do grupo 2"
+ ],
+ "descriptions": [
+ "Descrição do grupo 2 com CTA e benefícios"
+ ]
+ }},
+ {{
+ "name": "Nome descritivo do tema do grupo 3",
+ "keywords": [
+ {{"keyword": "palavra-chave do grupo 3", "match_type": "Phrase"}}
+ ],
+ "headlines": [
+ "Título do grupo 3"
+ ],
+ "descriptions": [
+ "Descrição do grupo 3 com CTA e benefícios"
+ ]
+ }}
],
"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"}}
],
@@ -183,44 +228,56 @@ Gere o seguinte em formato JSON:
]
}}
-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."""
+{rules}"""
def _validate_and_normalize(data: dict) -> dict:
- """Valida e normaliza os dados retornados pela IA."""
+ """Valida e normaliza os dados retornados pela IA (estrutura multi-grupo)."""
result = {
- "keywords": [],
+ "ad_groups": [],
"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"})
+ # Ad Groups — cada grupo contém keywords, headlines e descriptions
+ for group in data.get("ad_groups", []):
+ if not isinstance(group, dict):
+ continue
- # Negative Keywords
+ normalized_group = {
+ "name": group.get("name", "Grupo sem nome"),
+ "keywords": [],
+ "headlines": [],
+ "descriptions": [],
+ }
+
+ # Keywords do grupo
+ for kw in group.get("keywords", []):
+ if isinstance(kw, dict) and "keyword" in kw:
+ normalized_group["keywords"].append({
+ "keyword": kw["keyword"],
+ "match_type": kw.get("match_type", "Phrase"),
+ })
+ elif isinstance(kw, str):
+ normalized_group["keywords"].append({"keyword": kw, "match_type": "Phrase"})
+
+ # Headlines do grupo - garantir limite de 30 chars
+ for h in group.get("headlines", []):
+ if isinstance(h, str) and len(h) <= 30:
+ normalized_group["headlines"].append(h)
+ elif isinstance(h, str):
+ normalized_group["headlines"].append(h[:30])
+
+ # Descriptions do grupo - garantir limite de 90 chars
+ for d in group.get("descriptions", []):
+ if isinstance(d, str) and len(d) <= 90:
+ normalized_group["descriptions"].append(d)
+ elif isinstance(d, str):
+ normalized_group["descriptions"].append(d[:90])
+
+ result["ad_groups"].append(normalized_group)
+
+ # Negative Keywords (nível de campanha)
for nkw in data.get("negative_keywords", []):
if isinstance(nkw, dict) and "keyword" in nkw:
result["negative_keywords"].append({
@@ -230,21 +287,7 @@ def _validate_and_normalize(data: dict) -> dict:
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
+ # Sitelinks (nível de campanha)
for sl in data.get("sitelinks", []):
if isinstance(sl, dict) and "title" in sl:
result["sitelinks"].append({
@@ -253,7 +296,7 @@ def _validate_and_normalize(data: dict) -> dict:
"description2": sl.get("description2", "")[:35],
})
- # Callouts
+ # Callouts (nível de campanha)
for c in data.get("callouts", []):
if isinstance(c, str) and len(c) <= 25:
result["callouts"].append(c)
diff --git a/src/auditor.py b/src/auditor.py
new file mode 100644
index 0000000..503ccb8
--- /dev/null
+++ b/src/auditor.py
@@ -0,0 +1,134 @@
+"""
+Módulo de Auditoria - Google Ads Scorecard (GAS).
+
+Recebe o JSON de uma campanha gerada e avalia a qualidade dos ativos
+com base em critérios ponderados (Keyword Match, Compliance, Espaço,
+Gatilhos e Ativos). Também gera uma simulação visual do anúncio (CTR).
+Suporta múltiplos providers de IA (OpenAI e Google Gemini).
+"""
+
+import json
+from pathlib import Path
+from src.ai_generator import _call_openai, _call_gemini, _clean_json_response, MODELS
+
+# ─── Caminho do arquivo de regras de auditoria ────────────────────
+_RULES_FILE = Path(__file__).resolve().parent.parent / "regras_auditoria.md"
+
+
+# ─── Prompt de auditoria ──────────────────────────────────────────
+
+_AUDIT_SYSTEM_PROMPT = (
+ "Você é um Auditor Sênior de Google Ads com mais de 15 anos de experiência. "
+ "Seu papel é analisar JSONs de campanhas e atribuir uma nota rigorosa de 0 a 10, "
+ "avaliando cada critério individualmente. Seja específico nas falhas e nas "
+ "sugestões de correção. Responda SEMPRE em formato JSON válido, "
+ "sem markdown, sem blocos de código. Apenas o JSON puro."
+)
+
+
+def _load_rules() -> str:
+ """Carrega as regras de auditoria do arquivo externo regras_auditoria.md."""
+ if _RULES_FILE.exists():
+ return _RULES_FILE.read_text(encoding="utf-8")
+ raise FileNotFoundError(f"Arquivo de regras de auditoria não encontrado: {_RULES_FILE}")
+
+
+def _build_audit_user_prompt(campaign_json: str) -> str:
+ """Constrói o prompt do usuário para auditoria da campanha (multi-grupo)."""
+ rules = _load_rules()
+
+ return f"""Aja como Auditor de Google Ads. Analise o JSON de campanha abaixo e gere uma avaliação completa.
+A campanha possui estrutura multi-grupo (ad_groups). Avalie TODOS os grupos e gere uma nota consolidada.
+
+=== JSON DA CAMPANHA ===
+{campaign_json}
+=== FIM DO JSON ===
+
+{rules}"""
+
+
+def audit_campaign(
+ campaign_json: str,
+ provider: str = "OpenAI",
+ model: str = "gpt-4o-mini",
+) -> tuple[dict, dict]:
+ """
+ Audita uma campanha Google Ads a partir do JSON dos ativos.
+
+ Args:
+ campaign_json: JSON string da campanha a ser auditada.
+ provider: Provider de IA ("OpenAI" ou "Gemini").
+ model: Modelo a ser usado (deve ser compatível com o provider).
+
+ Returns:
+ Tupla com:
+ - audit_result: Dicionário com nota_final, criterios, resumo, simulacao.
+ - prompts: Dicionário com system_prompt e user_prompt usados.
+ """
+ user_prompt = _build_audit_user_prompt(campaign_json)
+
+ prompts = {
+ "system_prompt": _AUDIT_SYSTEM_PROMPT,
+ "user_prompt": user_prompt,
+ }
+
+ if provider == "OpenAI":
+ raw_response = _call_openai(_AUDIT_SYSTEM_PROMPT, user_prompt, model)
+ elif provider == "Gemini":
+ raw_response = _call_gemini(_AUDIT_SYSTEM_PROMPT, user_prompt, model)
+ else:
+ raise ValueError(f"Provider não suportado: {provider}")
+
+ raw_response = _clean_json_response(raw_response)
+ result = json.loads(raw_response)
+ audit_result = _validate_audit_result(result)
+
+ return audit_result, prompts
+
+
+def _validate_audit_result(data: dict) -> dict:
+ """Valida e normaliza o resultado da auditoria retornado pela IA (multi-grupo)."""
+ result = {
+ "nota_final": float(data.get("nota_final", 0)),
+ "criterios": [],
+ "resumo": data.get("resumo", "Sem resumo disponível."),
+ "simulacoes_anuncio": [],
+ }
+
+ for criterio in data.get("criterios", []):
+ if isinstance(criterio, dict) and "nome" in criterio:
+ result["criterios"].append({
+ "nome": criterio.get("nome", ""),
+ "peso": float(criterio.get("peso", 0)),
+ "nota": float(criterio.get("nota", 0)),
+ "nota_maxima": float(criterio.get("nota_maxima", 0)),
+ "falhas": criterio.get("falhas", []),
+ "sugestoes": criterio.get("sugestoes", []),
+ })
+
+ # Simulações de anúncio — uma por grupo
+ for sim in data.get("simulacoes_anuncio", []):
+ if isinstance(sim, dict):
+ result["simulacoes_anuncio"].append({
+ "grupo": sim.get("grupo", ""),
+ "titulo_linha_1": sim.get("titulo_linha_1", ""),
+ "titulo_linha_2": sim.get("titulo_linha_2", ""),
+ "titulo_linha_3": sim.get("titulo_linha_3", ""),
+ "url_display": sim.get("url_display", "www.exemplo.com.br"),
+ "descricao": sim.get("descricao", ""),
+ })
+
+ # Fallback: compatibilidade com formato antigo (simulacao_anuncio singular)
+ if not result["simulacoes_anuncio"] and "simulacao_anuncio" in data:
+ sim = data["simulacao_anuncio"]
+ if isinstance(sim, dict):
+ result["simulacoes_anuncio"].append({
+ "grupo": sim.get("grupo", "Grupo único"),
+ "titulo_linha_1": sim.get("titulo_linha_1", ""),
+ "titulo_linha_2": sim.get("titulo_linha_2", ""),
+ "titulo_linha_3": sim.get("titulo_linha_3", ""),
+ "url_display": sim.get("url_display", "www.exemplo.com.br"),
+ "descricao": sim.get("descricao", ""),
+ })
+
+ return result
diff --git a/src/exporter.py b/src/exporter.py
index c6ddae4..4cb9064 100644
--- a/src/exporter.py
+++ b/src/exporter.py
@@ -1,36 +1,38 @@
"""
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.
+Recebe os ativos gerados pela IA (estrutura multi-grupo) 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."""
+def create_keywords_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
+ """Cria DataFrame de palavras-chave no formato Google Ads Editor (multi-grupo)."""
rows = []
- for kw in assets.get("keywords", []):
- match_type = kw.get("match_type", "Phrase")
- keyword = kw.get("keyword", "")
+ for group in assets.get("ad_groups", []):
+ group_name = group.get("name", "Grupo")
+ for kw in group.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
+ # 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",
- })
+ rows.append({
+ "Campaign": campaign_name,
+ "Ad Group": group_name,
+ "Keyword": formatted_kw,
+ "Match Type": match_type,
+ "Status": "Enabled",
+ })
return pd.DataFrame(rows)
@@ -49,26 +51,31 @@ def create_negative_keywords_df(assets: dict, campaign_name: str = "Campanha LP"
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", [])
+def create_ads_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
+ """Cria DataFrame dos anúncios responsivos (RSA) no formato Google Ads Editor (multi-grupo)."""
+ rows = []
+ for group in assets.get("ad_groups", []):
+ group_name = group.get("name", "Grupo")
+ headlines = group.get("headlines", [])
+ descriptions = group.get("descriptions", [])
- row = {
- "Campaign": campaign_name,
- "Ad Group": ad_group,
- "Ad Type": "Responsive Search Ad",
- }
+ row = {
+ "Campaign": campaign_name,
+ "Ad Group": group_name,
+ "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é 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
+ # Preencher até 4 descrições
+ for i, d in enumerate(descriptions[:4], start=1):
+ row[f"Description {i}"] = d
- return pd.DataFrame([row])
+ rows.append(row)
+
+ return pd.DataFrame(rows) if rows else pd.DataFrame()
def create_sitelinks_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
@@ -97,7 +104,7 @@ def create_callouts_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.D
return pd.DataFrame(rows)
-def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> BytesIO:
+def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP") -> BytesIO:
"""
Exporta todos os ativos em um único arquivo Excel com múltiplas abas.
@@ -107,7 +114,7 @@ def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_gro
output = BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
- kw_df = create_keywords_df(assets, campaign_name, ad_group)
+ kw_df = create_keywords_df(assets, campaign_name)
if not kw_df.empty:
kw_df.to_excel(writer, sheet_name="Keywords", index=False)
@@ -115,7 +122,7 @@ def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_gro
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)
+ ads_df = create_ads_df(assets, campaign_name)
if not ads_df.empty:
ads_df.to_excel(writer, sheet_name="Ads RSA", index=False)
@@ -131,7 +138,7 @@ def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_gro
return output
-def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> dict[str, str]:
+def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP") -> dict[str, str]:
"""
Exporta todos os ativos como strings CSV separadas.
@@ -140,7 +147,7 @@ def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP", ad_group
"""
csvs = {}
- kw_df = create_keywords_df(assets, campaign_name, ad_group)
+ kw_df = create_keywords_df(assets, campaign_name)
if not kw_df.empty:
csvs["keywords"] = kw_df.to_csv(index=False)
@@ -148,7 +155,7 @@ def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP", ad_group
if not nkw_df.empty:
csvs["negative_keywords"] = nkw_df.to_csv(index=False)
- ads_df = create_ads_df(assets, campaign_name, ad_group)
+ ads_df = create_ads_df(assets, campaign_name)
if not ads_df.empty:
csvs["ads"] = ads_df.to_csv(index=False)