No artigo anterior, contei a história de por que criei o Decodifica.Tech. Agora vou abrir o capô e mostrar como funciona — a arquitetura, o fluxo de dados, as tecnologias e as decisões de design.

Se você já pensou em construir um agregador de conteúdo, uma newsletter automatizada, ou qualquer pipeline que envolva coleta → processamento → publicação, este artigo é para você.

Visão geral: 7 etapas, 4 minutos

A pipeline completa roda em ~4 minutos e tem 7 etapas:

242 feeds RSS → Ingest → Cluster → Rank → Synthesize → Podcast → Generate → Publish

Cada etapa é um módulo Python independente que pode rodar sozinho. O orquestrador (run_pipeline.py) chama cada módulo em sequência e lida com os dados intermediários via arquivos JSON.

Não tem fila de mensagens. Não tem microserviços. Não tem banco de dados. É um script Python linear que roda uma vez por dia via GitHub Actions.

Vou detalhar cada etapa.

Etapa 1: Ingest — Coletando 242 feeds RSS

O módulo ingest.py é o ponto de entrada. Ele lê um arquivo sources.yaml com 242 feeds RSS curados manualmente e puxa os artigos das últimas 24 horas.

O arquivo de fontes

As fontes são organizadas por categoria e tier:

sources:
  - name: Hacker News (Best)
    url: https://hnrss.org/best
    category: news
    tier: 1

  - name: Netflix Tech Blog
    url: https://netflixtechblog.com/feed
    category: dev
    tier: 1
  • Categorias: dev, cloud, devops, security, ai, news
  • Tier 1: fontes de alta frequência e alto sinal (sempre coletadas)
  • Tier 2: fontes boas mas menos frequentes

A curadoria manual das fontes é a parte mais importante da pipeline. Gastei mais tempo montando essa lista do que escrevendo código. Inclui blogs de engenharia (Netflix, Stripe, Cloudflare, GitHub), portais (Hacker News, Ars Technica, InfoQ), especialistas individuais (Julia Evans, Martin Fowler, Simon Willison), e fontes de segurança (Krebs, Schneier, Troy Hunt).

Filtragem de relevância

Fontes de mídia geral (TechCrunch, The Verge) publicam de tudo — incluindo artigos sobre moda, receitas e horóscopo. Para manter o foco em tecnologia, o ingest aplica dois filtros:

  1. Blocklist — Regex que descarta artigos com termos como “promo code”, “horoscope”, “fashion trend”
  2. Tech keywords — Se a fonte é generalista, o artigo precisa conter termos técnicos (software, API, kubernetes, cybersecurity, etc.) para ser aceito

Blogs de engenharia e vendors já são curados na origem — não precisam de filtragem.

Deduplicação via hash

Cada artigo recebe um hash baseado na URL normalizada. Artigos já vistos nos últimos 3 dias são descartados. Isso evita que crons de backup ou re-runs processem os mesmos artigos.

O output é um arquivo JSON: data/raw/articles_2026-05-10.json

Etapa 2: Cluster — Agrupando histórias com TF-IDF

Esse é o coração da pipeline. O problema: se o Hacker News, TechCrunch, Ars Technica e mais 15 blogs cobriram o mesmo anúncio do Kubernetes, eu quero 1 história, não 18.

O módulo cluster.py usa TF-IDF + similaridade de cosseno + Union-Find para agrupar artigos sobre o mesmo assunto.

Como funciona

# 1. Cria vetores TF-IDF a partir de título + resumo
texts = [f"{a['title']} {a.get('summary', '')}" for a in articles]
vectorizer = TfidfVectorizer(
    max_features=5000,
    stop_words="english",
    ngram_range=(1, 2),  # unigramas e bigramas
)
tfidf_matrix = vectorizer.fit_transform(texts)

# 2. Calcula similaridade entre todos os pares
sim_matrix = cosine_similarity(tfidf_matrix)

# 3. Agrupa artigos com similaridade > 0.35
# usando Union-Find para clusters transitivos

Por que Union-Find? Se o artigo A é similar a B, e B é similar a C, então A, B e C são sobre a mesma história — mesmo que A e C não sejam diretamente similares. Union-Find captura essa transitividade de forma eficiente.

O threshold de 0.35 foi calibrado empiricamente. Abaixo disso, artigos diferentes começam a ser agrupados. Acima, duplicatas escapam.

Para cada cluster, o sistema seleciona o melhor representante (fonte mais autoritativa) e mantém as demais como fontes secundárias.

