Construindo para a Copa do Mundo: narração de torcedor em tempo real e editorial automatizado
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:
| Evento | O que dispara |
|---|---|
| Gol / gol contra | Narração + push notification |
| Cartão amarelo / vermelho | Narração |
| Substituição | Narração |
| Mudança de período | Atualização de status da partida |
| Status da partida | AGENDADA → 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:
- Lê os eventos de narração já armazenados no Firestore — gols, cartões, substituições — produzidos pelo Produto 1
- Busca estatísticas da partida (posse de bola, chutes, faltas) na API de dados
- Envia tudo para um serviço LLM interno
- Recebe uma manchete, linha de apoio, deck, e texto de corpo completo
- 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.