A gota d’água
Eu estava no Twitter, rolando o feed, quando encontrei um artigo técnico com um layout bonito. O autor tinha um site lindo, e ao invés de discutir o conteúdo, os comentários estavam discutindo o design. Salvei o tweet nos bookmarks — “depois eu vejo com calma”.
Só que depois nunca chegou.
Dias depois, fui procurar aquele artigo e simplesmente não achei. No Twitter free, a busca de bookmarks é terrível. Você não pode filtrar por autor, não pode buscar por palavra-chave, não tem tags, não tem nada. É uma lista linear e cronológica, e você fica ali, paginando com scroll infinito, tentando achar aquela agulha no palheiro.
Isso me deu uma raiva. E raiva, quando bem canalizada, vira projeto.
O ponto de partida: uma extensão de 1 dólar
Antes de pensar em construir algo do zero, eu descobri que existia uma extensão pronta que exportava bookmarks do Twitter. Custava 1 dólar, prometia exportar até 1.000 itens. Comprei, usei, funcionou — até o limite.
Só que eu tinha mais de 1.000 bookmarks acumulados ao longo de anos. Muitos mais.
Então eu fiz o que qualquer pessoa com curiosidade técnica faria: abri o código, entendi como funcionava, e comecei a adaptar. Não era nada sofisticado — um content script que varria a página de bookmarks, extraía os dados, e montava um JSON. Mas era o suficiente para começar.
Foi aí que chamei o Hermes (meu assistente IA) para ajudar a transformar essa base numa extensão própria, completa, que resolvesse o problema do jeito certo.
Onde a porca torce o rabo (ou: os bugs que ninguém conta)
Construir a extensão parece simples no papel. Na prática, cada etapa revelou um problema novo.
1. O scroll que nunca terminava
O content.js original tinha um limite de 40 scrolls. Depois disso, parava — mesmo que ainda houvesse bookmarks para carregar. O Twitter carrega bookmarks em lotes, e se você tem milhares de salvos, 40 scrolls mal arranham a superfície.
Corrigir pareceu simples: remover o limite e deixar rolar até o fim.
Só que o Twitter não coopera. Depois de um tempo, a página para de carregar novos itens, seja por rate limiting, seja porque o DOM estabiliza. Tivemos que implementar um sistema de detecção: 15 scrolls vazios consecutivos + 5 ciclos de altura estável = acabou. Funcionou.
2. O contador mentia
Cada batch de bookmarks extraídos passava por um processo de dedup antes de ser armazenado. O problema é que o contador na interface somava total += newBatch.length — ou seja, pegava o tamanho do batch bruto, não o número real de itens novos que passaram pelo dedup.
Resultado: o contador mostrava 5.000, mas só 3.200 tinham sido salvos de verdade. Quem olhasse pro número achava que estava tudo certo, mas os dados estavam incompletos.
Correção: fazer o contador usar o retorno real do storeChunk, que informava quantos itens eram realmente novos.
3. O vídeo fantasma
O Twitter não renderiza a tag <video> no DOM até que o usuário interaja com o player. Então o content.js, que usava um simples querySelector('video'), simplesmente não detectava vídeos — mesmo quando o tweet claramente tinha um.
A solução foi usar múltiplos seletores: video, [data-testid="videoPlayer"], [aria-label*="Play"], e cruzar os resultados. Depois dessa correção, o número de vídeos detectados saltou de 8 para 849.
4. O pior de todos: 37% dos textos truncados
Esse foi o mais traiçoeiro. A página de bookmarks do Twitter trunca tweets longos. Aparece um botão “Mostrar mais”. O content.js lia o textContent do [data-testid="tweetText"], mas esse elemento só contém o texto visível — ou seja, o texto truncado.
Quando fui navegar pelos bookmarks no gerenciador, percebi que muitos tweets estavam cortados. Fui investigar e descobri: 6.226 bookmarks (37% do total) estavam incompletos, exatamente na faixa dos 250-300 caracteres.
A correção foi expandir cada tweet antes de extrair, clicando programaticamente no botão “Mostrar mais” de cada card. Mas você tem que fazer isso no momento certo, antes do Twitter considerar que você é um bot.
Cada um desses bugs foi descoberto não por testes, mas por uso real. Você está navegando, algo parece estranho, investiga, descobre um problema, corrige, repete.
Quando o frontend não aguenta: a migração forçada de arquitetura
Com os bookmarks finalmente exportados (pouco mais de 16 mil), montei um gerenciador HTML puro — um arquivo estático com JavaScript que carregava os dados e permitia buscar, filtrar, navegar.
Funcionou por alguns segundos. Depois congelava.
16 mil registros no DOM é demais para um HTML puro. A interface travava, a busca demorava, e a experiência era frustrante.
Eu precisava de um backend. Mas eu não queria montar uma infraestrutura nova — queria algo que rodasse nos servidores que já tenho, sem dependências exóticas, sem containers novos, sem estresse.
A escolha foi PHP + SQLite com FTS5. Por que?
- PHP já roda em tudo que é meu servidor
- SQLite com FTS5 é busca textual full-text embutida, sem instalar nada
- A base inteira cabe em um arquivo só — zero administração de banco
Não foi uma escolha de arquitetura por idealismo. Foi necessidade. E funcionou tão bem que virou a espinha dorsal do projeto.
A API responde em milissegundos. A busca FTS5 cobre 16 mil tweets com stemming em português. A interface SPA (um único HTML + JS puro, zero frameworks) carrega uma vez e faz chamadas AJAX para cada ação.
A saga do tagging com IA
Se a exportação foi trabalhosa, o tagging foi um parto.
Meu requisito era claro: nada de extração por heurística. Eu não queria um script que pegasse hashtags do texto e chamasse de “tag”. Queria uma IA que lesse o tweet, entendesse o assunto, e classificasse com tags semânticas de verdade.
O problema é que achar o modelo certo foi uma novela:
- Mixtral 8x22b — 404 direto. Nem rodou.
- DeepSeek v4 Flash — rodou, mas não entregou consistência.
- Llama 3.3 70b (NVIDIA) — teoricamente o melhor. Na prática, timeout atrás de timeout. Cada chamada levava mais de 2 minutos, e o processo morria antes de terminar.
- Llama 3.1 8b (NVIDIA) — o escolhido. Rápido (~0.7s por tweet), estável, e com qualidade de tag boa o suficiente.
Foram mais de 50 minutos de processamento contínuo para taggear os 16 mil tweets. No final: 96.5% dos bookmarks com tags, 18.258 tags únicas, 58.954 atribuições, média de 3.6 tags por bookmark.
Ver o negócio rodando e entregando resultado foi gratificante. Mas o processo me ensinou algo importante: o melhor modelo é o que termina, não o que tem a maior precisão teórica.
O prompt também teve sua própria evolução:
- Versão 1: “Classifique este tweet” — tags inconsistentes, “Tech” vs “Tecnologia”, categorias inventadas.
- Versão final: Prompt estruturado com categorias pré-definidas, formato JSON obrigatório, exemplos embutidos, e campo
suggested_actionpara classificar por prioridade (ler depois, implementar, compartilhar, arquivar).
A diferença entre um prompt ruim e um bom não é sutil — é a diferença entre dados inconsistentes e uma base de conhecimento navegável.
O refinamento visual: quando o usuário é exigente
A interface passou por ciclos e mais ciclos de feedback. Eu queria algo que se aproximasse da experiência do Twitter — porque é lá que os dados nasceram.
- Fonte: tivemos que acertar a font stack até ficar idêntica ao Twitter. Roboto, antialiasing, tamanhos específicos.
- Grid: comecei com auto-fill, mas os cards ficavam largos demais. Foi para
repeat(2, 1fr)— duas colunas de 50%. Melhor, mas ainda ajustamos. - Lista: o modo lista teve que replicar o layout do Twitter: avatar de 40px, fonte 15px, action bar com reply/retweet/like, mídia embedada com border-radius 16px. Cada detalhe importa.
- Tags e Autores: estavam na sidebar como dropdowns. Ficou poluído. Movi para abas dedicadas (🏷️ Tags, 👤 Autores), cada uma com busca própria e cards visuais.
- Tema escuro/claro: implementei com variáveis CSS e persistência em localStorage. O toggle está no header e a transição é instantânea — zero dependências.
- Lazy loading: com 16 mil registros, carregar tudo de uma vez é inviável. Usei IntersectionObserver para scroll infinito com batches de 50.
E claro, os bugs de UI: um loop infinito clássico quando você clicava numa tag no card — o evento disparava busca, que recarregava o dropdown de tags, que disparava outro evento de busca. Resolvido com um _tagChangeLock que parecia gambiarra, mas era elegante na prática.
O projeto de verdade
O repositório está no Gitea da Assistentia, não no GitHub. A escolha foi proposital: manter o código perto da infraestrutura que já uso — e reduzir dependência de provedores externos. (Escrevi um artigo comparando as duas plataformas, dá uma olhada.)
A stack final é simples, deliberadamente:
- Extensão: Manifest V3 para Chrome/Edge (content script + popup + service worker)
- Backend: PHP sem framework, roteamento manual, SQLite com FTS5
- Frontend: HTML + CSS + JS puro, Chart.js (CDN) para analytics
- IA: API NVIDIA com LLM Llama 3.1 8B, chamada via curl do PHP
- Deploy: Tunnel SSH para acesso local (segurança por design)
Nada de React, nada de Docker a mais, nada de Kubernetes. Um projeto que resolve um problema real com as ferramentas certas, não com as ferramentas da moda.
O que eu aprendi
1. O problema certo gera o projeto certo
Não foi “vou criar uma extensão” — foi “não acho mais meus bookmarks e isso me irrita”. O projeto nasceu de uma frustração real, e isso fez toda a diferença na motivação para resolver cada bug, cada truncagem, cada timeout da API.
2. Arquitetura emerge, não se planeja
Comecei com uma extensão simples, fui para um HTML estático, e só quando ele quebrou é que migrei para PHP + SQLite. Se eu tivesse planejado a arquitetura final de antemão, teria superengenhado algo que talvez nunca precisasse. Deixar a necessidade ditar a evolução foi mais eficiente.
3. IA não é para gerar — é para organizar
A maior utilidade prática da IA que experimentei até hoje não foi escrever artigos ou gerar código. Foi pegar 16 mil tweets brutos e transformá-los em uma base de conhecimento categorizada, buscável, filtrável. Isso é mais transformador do que qualquer texto gerado.
4. Exigência de qualidade não é frescura
Você poderia ter aceitado heurística para tags. Poderia ter aceitado texto truncado. Poderia ter aceitado uma interface feia. Cada vez que você não aceitou, o projeto ficou melhor. Prompt refinado, bug corrigido, layout ajustado — a diferença entre algo que “funciona” e algo que “é bom” são dezenas de pequenas exigências.
Os números
- 16.815 bookmarks exportados
- 96.5% com tags semânticas geradas por IA
- 18.258 tags únicas
- 58.954 atribuições de tags
- 7 versões do prompt de tagging até acertar
- 4 modelos de IA testados até achar o estável
- 37% dos tweets estavam truncados — e foram recuperados
- 849 vídeos detectados (vs 8 na versão inicial)
- Dezenas de commits, cada um resolvendo um problema real descoberto no uso
Para onde isso vai
A extensão e o servidor resolveram meu problema. Mas o conceito — exportar dados de uma plataforma, organizar com IA, e criar uma interface própria para navegar — tem potencial maior.
Estou pensando em aplicar a mesma abordagem para organizar artigos salvos do Dev.to, posts do LinkedIn, e talvez transformar isso em algo que outras pessoas possam usar sem precisar passar pelos mesmos bugs que eu passei.
Mas isso, como dizem, fica para um próximo artigo.