O output: data/clustered/stories_2026-05-10.json — cada story tem um artigo principal e uma lista de fontes que cobriram o mesmo assunto.

Vou detalhar essa etapa em um artigo dedicado na próxima semana: Clusterização de notícias com TF-IDF e similaridade de cosseno.

Etapa 3: Rank — Seleção dinâmica por score

O módulo rank.py transforma uma lista de stories clusterizadas em duas listas: histórias principais (análise profunda) e radar rápido (resumo breve).

O sistema de pontuação

Cada story recebe um score composto por:

FatorPesoExemplo
Autoridade da fonte0-10Netflix Tech Blog = 9, blog pessoal = 3
CoberturaVariável15 fontes cobriram = +15 pontos
RecênciaBonusPublicado há <6h = +3
CategoriaAjusteBalanceia para não ter 100% IA/ML

As fontes mais autoritativas são mapeadas manualmente:

AUTHORITY_SCORES = {
    "Netflix Tech Blog": 9,
    "Cloudflare Blog": 9,
    "Stripe Engineering": 9,
    "Krebs on Security": 9,
    "OpenAI Blog": 9,
    "Martin Fowler": 9,
    "Ars Technica": 9,
    "Hacker News (Best)": 10,
    # ...
}

Seleção dinâmica (não fixa)

Em vez de “top 10 do dia”, o sistema usa thresholds:

  • Score ≥ 20 → história principal (análise em 2-3 parágrafos)
  • Score ≥ 10 → radar rápido (1-2 frases)
  • Mínimo de 8 histórias principais garantidas (mesmo em dias calmos)
  • Máximo de 20 para não sobrecarregar o post

Isso significa que em dias movimentados (lançamento de produto, conferência), o post pode ter 15+ histórias. Em domingos calmos, 8-10. O conteúdo se adapta ao dia, sem forçar histórias fracas.

Deduplicação cross-dia

O ranking também consulta um histórico de publicação dos últimos 3 dias. Se uma história já apareceu ontem, ela é filtrada — mesmo que ainda esteja sendo coberta. Isso evita repetições que cansam o leitor.

Etapa 4: Synthesize — IA que sintetiza, não traduz

O módulo synthesize.py é onde a mágica acontece. Ele recebe as listas rankeadas e usa Azure OpenAI (GPT-4o-mini) para gerar o post em Português Brasileiro.

Duas chamadas separadas

A síntese é feita em dois passes:

  1. Main stories — Prompt detalhado que pede 2-3 parágrafos por história, com contexto e implicações
  2. Radar Rápido — Prompt enxuto que pede 1-2 frases por história

Separar em duas chamadas melhora a qualidade. Um prompt único com 40+ histórias degrada o output.

O system prompt

O prompt de sistema é bem específico:

“Você é um editor de tecnologia experiente que escreve para profissionais de TI brasileiros. Seu trabalho é sintetizar as principais notícias de tecnologia do dia. Escreva em Português Brasileiro natural e fluente — NÃO traduza do inglês. Para cada história, SINTETIZE — não resuma. Extraia insights, explique implicações, contextualize.”

A diferença entre traduzir, resumir e sintetizar é crucial:

  • Traduzir = mudar idioma (pior resultado, soa artificial)
  • Resumir = encurtar o original (perde contexto)
  • Sintetizar = extrair o que importa e recontextualizar (o que queremos)

Fallback chain

Se o Azure OpenAI falhar, o sistema tenta Groq (Llama 3.3 70B) como fallback. Na prática, nunca precisei — mas a resiliência está lá.

Etapa 5: Podcast — Três vozes neurais da Azure

O módulo podcast.py gera um podcast diário com três apresentadores fictícios:

  • Anna — Âncora principal, conduz o programa (voz: pt-BR-Thalita:DragonHDLatestNeural)
  • Maria — Contexto técnico e opiniões fortes (voz: pt-BR-FranciscaNeural)
  • Pedro — Humor, analogias e perguntas provocativas (voz: pt-BR-Macerio:DragonHDLatestNeural)

O fluxo

  1. GPT-4o-mini gera um roteiro de diálogo a partir da síntese do dia
  2. O roteiro é convertido em SSML (Speech Synthesis Markup Language) com tags de voz para cada speaker
  3. Azure AI Speech renderiza o áudio com vozes DragonHD — a última geração de TTS neural da Microsoft
  4. Um jingle de abertura é concatenado via pydub
  5. O MP3 final vai para site/static/audio/

