Um gol é marcado numa partida da Copa do Mundo. Sessenta segundos depois, uma push notification chega no celular de um torcedor — com o nome do jogador, o minuto, e uma linha de narração. Nenhum jornalista escreveu. Ninguém apertou enviar. O sistema simplesmente soube.

Esse é o produto que herdei. O que construí veio depois.


O contexto

Durante os anos em que trabalhei numa empresa de jornalismo, tive a chance de trabalhar com uma plataforma esportiva que produzia cobertura de partidas ao vivo havia mais de oito anos. A infraestrutura era real e testada em batalha: um pipeline que ingere eventos de futebol, os roteia por múltiplos serviços, e entrega narrações para usuários mobile e web quase em tempo real. É anterior à minha passagem por lá, está em produção, e funciona.

A Copa do Mundo só acontece a cada quatro anos. Para uma organização de notícias, isso significa uma janela concentrada de demanda intensa de leitores, prazos de publicação apertados, e todo jogo exigindo cobertura simultaneamente. Quando a Copa 2026 chegou, a pergunta não era se tínhamos dados — tínhamos. A pergunta era se conseguiríamos fazer mais com eles.

Minha contribuição foi o segundo produto: conteúdo editorial automatizado, construído em cima da infraestrutura de dados que já existia. Dois produtos, mas origens bem diferentes — um eu herdei, outro eu construí.


Produto 1: o sistema de narração em tempo real

Esse sistema tem mais de oito anos. Quando um evento de partida acontece — um gol, um cartão, uma substituição — ele entrega uma narração aos usuários em menos de 60 segundos.

Esse teto de 60 segundos é estrutural, não uma decisão de design que tomamos. O provedor de dados não oferece webhooks ou eventos push. É uma API de polling. A cada minuto, um serviço de ingestão chama seus endpoints, compara o que voltou com o que já sabe, e dispara eventos para qualquer coisa nova. A latência já vem embutida.

A partir daí, os dados viajam por cinco serviços Java, cada um publicando no Google Cloud Pub/Sub. As narrações chegam no Firebase Firestore, e de lá o SDK do Firebase as entrega aos clientes em tempo real — sem WebSocket, sem SSE, sem long-polling customizado do nosso lado.

A camada de entrega em tempo real é inteiramente problema do Firebase. O backend é uma sequência de consumidores de Pub/Sub que escrevem num banco de dados; os clientes observam esse banco diretamente. É uma separação limpa, e significa que o sistema não precisa gerenciar conexões para milhares de usuários simultâneos nos momentos de pico das partidas. O trade-off é uma dependência forte da infraestrutura do Firebase — algo que vou retomar no terceiro post desta série.

Os eventos que ele rastreia, e o que cada um dispara:

EventoO que dispara
Gol / gol contraNarração + push notification
Cartão amarelo / vermelhoNarração
SubstituiçãoNarração
Mudança de períodoAtualização de status da partida
Status da partidaAGENDADA → AO VIVO → ENCERRADA

Cada evento carrega tipo, timestamp, jogador, time, e período da partida. Essa granularidade é o que faz as narrações parecerem narrações em vez de atualizações de placar — “34’, pé esquerdo da entrada da área” em vez de simplesmente “1–0”. Para uma notificação de gol, 60 segundos é uma restrição de produto com a qual você aprende a conviver; na prática, ainda assim bate a maioria das transmissões de TV terminando o replay.

Funciona. Funciona há anos. Também tem a forma inconfundível de um sistema que cresceu incrementalmente: cinco serviços para o que é essencialmente um pipeline linear, cada salto um publish no Pub/Sub e uma única transformação. Estamos olhando para refatorar isso, e vou falar sobre o que esse processo revelou no próximo post.


A pergunta que a Copa forçou

Com um sistema já produzindo dados de evento estruturados, com timestamp, por jogador, para cada partida, a Copa levantou uma pergunta óbvia: será que conseguiríamos usar esses dados para produzir conteúdo editorial automaticamente — sem esperar um jornalista escrever?

Os dados estavam lá. O prazo era real. A resposta acabou sendo sim.


Produto 2: editorial automatizado

Isso é o que eu construí. A premissa: quando uma partida da Copa do Mundo termina, produzir uma crônica editorial completa e publicá-la — sem um jornalista escrevendo, e sem um editor revisando antes de ir ao ar.

O enquadramento com que começamos foi “materiais editoriais pré-preenchidos”, implicando que um editor receberia conteúdo estruturado, ajustaria o tom, e apertaria publicar. O que de fato construímos é mais agressivo: o conteúdo chega ao CMS já formatado, taggeado, e pronto. O time editorial recebe uma notificação por e-mail depois da publicação.

O pipeline roda num serviço Python usando APScheduler com jobs de cron e intervalo. Para a maioria das categorias de conteúdo — cotação de moedas, resultados de loteria, agenda de jogos do dia — ele busca de várias fontes e renderiza um template Jinja2 diretamente numa mensagem Pub/Sub que chega ao CMS. Isso é templated, previsível, e o risco editorial é baixo.

Crônicas da Copa do Mundo são diferentes. Quando uma partida termina, o job:

  1. Lê os eventos de narração já armazenados no Firestore — gols, cartões, substituições — produzidos pelo Produto 1
  2. Busca estatísticas da partida (posse de bola, chutes, faltas) na API de dados
  3. Envia tudo para um serviço LLM interno
  4. Recebe uma manchete, linha de apoio, deck, e texto de corpo completo
  5. Publica diretamente no Pub/Sub → CMS

Essa dependência do Firestore é o que conecta os dois produtos — e não é acidental. A mesma estrutura de eventos que faz a narração em tempo real funcionar (o que aconteceu, quando, quem) é exatamente o que você precisa para escrever uma crônica pós-jogo. Um gol com jogador, minuto, e contexto é ao mesmo tempo uma push notification e um parágrafo. O pipeline de narração foi desenhado para consumo do torcedor no momento; o que percebi é que ele também estava produzindo um registro completo e estruturado da partida. Eu não construí uma camada de dados. Eu encontrei uma.


O que aprendemos

Publicação totalmente automatizada é uma decisão maior do que parece. Sem revisão humana antes de publicar significa que a saída do LLM vai ao ar independente do que ela diz. Para crônicas da Copa do Mundo isso funcionou bem — eventos estruturados, estatísticas limpas, um modelo com priors fortes sobre futebol. Mas “isso funcionou” e “isso é seguro” não são a mesma coisa. Tivemos sorte nas partidas que cobrimos.

Construir em cima de um sistema antigo significa aceitar suas restrições. Eu não escolhi o Firebase. Eu não escolhi cinco serviços Java em cadeia. Eles já estavam lá, e funcionavam bem o suficiente para que construir ao redor fosse mais rápido do que reconstruir. A Copa do Mundo tinha um prazo; eu usei o que tinha.

O LLM foi introduzido especificamente para a Copa do Mundo, não como uma capacidade geral. Jogos do campeonato nacional ainda recebem artigos gerados por template. A decisão foi deliberada — a Copa do Mundo era um escopo delimitado onde a barra de qualidade valia o investimento. Essa contenção foi inteligente, mesmo que crie uma inconsistência visível no produto.

Um sistema de 8 anos mostra sua idade em lugares inesperados. Quando o provedor de dados introduziu rate limiting, a estrutura daqueles cinco serviços — cada um fazendo suas próprias chamadas, sem cache compartilhado, sem deduplicação — se tornou um problema real. Vou entrar nisso nos próximos posts desta série.