As vozes DragonHD são impressionantemente naturais. Incluem respirações, pausas e variação tonal que fazem o podcast soar como uma conversa real. Se você nunca ouviu, escute um episódio — a qualidade surpreende.

O sistema também lida com pronúncia de termos em inglês (Kubernetes, Docker, GitHub) usando tags <lang xml:lang="en-US"> no SSML, para que as vozes brasileiras pronunciem termos técnicos corretamente.

Vou dedicar um artigo inteiro a essa etapa: Gerando podcast com vozes neurais da Azure AI Speech.

Etapa 6: Generate — Montando o post Hugo

O módulo generate_post.py combina tudo em um post Markdown compatível com Hugo:

  • Front matter com título (a manchete gerada pela IA), data, slug, categorias e tags extraídas automaticamente
  • Player de áudio embutido se o podcast foi gerado
  • Seções de síntese com links para fontes originais
  • Radar Rápido com resumos breves
  • Tags inteligentes — entidades extraídas do conteúdo (tecnologias, empresas, produtos)

O resultado é um arquivo como site/content/posts/2026-05-10-daily.md.

Etapa 7: Publish — Zero intervenção manual

A publicação é 100% automatizada via GitHub Actions:

on:
  schedule:
    - cron: '0 10 * * *'    # 07:00 BRT (primário)
    - cron: '45 10 * * *'   # 07:45 BRT (backup)

O workflow faz tudo:

  1. Checkout do repo
  2. Instala Python 3.12 + dependências (com cache de pip)
  3. Roda a pipeline completa
  4. Builda o site com Hugo
  5. Commit e push do novo post
  6. Deploy para Cloudflare Pages
  7. Espera 30s para propagação do CDN
  8. Publica o carrossel no Instagram via Graph API

Tem um cron de backup 45 minutos depois, caso o primeiro não dispare. GitHub Actions tem um SLA informal de que crons podem atrasar até 15 minutos ou simplesmente não rodar — o backup é uma rede de segurança.

O deploy também tem guard de idempotência: se o post do dia já existe, a pipeline pula em vez de sobrescrever. Útil para re-runs manuais e para o cron de backup não duplicar posts.

A stack completa

ComponenteTecnologiaCusto
Coleta de feedsPython 3.12, feedparser, requests$0.00
Clusterizaçãoscikit-learn (TF-IDF + cosine similarity)$0.00
Síntese IAAzure OpenAI (GPT-4o-mini)~$0.10/mês
Podcast TTSAzure AI Speech (DragonHD)~$0.08/mês
Imagens InstagramHugging Face (FLUX.1-schnell)$0.00
Gerador de siteHugo + PaperMod$0.00
HospedagemCloudflare Pages$0.00
CI/CDGitHub Actions$0.00
Total~$0.20/mês

O custo absurdamente baixo vem de uma combinação de free tiers generosos e escolhas deliberadas:

  • GPT-4o-mini em vez de GPT-4o: 10x mais barato, qualidade suficiente para síntese
  • Cloudflare Pages em vez de Vercel/Netlify: bandwidth ilimitado no free tier
  • Hugo em vez de Next.js: site estático puro, sem server-side rendering
  • GitHub Actions em vez de servidor dedicado: 2000 min/mês grátis no free tier

O que eu faria diferente

Se começasse hoje, mudaria duas coisas:

  1. Testes automatizados desde o dia 1. A pipeline cresceu organicamente e os testes foram ficando para depois. Hoje, a validação é manual — funciona porque é simples, mas não escala.

  2. Embeddings em vez de TF-IDF para clusterização. TF-IDF funciona bem para títulos em inglês, mas embeddings semânticos (como os do text-embedding-3-small da OpenAI) capturariam melhor a similaridade conceitual. O custo adicional seria mínimo.

Reproduza o projeto

O código é open-source: github.com/ricmmartins/decodifica.tech

Para rodar localmente:

cd pipeline
pip install -r requirements.txt
python run_pipeline.py

Você vai precisar de:

  • Chave da Azure OpenAI (para síntese)
  • Chave da Azure Speech (para podcast — opcional)
  • Token do Hugging Face (para imagens do Instagram — opcional)

A coleta e clusterização funcionam sem nenhuma API key.

Próximo artigo

Na próxima semana, vou mergulhar fundo na clusterização: como TF-IDF funciona, por que escolhi similaridade de cosseno, como o Union-Find agrupa transitivamente, e exemplos reais do pipeline com dados do dia.


Tem perguntas sobre a arquitetura? Comenta aqui embaixo ou manda um email. E se curtiu, compartilha com aquele colega que também quer automatizar algo.