<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AkitaOnRails.com</title><link>https://www.akitaonrails.com/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 20 Apr 2026 15:55:29 GMT</lastBuildDate><atom:link href="https://www.akitaonrails.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Clean Code pra Agentes de IA</title><link>https://www.akitaonrails.com/2026/04/20/clean-code-para-agentes-de-ia/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/20/clean-code-para-agentes-de-ia/</guid><pubDate>Mon, 20 Apr 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;Em 2008, Robert C. Martin (Uncle Bob) lançou &lt;strong&gt;Clean Code: A Handbook of Agile Software Craftsmanship&lt;/strong&gt;. É um dos livros mais influentes da engenharia de software das últimas décadas. Pra quem não sabe, Uncle Bob começou a programar profissionalmente aos 17 anos, fundou a Object Mentor, assinou o Agile Manifesto, foi o primeiro chairman da Agile Alliance, é o criador do acrônimo SOLID. Ele escreveu uma dúzia de livros de design, arquitetura e prática, e influenciou gerações inteiras de desenvolvedores.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Em 2008, Robert C. Martin (Uncle Bob) lançou <strong>Clean Code: A Handbook of Agile Software Craftsmanship</strong>. É um dos livros mais influentes da engenharia de software das últimas décadas. Pra quem não sabe, Uncle Bob começou a programar profissionalmente aos 17 anos, fundou a Object Mentor, assinou o Agile Manifesto, foi o primeiro chairman da Agile Alliance, é o criador do acrônimo SOLID. Ele escreveu uma dúzia de livros de design, arquitetura e prática, e influenciou gerações inteiras de desenvolvedores.</p>
<p>Eu acompanho o Uncle Bob há muitos anos. Troquei mensagem com ele algumas vezes ao longo das décadas, tenho opinião formada sobre as posições dele, e cheguei a fazer uma live no meu canal onde a gente conversou por quase uma hora sobre Clean Code, Agile, Craftsmanship e pra onde a indústria tava indo. Se você nunca viu, recomendo:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/ycvaECDc31w"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>O Clean Code especificamente estabeleceu um padrão: código é escrito uma vez, mas lido dezenas de vezes. O trabalho do programador não é só fazer funcionar. É fazer funcionar <strong>de um jeito que outro programador consiga entender, modificar e não quebrar</strong>. Nomes significativos, funções pequenas, uma responsabilidade por classe, sem repetição, testes automatizados, estrutura clara. O público-alvo sempre foi outro ser humano sentado em frente ao editor tentando entender o que o primeiro fez.</p>
<p>Em 2026, esse público-alvo mudou.</p>
<h2>O público não é mais humano<span class="hx:absolute hx:-mt-20" id="o-público-não-é-mais-humano"></span>
    <a href="#o-p%c3%bablico-n%c3%a3o-%c3%a9-mais-humano" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Semana passada escrevi <a href="/2026/04/11/vs-code-e-o-novo-cartao-perfurado/">VS Code é o novo cartão perfurado</a>. A tese do artigo é que digitar código manualmente em editor de texto tá virando atividade de nicho, do mesmo jeito que digitar binário direto em painel de Altair virou relíquia depois que compiladores ficaram bons. O agente de IA é o novo compilador: eu descrevo intenção em linguagem natural, o agente navega código, edita, roda teste, ajusta, entrega.</p>
<p>Se essa tese tiver mesmo em pé, a pergunta seguinte é óbvia: <strong>pra quem a gente tá escrevendo código agora?</strong></p>
<p>Não é pro programador humano que vai sentar amanhã pra manter. É pro agente que vai ler, editar e estender. E agente não é humano. Agente tem restrições técnicas diferentes, vieses diferentes, limitações diferentes. Uma parte do Clean Code continua valendo (em alguns casos fica mais crítica). Outra parte muda de peso. E aparecem exigências novas que o Uncle Bob não tinha como antecipar em 2008.</p>
<p>Esse artigo é sobre isso. Qual a versão de Clean Code que faz sentido quando o leitor primário é um LLM?</p>
<h2>As restrições reais dos agentes<span class="hx:absolute hx:-mt-20" id="as-restrições-reais-dos-agentes"></span>
    <a href="#as-restri%c3%a7%c3%b5es-reais-dos-agentes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de re-ranquear, vale revisar o que os agentes enfrentam de verdade.</p>
<p><strong>Truncamento de arquivo.</strong> A maioria das CLIs de agente limita leitura de arquivo a faixas pequenas. O Claude Code lê por default 2000 linhas por vez. Cursor, Codex, Windsurf, todos têm limites parecidos. Arquivo gigante simplesmente não entra inteiro na janela de contexto numa tacada só, agente tem que pedir pedaço por pedaço, ou pior, usar grep e reconstruir mentalmente.</p>
<p><strong>Atenção degrada com contexto.</strong> Claude Opus tem contexto de 200k tokens, Sonnet de 1M, Gemini de 1M. Parece muito. Na prática, testes de &ldquo;needle in haystack&rdquo; mostram que a qualidade da recuperação cai bem antes do limite declarado. Flash Attention e variantes aceleram o cálculo, mas não substituem a atenção nativa cheia. Quanto mais coisa você enfia na janela, pior a precisão de detalhe. E o contexto do agente não tem só o seu código: tem CLAUDE.md, tem prompt do sistema, tem histórico de conversa, tem saída de tool, tem log de erro, tem output de teste. Tudo concorrendo pela mesma janela.</p>
<p><strong>Grep é mais barato que read.</strong> O agente sabe disso. Ele prefere <code>rg &quot;funcName&quot;</code> do que carregar arquivo inteiro. Fica mais rápido, usa menos token, mira no alvo. Nomes únicos e distintivos tornam isso muito mais eficaz. Isso não é atalho, é decisão arquitetural: escrevi sobre isso em detalhe em <a href="/2026/04/06/rag-esta-morto-contexto-longo/">RAG Está Morto? Contexto Longo, Grep e o Fim do Vector DB Obrigatório</a>, mostrando que o próprio Claude Code navega repositório com <code>Glob</code> e <code>Grep</code>, sem vector DB, sem embedding, e isso não é deficiência, é design maduro. Busca lexical + leitor inteligente consome o texto bruto bate retriever denso + top-k em praticamente todo benchmark de domínio real. Você se beneficia disso na hora de organizar seu código: nomes grepáveis não são só &ldquo;bom pra humano&rdquo;, são a API primária de navegação do agente.</p>
<p><strong>Tool calls custam token.</strong> Cada <code>Read</code> ou <code>Edit</code> ou <code>Bash</code> gasta tokens de input e output. Arquivo curto, output de teste pequeno, log enxuto, tudo isso mantém o agente produtivo e a conta baixa.</p>
<p><strong>Latência importa.</strong> Agente em loop, cada tool call adiciona segundos. Arquivo grande demorado de processar vira gargalo perceptível de sessão.</p>
<p><strong>Grepar por padrão visual é difícil.</strong> Se você usou indentação inconsistente, tab vs espaço misturado, brace style variado entre arquivos, o agente gasta tokens internalizando a bagunça. Consistência ajuda.</p>
<p>A partir dessas restrições, dá pra re-ranquear os princípios do Clean Code em ordem de relevância pra trabalho com agente.</p>
<h2>Re-ranqueamento: Clean Code na era dos agentes<span class="hx:absolute hx:-mt-20" id="re-ranqueamento-clean-code-na-era-dos-agentes"></span>
    <a href="#re-ranqueamento-clean-code-na-era-dos-agentes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vou da mais importante pra menos. Um aviso: não é que as de baixo deixam de importar. É que as de cima passaram a importar MUITO mais.</p>
<h3>1. Funções pequenas (e arquivos pequenos)<span class="hx:absolute hx:-mt-20" id="1-funções-pequenas-e-arquivos-pequenos"></span>
    <a href="#1-fun%c3%a7%c3%b5es-pequenas-e-arquivos-pequenos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Uncle Bob: &ldquo;funções devem fazer UMA coisa, devem fazer BEM, e devem fazer só isso&rdquo;. Tamanho ideal de 4 a 20 linhas, no livro.</p>
<p>Pra agente, essa recomendação virou obrigação técnica. Uma função pequena cabe numa única tool call sem truncamento. Um arquivo curto (mantenha abaixo de 500 linhas, idealmente 200-300) cabe numa única leitura. Se o agente consegue pegar a unidade inteira de sentido numa chamada, ele raciocina sobre ela com atenção cheia. Se tem que paginar, ele monta um modelo mental fragmentado, e cada fragmento custa atenção.</p>
<p>Antigamente, &ldquo;função pequena&rdquo; era bom pra humano porque facilita leitura. Hoje, &ldquo;função pequena&rdquo; é bom porque casa com a unidade de processamento do modelo. Se tem uma recomendação pra levar em consideração, é essa.</p>
<h3>2. Single Responsibility Principle (SRP)<span class="hx:absolute hx:-mt-20" id="2-single-responsibility-principle-srp"></span>
    <a href="#2-single-responsibility-principle-srp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Cada módulo faz uma coisa e tem uma razão pra mudar. Já era o coração do Clean Code. Pra agente, vira ainda mais crítico porque:</p>
<ul>
<li>O agente consegue isolar a unidade pra entender sem carregar o resto do sistema</li>
<li>Dá pra rodar teste focado sobre ela</li>
<li>Dá pra editar sem temer efeito colateral</li>
<li>Grep por responsabilidade vira previsível</li>
</ul>
<p>Código com responsabilidades embaralhadas força o agente a carregar muito mais contexto pra fazer qualquer mudança simples. Classe de 800 linhas que faz três coisas é pior pro agente do que três classes de 250 linhas, mesmo que o total seja o mesmo.</p>
<h3>3. Nomes significativos e únicos<span class="hx:absolute hx:-mt-20" id="3-nomes-significativos-e-únicos"></span>
    <a href="#3-nomes-significativos-e-%c3%banicos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code já pregava: nomes revelam intenção, sem disinformação, distintivos, pronunciáveis, pesquisáveis. Pro agente, &ldquo;pesquisáveis&rdquo; virou a propriedade mais importante dessa lista.</p>
<p>Agente pesquisa código via grep/ripgrep o tempo todo. Nome genérico (<code>data</code>, <code>process</code>, <code>handler</code>, <code>Manager</code>, <code>Service</code>) retorna cinquenta matches e obriga o agente a ler cada um. Nome distintivo (<code>UserRegistrationValidator</code>, <code>InvoiceLineItemTotal</code>, <code>ClaudeCodeSessionTracker</code>) retorna três matches e o agente vai direto no certo.</p>
<p>Regra prática: se você grep o nome e vem muita coisa irrelevante, o nome tá ruim pro agente. Se vem só o que importa, o nome tá certo.</p>
<h3>4. Comentários com contexto e proveniência<span class="hx:absolute hx:-mt-20" id="4-comentários-com-contexto-e-proveniência"></span>
    <a href="#4-coment%c3%a1rios-com-contexto-e-proveni%c3%aancia" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui é onde a inversão é mais gritante. Pra Uncle Bob em 2008, o axioma era: &ldquo;código bom se explica sozinho, comentário em excesso é code smell, cada comentário é uma dívida que fica desatualizada&rdquo;. Todo programador experiente que leu o livro absorveu essa regra. Código bem nomeado não precisa de comentário. Comentário demais = alerta de código ruim tentando se justificar.</p>
<p>Agora inverte. <strong>O agente lê os comentários. E gosta deles</strong>. Comentário vira contexto de primeira classe. O agente tem fluência perfeita de sintaxe, sabe exatamente o que <code>x++</code> faz, não precisa de legenda óbvia (esse tipo ainda é ruim, olha o item 13). O que ele NÃO sabe é por que você escolheu essa abordagem em vez da óbvia, qual bug de produção motivou essa lógica estranha, qual constraint de negócio força essa ordem específica, qual workaround existe porque a lib upstream tem bug conhecido #1234, qual commit introduziu essa decisão, qual issue do Jira é a referência. Esse tipo de informação é <strong>proveniência</strong>: o porquê da decisão. Só existe na cabeça do humano que escreveu, na mensagem de commit, ou num comentário bem colocado. Pro agente, o comentário é a fonte mais acessível durante um tool call.</p>
<p>Docstring com intenção e exemplo de uso também viraram sinal forte. Quando o agente pega uma função sem entender o contexto, docstring de cabeça (tipo JSDoc com exemplos, Python <code>&quot;&quot;&quot;</code>, Rust <code>///</code>) encurta drasticamente o caminho pra uma mudança correta. Uncle Bob era cético de JavaDoc em 2008 porque ficava desatualizado. Hoje, com o agente podendo reescrever a docstring junto com o código, esse contra-argumento perdeu peso.</p>
<p>Uma consequência prática disso é: <strong>não pode os comentários que o agente escreve</strong>. Se você tem o reflexo &ldquo;comentário verboso é ruído&rdquo; herdado da era Clean Code original, essa regra mudou. O agente colocou aquele comentário porque no ato de gerar o código ele decidiu que aquela informação era digna de preservar pra futura edição. Remover o comentário no code review é tirar contexto que o próprio agente vai querer ler na próxima interação. Deixa o agente comentar. Ele sabe o que faz. O único tipo de comentário do agente que vale remover é o comentário óbvio e redundante (item 13), e mesmo esses os modelos recentes raramente produzem se o system prompt tá bem escrito.</p>
<h3>5. Tipos explícitos<span class="hx:absolute hx:-mt-20" id="5-tipos-explícitos"></span>
    <a href="#5-tipos-expl%c3%adcitos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso não tá no Clean Code de 2008 porque a indústria ainda não tinha se convertido. Mas em 2026 é critério fundamental.</p>
<p>Python sem type hints, JavaScript em vez de TypeScript, Ruby sem RBS. Código dinâmico sem anotação obriga o agente a inferir tipo a partir de uso, o que custa raciocínio e erra frequentemente. Código tipado dá gabarito imediato: assinatura fala o que entra, o que sai, quais estados são válidos. O agente economiza trabalho de descoberta e erra menos.</p>
<p>Se você ainda tá em Python 3 sem type hints, a transição vai aumentar a produtividade do agente muito mais do que qualquer refactoring de lógica.</p>
<h3>6. DRY (Don&rsquo;t Repeat Yourself)<span class="hx:absolute hx:-mt-20" id="6-dry-dont-repeat-yourself"></span>
    <a href="#6-dry-dont-repeat-yourself" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code já dizia: duplicação é raiz de todo mal. Pra agente, duplicação é pior que pra humano por uma razão específica: quando o agente tem que mudar uma coisa que tá replicada, ele pode atualizar uma cópia e esquecer das outras. A janela de atenção dele não tem gravidade natural que puxe &ldquo;ah, tem mais duas cópias disso em outros arquivos&rdquo;. Ele tem que achar cada uma via grep, e se o padrão tem variação sutil entre as cópias, o resultado fica inconsistente.</p>
<p>Fatorar em função ou módulo reutilizável não é estética. É segurança de refactor automatizado.</p>
<h3>7. Testes que o agente consegue rodar<span class="hx:absolute hx:-mt-20" id="7-testes-que-o-agente-consegue-rodar"></span>
    <a href="#7-testes-que-o-agente-consegue-rodar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Uncle Bob dedica um capítulo inteiro a Unit Tests e o F.I.R.S.T (Fast, Independent, Repeatable, Self-Validating, Timely). Continua valendo tudo, com um adendo importante: <strong>o teste precisa ser executável pelo agente sem setup humano</strong>.</p>
<p>Isso significa: comando pra rodar o teste tá no README ou CLAUDE.md, tá no <code>Makefile</code>, tá no <code>package.json</code>. Output tem formato previsível que o agente parseia. Não depende de seed manual de banco, de arquivo de config que não tá no repo, de credencial secreta. O agente escreve código, roda teste, lê output, ajusta, roda de novo. Esse ciclo é a base de tudo. Se o teste não roda headless, o agente fica cego.</p>
<p>Aqui eu falo com experiência de campo. Documentei isso no <a href="/2026/02/20/do-zero-a-pos-producao-em-1-semana-como-usar-ia-em-projetos-de-verdade-bastidores-do-the-m-akita-chronicles/">Do Zero a Pós-Produção em 1 Semana com IA</a>, onde mandei ver num projeto real: 274 commits em 8 dias, 4 aplicações integradas, 1.323 testes automatizados no final. O que fez aquilo funcionar não foi &ldquo;IA programa sozinha&rdquo;. Foi <strong>Extreme Programming com agente no lugar do par humano</strong>. Rodar teste em cada commit, ter CI apertado, cobertura acima de 80% (95%+ na lógica de negócio), ratio de linha-de-teste pra linha-de-código em alguns módulos maior que 1:1. Parece overkill. Não é. Em 274 commits, o CI pegou bug real mais de 50 vezes, bugs que iriam direto pra produção se eu tivesse confiado cegamente no agente. Sem teste, o agente te entrega código plausível que silenciosamente quebra algo que funcionava ontem. Com teste forte, o agente vira multiplicador: ele gera teste, o teste valida o código que ele escreveu, o teste é a rede de segurança da próxima mudança dele. Loop virtuoso.</p>
<p>As práticas do XP (pair programming, CI, testes antes, refactoring contínuo, feedback curto) não ficaram obsoletas. Viraram <strong>exatamente o jeito certo de trabalhar com agente</strong>. Quem programa em cowboy mode sem teste, hoje, não é rebelde. É só lento, porque o agente sem teste fica chutando, e chute precisa ser revisado na mão, o que mata a velocidade que o agente deveria trazer. Testes bons com cobertura bem feita viraram a diferença entre agente produtivo e agente que fica chutando. Ou, dito de outro jeito: <strong>TDD virou obrigação técnica, não mais filosofia</strong>.</p>
<p>Cobri esse tema de outro ângulo em <a href="/2026/03/01/software-nunca-esta-pronto-4-projetos-a-vida-pos-deploy-e-por-que-one-shot-prompt-e-mito/">Software Nunca Está &ldquo;Pronto&rdquo;</a>, mostrando que a vida de pós-deploy é onde o teste mais importa: em dez dias de operação depois do lançamento, eu rodei 56 commits de correção, hardening e ajuste em resposta a comportamento real, e cada commit foi acompanhado de teste de regressão. Sem rede, cada um desses 56 commits seria uma oportunidade de quebrar algo que funcionava ontem. TDD não é fase, é hábito.</p>
<h3>8. Estrutura de diretório previsível<span class="hx:absolute hx:-mt-20" id="8-estrutura-de-diretório-previsível"></span>
    <a href="#8-estrutura-de-diret%c3%b3rio-previs%c3%advel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code pouco fala sobre isso (tava mais focado em código dentro de arquivo). Pra agente, a organização em árvore importa. Se <code>src/controllers/users.rb</code> implica que tem <code>src/models/user.rb</code> e <code>src/views/users/</code>, o agente consegue antecipar paths sem precisar listar diretório. Se o projeto usa nomenclatura idiossincrática (arquivos random, nomes sem padrão, tudo flat numa pasta), o agente perde tempo com <code>find</code>.</p>
<p>Convenções fortes de framework (Rails, Django, Next.js, Laravel) ajudam muito agente. Projeto sem convenção, o agente cria uma com o tempo, mas até lá gasta tokens explorando.</p>
<h3>9. Dependency Injection e Testabilidade<span class="hx:absolute hx:-mt-20" id="9-dependency-injection-e-testabilidade"></span>
    <a href="#9-dependency-injection-e-testabilidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Código com dependências injetadas (não hardcoded) é mais fácil de testar em isolamento. O agente aproveita isso. Ele consegue substituir o <code>EmailSender</code> real por um <code>FakeEmailSender</code> no teste, sem tocar na lógica. Código que instancia suas dependências internamente obriga o agente a gambiarra-monkey-patch-sobe-servidor-de-mentira, que é lento, frágil, e polui a sessão com sujeira de infra.</p>
<p>DI não é cerimônia. É escopo de isolamento. E em projeto de vida real, DI vira rapidamente refactor load-bearing: num dos meus projetos (o <a href="/2026/03/01/software-nunca-esta-pronto-4-projetos-a-vida-pos-deploy-e-por-que-one-shot-prompt-e-mito/">M.Akita Chronicles</a>), eu descobri depois do lançamento que precisava trocar o modelo LLM default pra outro provedor. A variável de ambiente existia desde o início. Mas o nome do modelo ainda estava hardcoded em referência em 24 arquivos. Um commit inteiro (<code>Centralize LLM model config</code>) tocou os 24 pra isolar a config numa constante única. Trocar de modelo depois disso virou mudança de uma linha. Esse é exatamente o tipo de refactor que só aparece depois que o software encosta na realidade, e é onde DI e isolamento de config pagam caro se você não fez antes.</p>
<h3>10. Evitar aninhamento profundo<span class="hx:absolute hx:-mt-20" id="10-evitar-aninhamento-profundo"></span>
    <a href="#10-evitar-aninhamento-profundo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code fala de nível único de abstração por função. Um corolário é: evite <code>if</code> dentro de <code>for</code> dentro de <code>if</code> dentro de <code>try</code>. Cada nível de indentação é mais atenção que o modelo tem que gastar rastreando estado. Indentação de quatro níveis é MUITO mais cara cognitivamente pro agente do que dois níveis com early return.</p>
<p>Pattern matching, guard clauses, early returns, flatten de lógica, tudo isso melhora a legibilidade pro modelo igual melhora pra humano, só que de forma mais mensurável porque o custo é medido em qualidade de resposta.</p>
<h3>11. Erro com contexto<span class="hx:absolute hx:-mt-20" id="11-erro-com-contexto"></span>
    <a href="#11-erro-com-contexto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>raise ValueError(&quot;invalid input&quot;)</code> não ajuda o agente quando ele lê o stack trace. <code>raise ValueError(f&quot;invalid input: received {repr(x)}, expected non-empty string of digits&quot;)</code> ajuda. O agente usa mensagem de exceção como sinal pra debug. Mensagem vaga = agente roda round extra pra descobrir o que deu errado.</p>
<p>Uncle Bob falava disso em Error Handling: &ldquo;Provide context with exceptions&rdquo;. Virou crítico agora.</p>
<h3>12. Formatação e estilo<span class="hx:absolute hx:-mt-20" id="12-formatação-e-estilo"></span>
    <a href="#12-formata%c3%a7%c3%a3o-e-estilo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Não perde tempo nisso. Usa o formatador default ou mais popular da linguagem: <code>cargo fmt</code> pra Rust, <code>gofmt</code> pra Go, <code>prettier</code> pra JS/TS, <code>black</code> ou <code>ruff</code> pra Python, <code>rubocop -A</code> pra Ruby. Configura no pre-commit, configura no editor pra rodar ao salvar, e segue a vida. O agente lida bem com qualquer estilo consistente, e o formatador automático garante que o diff não fica bagunçado entre commits. Discussão de tab vs espaço, 80 vs 100 colunas, brace style, tudo isso virou ruído. O formatador decide, você aceita.</p>
<h3>13. Comentário que descreve o óbvio<span class="hx:absolute hx:-mt-20" id="13-comentário-que-descreve-o-óbvio"></span>
    <a href="#13-coment%c3%a1rio-que-descreve-o-%c3%b3bvio" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A última da lista. Continua ruim, virou ainda mais ruim. Comentários como <code>// increment i by 1</code> acima de <code>i++</code> desperdiçam tokens no agente exatamente como desperdiçavam paciência no humano. Modelo sabe ler código, não precisa de legenda óbvia.</p>
<p>Se você tem o hábito de escrever comentários óbvios porque alguma escola te ensinou assim, esse é o momento de parar. Em 2008 era ruim porque poluía visual. Em 2026 é ruim porque custa dinheiro real em tokens.</p>
<h2>O que Uncle Bob não podia antever<span class="hx:absolute hx:-mt-20" id="o-que-uncle-bob-não-podia-antever"></span>
    <a href="#o-que-uncle-bob-n%c3%a3o-podia-antever" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Além de re-ranquear o que tava no livro, algumas coisas novas surgiram que são específicas do mundo de agentes:</p>
<p><strong>Arquivos de meta-documentação pra agentes.</strong> <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.cursor/rules</code>, e afins. São arquivos que o agente lê antes de qualquer tool call, descrevendo convenções do projeto, comandos importantes, caveats, coisas que não vão em docstring. Escrever esses arquivos é um skill novo: curto, direto, imperativo, orientado a ações. Nada de prosa filosófica. Bulletpoint do que agente precisa saber pra não cagar.</p>
<p><strong>README com arquitetura alto nível.</strong> Uncle Bob pouco se importava com README (o livro é sobre código). Pra agente, READMEs bem feitos encurtam muito o caminho pro agente entender o shape do projeto. Diagrama simples em ASCII ou Mermaid ajuda.</p>
<p><strong>Logging estruturado.</strong> Log em formato JSON com campos nomeados é muito mais útil pro agente do que log em prosa. Agente parseia JSON trivialmente, usa os campos pra filtrar erro relevante, correlaciona entre serviços. <code>printf</code> solto em texto livre obriga parsing heurístico.</p>
<p><strong>Comandos de observabilidade acessíveis.</strong> <code>pnpm test</code>, <code>make lint</code>, <code>cargo check</code>, <code>python -m mypy</code> — quanto mais o projeto expõe comandos previsíveis que o agente pode invocar pra validar mudanças, melhor. Se pra rodar teste precisa de 10 passos de setup manual, o agente não roda teste, e o ciclo de feedback quebra.</p>
<p><strong>Scripts de setup idempotentes.</strong> Agente tem que poder rodar <code>bin/setup</code> ou <code>scripts/bootstrap.sh</code> numa máquina limpa e chegar num estado que permita trabalhar. Se o onboarding depende de instruções em cabeça humana, o agente tá excluído do jogo.</p>
<h2>Instruindo o agente a escrever código limpo<span class="hx:absolute hx:-mt-20" id="instruindo-o-agente-a-escrever-código-limpo"></span>
    <a href="#instruindo-o-agente-a-escrever-c%c3%b3digo-limpo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem um detalhe importante que só fica claro depois de umas 500 horas usando agente: <strong>nenhum LLM faz essas coisas por default</strong>. Você pede pra ele &ldquo;implementa a feature X&rdquo; e ele vai implementar do jeito que o modelo acha médio. Sem injeção de dependência. Com funções de 80 linhas. Sem testes, ou com testes que mockam coisa errada. Duplicando lógica porque é mais rápido. Criando arquivo de 2000 linhas porque &ldquo;fica tudo junto&rdquo;. Você precisa ESCREVER essas regras. O agente lê, e segue.</p>
<p>Onde escrever: <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.cursor/rules</code>, <code>.github/copilot-instructions.md</code>, dependendo da CLI. Formato: curto, imperativo, orientado a ação. Agente lê esses arquivos a cada iteração (o Claude Code relê o CLAUDE.md em toda query), então cada linha gasta token de contexto — densidade importa.</p>
<p>Abaixo segue uma proposta de template pra você colar num <code>CLAUDE.md</code> de projeto novo, consolidando o que discuti acima em formato que o agente consome. <strong>Não é versão definitiva</strong>, é ponto de partida pra testar, ajustar à sua linguagem, ao seu time, ao seu fluxo. Se alguma regra não encaixar no teu contexto, remove. Se precisar de regra nova, adiciona. O ponto é ter o esqueleto estruturado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## Code style
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Functions: 4-20 lines. Split if longer.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Files: under 500 lines. Split by responsibility.
</span></span><span class="line"><span class="cl"><span class="k">-</span> One thing per function, one responsibility per module (SRP).
</span></span><span class="line"><span class="cl"><span class="k">-</span> Names: specific and unique. Avoid <span class="sb">`data`</span>, <span class="sb">`handler`</span>, <span class="sb">`Manager`</span>.
</span></span><span class="line"><span class="cl">  Prefer names that return &lt;5 grep hits in the codebase.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Types: explicit. No <span class="sb">`any`</span>, no <span class="sb">`Dict`</span>, no untyped functions.
</span></span><span class="line"><span class="cl"><span class="k">-</span> No code duplication. Extract shared logic into a function/module.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Early returns over nested ifs. Max 2 levels of indentation.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Exception messages must include the offending value and expected shape.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Comments
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Keep your own comments. Don&#39;t strip them on refactor — they carry
</span></span><span class="line"><span class="cl">  intent and provenance.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Write WHY, not WHAT. Skip <span class="sb">`// increment counter`</span> above <span class="sb">`i++`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Docstrings on public functions: intent + one usage example.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Reference issue numbers / commit SHAs when a line exists because
</span></span><span class="line"><span class="cl">  of a specific bug or upstream constraint.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Tests
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Tests run with a single command: <span class="sb">`&lt;project-specific&gt;`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Every new function gets a test. Bug fixes get a regression test.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mock external I/O (API, DB, filesystem) with named fake classes,
</span></span><span class="line"><span class="cl">  not inline stubs.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Tests must be F.I.R.S.T: fast, independent, repeatable,
</span></span><span class="line"><span class="cl">  self-validating, timely.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Dependencies
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Inject dependencies through constructor/parameter, not global/import.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Wrap third-party libs behind a thin interface owned by this project.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Structure
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Follow the framework&#39;s convention (Rails, Django, Next.js, etc.).
</span></span><span class="line"><span class="cl"><span class="k">-</span> Prefer small focused modules over god files.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Predictable paths: controller/model/view, src/lib/test, etc.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Formatting
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Use the language default formatter (<span class="sb">`cargo fmt`</span>, <span class="sb">`gofmt`</span>, <span class="sb">`prettier`</span>,
</span></span><span class="line"><span class="cl">  <span class="sb">`black`</span>, <span class="sb">`rubocop -A`</span>). Don&#39;t discuss style beyond that.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Logging
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Structured JSON when logging for debugging / observability.
</span></span><span class="line"><span class="cl">- Plain text only for user-facing CLI output.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse bloco cabe em menos de 100 linhas e gasta cerca de 500 tokens por iteração. Parece muito, mas a economia em qualidade de código e em ausência de retrabalho compensa facilmente, especialmente se você tá numa conta API paga por token. Tomem como base e evoluam com a experiência de cada um.</p>
<p>Alguns itens (SRP, funções pequenas, testes) o agente até tenta fazer sozinho. Outros (DI, DRY rigoroso, tipos explícitos em TODO lugar, nomes agressivamente únicos) ele só faz quando você fala explícito. E alguns (como &ldquo;não apaga os comentários que você mesmo escreveu no refactor&rdquo;) são tão contraintuitivos pro treino padrão dele que sem a instrução ele VAI podar. Daí a importância do arquivo de regras existir e ser lido a cada iteração.</p>
<p>Tem um padrão análogo que vale mencionar, ligado ao princípio de defensive programming: o agente implementa circuit breaker, retry com backoff, timeout agressivo, graceful degradation, tudo certinho — <strong>quando você pede</strong>. Mas sozinho ele não vai propor. O agente não sabe quais são os pontos de falha operacional do teu sistema, então ele implementa o caminho feliz e espera instrução. Se a tua CLAUDE.md listar as categorias de defensive code que o projeto precisa (rate limit, retry, breaker, fallback), o agente cobre. Se não listar, ele não inventa. É outro caso em que a instrução explícita pro agente é o que separa código robusto de código ingênuo.</p>
<p>Se você usar o mesmo agente em projetos diferentes, vale ter um template base e adicionar rules específicas por projeto no topo. Mas comece com algo parecido com o acima e itere em cima.</p>
<h2>A versão curta<span class="hx:absolute hx:-mt-20" id="a-versão-curta"></span>
    <a href="#a-vers%c3%a3o-curta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uncle Bob escreveu o Clean Code pra ser lido por outros humanos. Em 2026, o leitor primário virou o agente. A boa notícia é que a maioria das coisas que o livro pregava continua valendo. A má notícia é que algumas coisas que eram opinião (&ldquo;arquivo deve ter N linhas&rdquo;) viraram restrição técnica (&ldquo;arquivo com X linhas faz o agente performar pior&rdquo;). A diferença é que agora tem métrica: custo de token, latência de tool call, qualidade de output. Quem escreve código limpo pro agente economiza dinheiro de conta de API, tempo de sessão, e tem menos alucinação no output.</p>
<p>E tem um bônus cultural que vale registrar: todas essas práticas (XP, TDD, SOLID, SRP, DI, código pequeno, testes abundantes) estavam caindo em desuso nos anos 2010, substituídas por &ldquo;move fast and break things&rdquo; e bootcamp de dois meses. Os programadores que investiram em fundamento viraram minoria, e virou fashion falar mal de Uncle Bob na internet. Acontece que exatamente esses fundamentos passaram a ser o diferencial técnico de trabalhar com agente. Quem manteve a disciplina, tá bem servido. Quem desprezou, tá apanhando pra ensinar o agente a não cometer erros que a turma do XP tinha mapeado 25 anos atrás.</p>
<p>Código limpo nunca foi moda. Virou infraestrutura.</p>
]]></content:encoded><category>clean-code</category><category>AI</category><category>claude-code</category><category>vibecoding</category><category>software-engineering</category></item><item><title>Meus Retrogames de Corrida Favoritos Rodando no Meu Distrobox</title><link>https://www.akitaonrails.com/2026/04/19/retrogames-de-corrida-favoritos-no-distrobox/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/19/retrogames-de-corrida-favoritos-no-distrobox/</guid><pubDate>Sun, 19 Apr 2026 20:00:00 GMT</pubDate><description>&lt;p&gt;Eu passei o domingo inteiro nisso, e foi um dos domingos mais produtivos que eu tive em tempos. A missão foi específica: fechar os jogos de corrida simcade mais difíceis de emular, que eu quero rodar há anos, e só agora consegui chegar num estado confiável.&lt;/p&gt;
&lt;p&gt;No topo dessa lista estão dois monstros. Primeiro, &lt;strong&gt;&lt;a href="#driveclub-o-imposs%c3%advel-finalmente-poss%c3%advel"&gt;Driveclub&lt;/a&gt;&lt;/strong&gt; no shadPS4. É exclusivo de PS4, nunca foi portado, a Evolution Studios foi fechada, não tem remaster. Pra jogar fora do PS4 original, só via emulação, e emulação de PS4 ainda é a parte mais imatura da ecossistema hoje. Esse é &lt;strong&gt;de longe o mais difícil da lista&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu passei o domingo inteiro nisso, e foi um dos domingos mais produtivos que eu tive em tempos. A missão foi específica: fechar os jogos de corrida simcade mais difíceis de emular, que eu quero rodar há anos, e só agora consegui chegar num estado confiável.</p>
<p>No topo dessa lista estão dois monstros. Primeiro, <strong><a href="#driveclub-o-imposs%c3%advel-finalmente-poss%c3%advel">Driveclub</a></strong> no shadPS4. É exclusivo de PS4, nunca foi portado, a Evolution Studios foi fechada, não tem remaster. Pra jogar fora do PS4 original, só via emulação, e emulação de PS4 ainda é a parte mais imatura da ecossistema hoje. Esse é <strong>de longe o mais difícil da lista</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/19/distrobox-gaming/shadps4-launcher.png" alt="shadPS4 Qt Launcher mostrando DRIVECLUB CUSA00003 pronto pra rodar"  loading="lazy" /></p>
<p>Segundo, <strong><a href="#forza-motorsport-4-o-goat">Forza Motorsport 4 com Project Forza Plus</a></strong> no Xenia Canary. FM4 é o GOAT da era Xbox 360, Project Forza Plus é a mod comunitária que consolida patches e conteúdo, e fazer os dois rodarem juntos no Xenia com gráfico decente, sem crash de áudio e sem shadow bug, levou horas sérias de trial and error.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/19/distrobox-gaming/xenia-manager.png" alt="Xenia Manager com a coleção Forza do Xbox 360 instalada"  loading="lazy" /></p>
<p>Em torno desses dois eu organizei o resto do que eu já sabia que funcionava em algum nível, só faltava consolidar num setup repetível: <strong><a href="#gran-turismo-4">Gran Turismo 4 com Retexture 3.0 e HD HUD</a></strong>, <strong><a href="#gran-turismo-3-a-spec">Gran Turismo 3 com HD textures e widescreen</a></strong>, <strong><a href="#gran-turismo-2-spec-ii-mod">Gran Turismo 2 na mod Spec II</a></strong>, os Gran Turismos de PS3 (<a href="#gran-turismo-5">GT5</a> e <a href="#gran-turismo-6">GT6</a>), os Forza Horizon <a href="#forza-horizon-1-com-xe-mod">1</a> e <a href="#forza-horizon-2">2</a>, os <a href="#project-gotham-racing-3-e-4">PGRs</a>, <a href="#ridge-racer-v">Ridge Racer</a>, <a href="#enthusia-professional-racing">Enthusia</a>, <a href="#colin-mcrae-rally-1-e-2">Colin McRae</a>. Esses eu tinha ideia de como fazer, só precisava consolidar num setup que não volta a quebrar na próxima vez que eu reinstalar o sistema.</p>
<p>Antes de entrar no detalhe, preciso esclarecer duas coisas.</p>
<p>Primeiro, sobre a infra: não vou repetir aqui o como o setup foi montado. Já escrevi isso em detalhe no artigo <a href="/2026/04/11/distrobox-de-emulacao-com-claude-code/">Distrobox de Emulação com Claude Code</a>. Arch Linux num distrobox com <code>--nvidia</code>, 17 roles Ansible, todos os emuladores no AUR, ES-DE como frontend, configs per-game automatizadas, scripts Python pra PS3 update check e Xbox 360 title updates, Xenia via Wine. Lê lá. O repo tá em <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">akitaonrails/distrobox-gaming</a> pra quem quiser reproduzir. Aqui eu vou falar dos jogos.</p>
<p>Segundo, sobre o gosto: eu gosto de simcade de corrida. Gran Turismo é meu vício declarado desde o PS1, e escrevi sobre o resto no artigo do <a href="/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/">meu cockpit Formula FX1</a>. Volante direct drive, pedal com load cell, triple monitor quando tô afim de sofrer. Esse é o tipo de corrida que eu gosto.</p>
<h2>O aviso pra quem já tá de dedo no teclado<span class="hx:absolute hx:-mt-20" id="o-aviso-pra-quem-já-tá-de-dedo-no-teclado"></span>
    <a href="#o-aviso-pra-quem-j%c3%a1-t%c3%a1-de-dedo-no-teclado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sim, eu conheço iRacing e os outros sims e simcades atuais. Eu tenho 12 TB de ISO e ROM no NAS, provavelmente tenho algum que você ia lembrar de citar. Não é por falta de opção que eu tô focando em emulação hoje.</p>
<p>Aliás, dos jogos novos que eu realmente tô esperando: <strong><a href="https://forza.net/"target="_blank" rel="noopener">Forza Horizon 6</a> em Tóquio</strong>, <strong><a href="https://www.assettocorsa.it/"target="_blank" rel="noopener">Assetto Corsa EVO</a></strong> e <strong><a href="https://www.assettocorsa.it/"target="_blank" rel="noopener">Assetto Corsa Rally</a></strong>. Tô jogando as demos dos dois Assetto Corsa e gostando muito das campanhas single-player. Quando eles saírem em versão final, vão virar meus jogos principais de sim na rotina. Esse artigo aqui cobre um lado diferente da coleção, que é tirar do esquecimento os clássicos que ainda não têm remaster.</p>
<p>Hoje, aqui, eu estou interessado nesses jogos específicos que listei acima. Ponto. Você faz o que você quer, eu faço o que eu quero. Se você quer iRacing, abra um Steam e vá em paz. Esse artigo é sobre mexer com emulador, preservar jogo velho, e tirar essas pérolas do esquecimento num setup Linux. Quem tá procurando review de sim atual não vai encontrar aqui.</p>
<p>Agora vamos ao que interessa.</p>
<p><strong>Observação sobre os vídeos:</strong> a maioria tá sem áudio porque eu esqueci de ligar a captura de som no OBS antes de gravar e fiquei com preguiça de re-gravar. O áudio funciona perfeito em todos os emuladores aqui, é falha minha no OBS, não do setup.</p>
<h2>Settings globais por emulador<span class="hx:absolute hx:-mt-20" id="settings-globais-por-emulador"></span>
    <a href="#settings-globais-por-emulador" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes dos jogos, vale consolidar as configs globais que valem pra tudo em cada emulador. Isso tá automatizado no repo, mas se você tá configurando na mão, aqui tá o resumo.</p>
<h3>DuckStation (PS1)<span class="hx:absolute hx:-mt-20" id="duckstation-ps1"></span>
    <a href="#duckstation-ps1" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Renderer:</strong> Vulkan</li>
<li><strong>Internal Resolution Scale:</strong> 8x (8K render, downscalado pro monitor)</li>
<li><strong>Texture Filtering:</strong> JINC2 (preserva pixel art mas suaviza)</li>
<li><strong>PGXP:</strong> ligado (corrige wobble de vértice típico do PS1)</li>
<li><strong>Widescreen Hack:</strong> ligado como fallback, mas preferência por cheat widescreen per-game</li>
<li><strong>Aspect Ratio:</strong> 16:9 com widescreen cheat, 4:3 sem</li>
</ul>
<p>PS1 hoje em dia sobe liso no DuckStation. É o emulador mais maduro da lista. A atenção vai pras escolhas per-game (alguns jogos precisam de cheat específico de widescreen).</p>
<h3>PCSX2 (PS2)<span class="hx:absolute hx:-mt-20" id="pcsx2-ps2"></span>
    <a href="#pcsx2-ps2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Renderer:</strong> Vulkan</li>
<li><strong>Upscale:</strong> 4x SSAA (1440p nativo → 4K interno)</li>
<li><strong>Anisotropic Filtering:</strong> 16x</li>
<li><strong>Post Processing:</strong> FXAA ligado, PCRTC antiblur ligado</li>
<li><strong>Deinterlacing:</strong> Automatic (alguns jogos precisam override)</li>
<li><strong>Controller:</strong> bindings Xbox-style (eu uso controle 8BitDo)</li>
</ul>
<p>PCSX2 2.7.x é muito superior ao 2.6.3 que ainda tá no repo oficial do Arch. O AUR <code>pcsx2-latest-bin</code> tracka o 2.7.x e é onde mora o texture replacement, FXAA moderno e PCRTC antiblur. Se você tá em distro diferente e tá no 2.6.3, procure o AppImage oficial.</p>
<h3>RPCS3 (PS3)<span class="hx:absolute hx:-mt-20" id="rpcs3-ps3"></span>
    <a href="#rpcs3-ps3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Renderer:</strong> Vulkan</li>
<li><strong>Resolution Scale:</strong> 300% (geralmente) ou nativo (jogos com issue de upscale)</li>
<li><strong>Shader Precision:</strong> Ultra</li>
<li><strong>Force High Precision Z:</strong> ligado</li>
<li><strong>SPU XFloat:</strong> Accurate</li>
<li><strong>Multithreaded RSX:</strong> ligado</li>
<li><strong>Write Color Buffers (WCB) / Read Color Buffers (RCB):</strong> desligados por padrão (GT-series safety)</li>
</ul>
<p>RPCS3 é onde mais tem armadilha. Cada jogo tem preset recomendado na <a href="https://rpcs3.net/compatibility"target="_blank" rel="noopener">DB de compatibilidade oficial</a>. E atenção: configs per-game ficam em <code>~/.config/rpcs3/custom_configs/config_&lt;SERIAL&gt;.yml</code>. Prefixo <code>config_</code> obrigatório. Perdi tempo até descobrir isso, o RPCS3 aceita o YAML silenciosamente e depois ignora se o nome tá errado.</p>
<h3>Xenia Canary (Xbox 360)<span class="hx:absolute hx:-mt-20" id="xenia-canary-xbox-360"></span>
    <a href="#xenia-canary-xbox-360" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Build:</strong> Canary (o <code>master</code> tá parado há meses, Canary é o que anda)</li>
<li><strong>Renderer:</strong> Vulkan</li>
<li><strong>Render Target Path:</strong> <code>rtv</code> por default, <code>fsi</code> pra jogos específicos (PGR4)</li>
<li><strong>User Profile:</strong> criado uma vez, persiste</li>
<li><strong>Wine Prefix:</strong> dedicado, gerenciado pelo Xenia Manager</li>
</ul>
<p>Xenia no Linux roda via Wine, gerenciado pelo <a href="https://github.com/xenia-manager/xenia-manager"target="_blank" rel="noopener">Xenia Manager</a>. Não tem build nativo decente. Funciona bem, mas cada jogo pede ajuste específico, e Title Updates tem que ser puxado manualmente do archive.org (escrevi um script pra isso, detalhes no artigo anterior).</p>
<h3>shadPS4 (PS4)<span class="hx:absolute hx:-mt-20" id="shadps4-ps4"></span>
    <a href="#shadps4-ps4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse merece a seção dedicada no final. Resumindo: funciona pra alguns jogos simples, pra Driveclub ainda não é confiável. Continua sendo um tiro no escuro.</p>
<h2>PS1 no DuckStation<span class="hx:absolute hx:-mt-20" id="ps1-no-duckstation"></span>
    <a href="#ps1-no-duckstation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Gran Turismo (o primeiro, de 1997)<span class="hx:absolute hx:-mt-20" id="gran-turismo-o-primeiro-de-1997"></span>
    <a href="#gran-turismo-o-primeiro-de-1997" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%201.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>Esse aqui é puro sentimento. GT1 foi um dos primeiros jogos que eu comprei pro PS1, e joguei até rachar o CD. Na época, era outro patamar. Simulação real numa máquina de 128K de RAM de vídeo, física decente, 140 carros quando todo mundo tinha 15, trilha sonora memorável.</p>
<p>Mas sejamos honestos: não dá pra voltar hoje. A física do GT1 tem drift exagerado, o carro desliza mais do que deveria em qualquer curva acima de 80 km/h. É parte do charme de 1997 mas irrita em 2026. O <strong>modelo de controle foi muito refinado a partir do GT2</strong>, e tudo que veio depois só melhorou. Entre GT1 e GT2, GT2 vence em conteúdo (650+ carros vs 140) e principalmente em dirigibilidade.</p>
<p>Eu deixo o GT1 no ES-DE pra abrir em momentos de nostalgia, jogar uns 10 minutos em Trial Mountain e fechar. Pra gameplay sério da era PS1, vai de GT2.</p>
<h3>Gran Turismo 2 (Spec II Mod)<span class="hx:absolute hx:-mt-20" id="gran-turismo-2-spec-ii-mod"></span>
    <a href="#gran-turismo-2-spec-ii-mod" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%202.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>O Gran Turismo original do PS1 é um marco histórico. Eu joguei muito na época. Hoje, pra jogar de novo? Não. GT2 existe, e GT2 é superior em tudo. Mais carros (650+), mais pistas, mais modos, e principalmente, <strong>um modelo de controle muito melhor</strong>. O GT1 tem aquele drift exagerado onde o carro desliza mais do que deveria, o GT2 ajustou isso pra algo mais próximo do que a série seria a partir do PS2. Entre jogar GT1 ou GT2, sempre GT2.</p>
<p>E o GT2 que eu rodo hoje é o <strong>GT2 Spec II Mod</strong> do <a href="https://x.com/projectaspec?lang=en"target="_blank" rel="noopener">Project A-Spec</a>, um projeto comunitário que consolida as duas variantes regionais (Arcade + Simulation) num único jogo, traz de volta eventos que foram cortados no release final, corrige bugs de física, adiciona suporte a widescreen nativo e dá quality-of-life updates em menus. É o jeito definitivo de jogar GT2 em 2026. O projeto não mantém mais site próprio, as atualizações vêm pelo Twitter do autor.</p>
<p>Pra rodar:</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disco</td>
          <td>GT2 Spec II Mod (patch aplicado sobre ISO Simulation USA)</td>
      </tr>
      <tr>
          <td>Serial</td>
          <td>SCUS-94455</td>
      </tr>
      <tr>
          <td>Widescreen cheat</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>8 MB RAM cheat</td>
          <td>Ligado (corrige áudio em pistas cheias)</td>
      </tr>
      <tr>
          <td>Texture Filter</td>
          <td>JINC2</td>
      </tr>
      <tr>
          <td>Resolution Scale</td>
          <td>8x</td>
      </tr>
  </tbody>
</table>
<p>O cheat de 8 MB RAM é importante pra pistas cheias (tipo Seattle em corridas populated) onde o áudio começa a estourar. Ele simula a expansão de memória da versão japonesa que nunca veio pro Ocidente.</p>
<h3>Colin McRae Rally 1 e 2<span class="hx:absolute hx:-mt-20" id="colin-mcrae-rally-1-e-2"></span>
    <a href="#colin-mcrae-rally-1-e-2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Jogos icônicos do PS1. CMR1 tem aquela sensação áspera de rally primeiro-gen que tem seu charme. CMR2 é tecnicamente superior, física melhor, mais rallys, melhor grafia. Mas eu vou ser honesto: <strong>pra jogar CMR2 hoje, vale procurar a versão PC original</strong>. Resolução maior, 60fps nativo (a versão PS1 trava em 30), controle direto de teclado/volante. Existem repacks do Codemasters na internet, tem umas 40 segundos num search decente. Não vou linkar, você sabe onde procurar.</p>
<p>No PS1 via DuckStation eles rodam liso, nostalgia garantida, mas não é onde eu passo o tempo.</p>
<h2>PS2 no PCSX2<span class="hx:absolute hx:-mt-20" id="ps2-no-pcsx2"></span>
    <a href="#ps2-no-pcsx2" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Gran Turismo 3 A-Spec<span class="hx:absolute hx:-mt-20" id="gran-turismo-3-a-spec"></span>
    <a href="#gran-turismo-3-a-spec" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%203.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>GT3 foi um salto geracional. Saiu do mundo quadradinho do PS1 pra um mundo de shaders, environment mapping decente, modelos de carro com polígono sério, física que finalmente começou a parecer com física de verdade. A trilha sonora com Feeder de &ldquo;Just a Day&rdquo; abrindo o jogo ainda me dá arrepio.</p>
<p>O problema é que GT3 tem MENOS conteúdo que GT2. Polyphony foi mais cautelosa, cortou modos pra focar em qualidade. Licença A/B/S é menor, carros são menos, pistas são fewer. Mas o que tem é polido.</p>
<p>Em 2026 no PCSX2 2.7.x, GT3 fica muito bom. HD textures de comunidade, widescreen pnach, anti-aliasing via SSAA, 16x AF. Parece jogo relançado.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disco</td>
          <td>GT3 A-spec (SCUS-97102) ou Bundle (PBPX-95503)</td>
      </tr>
      <tr>
          <td>Widescreen pnach</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>Pack de retextura</td>
          <td>Opcional (desativado por default, ativar se quiser ver tudo HD)</td>
      </tr>
      <tr>
          <td>Upscale</td>
          <td>4x SSAA</td>
      </tr>
      <tr>
          <td>Deinterlacing</td>
          <td>Automatic</td>
      </tr>
  </tbody>
</table>
<p>Caveat sobre o pack de retextura: quando ligado, tem cenas de mostruário de carro no garage onde o NFS stream causa um flicker momentâneo. Não incomoda em corrida. Se te irrita no menu, desliga.</p>
<p><strong>Lembrete:</strong> pro widescreen pnach fazer efeito você ainda precisa entrar nas opções <strong>dentro do jogo</strong> e mudar o aspect ratio pra 16:9. O pnach libera a opção no menu, não aplica automaticamente.</p>
<h3>Gran Turismo 4 (Spec II Mod)<span class="hx:absolute hx:-mt-20" id="gran-turismo-4-spec-ii-mod"></span>
    <a href="#gran-turismo-4-spec-ii-mod" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%204.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>GT4 é o meu GOAT da série. 700+ carros, 50+ circuitos, física ainda respeitável pra padrão 2026, e aquela trilha sonora que mistura rock alternativo, lounge japonês, e o infame &ldquo;Sail Away&rdquo; do Moby no menu principal. Foi o jogo que mais mexi em 2004, e é o que eu mais mexo hoje.</p>
<p>Igual o GT2, o GT4 que eu rodo hoje <strong>não é o disco original</strong>. É o <a href="https://www.theadmiester.co.uk/specii/"target="_blank" rel="noopener"><strong>Gran Turismo 4: Spec II</strong></a> mantido pelo TheAdmiester — uma mod comunitária massiva que é objetivamente o melhor jeito de jogar GT4 em 2026. Ela combina o disco final USA, a Beta prototype de 2003 e o release japonês original num único jogo, restaurando carros cortados (incluindo vários que nunca saíram fora do Japão), eventos cancelados, dezenas de faixas de trilha sonora faltantes, suporte a widescreen 16:9, mais de 30 cheats integrados como opções de menu (pro tuning, velocidade máxima, economia de combustível), opção de trocar entre idiomas Inglês/Japonês, e correções de bugs originais. O projeto é ativo, ainda recebe patches e tem instalador próprio que aplica sobre uma ISO USA limpa.</p>
<p>Na prática, Spec II é o que o GT4 deveria ter sido se a Polyphony tivesse tido mais seis meses pra polir. Quando você joga hoje, é GT4 com todo o conteúdo que ficou de fora + quality-of-life moderno. Pra qualquer um que gostou do original, é obrigatório.</p>
<p>Por cima disso vai o <strong>Retexture 3.0</strong> da comunidade (pack de textura HD) e o <strong>HD HUD</strong> do <a href="https://github.com/Silentwarior112/GT4-HD-HUD-Pack"target="_blank" rel="noopener">Silentwarior112</a>. Com essas três peças juntas (Spec II Mod + Retexture + HD HUD), GT4 roda a 1440p SSAA no PCSX2 2.7.x com 16x AF, FXAA, e uma cara que honestamente é indistinguível de um jogo novo em low-poly style. É o meu atual favorito pra sentar e jogar longo.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disco</td>
          <td>GT4 USA (SCUS-97328) ou Spec II (SCUS-97436, CRC <code>4CE521F2</code>)</td>
      </tr>
      <tr>
          <td>HD HUD pack</td>
          <td>Instalado via symlink (Silentwarior112)</td>
      </tr>
      <tr>
          <td>Retexture 3.0</td>
          <td>Instalado</td>
      </tr>
      <tr>
          <td>Widescreen pnach</td>
          <td>Ligado (renomeado pro CRC do Spec II se for Spec II)</td>
      </tr>
      <tr>
          <td>Silent&rsquo;s trigger/camera patches</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>Deinterlace (Spec II)</td>
          <td>Mode 8 Adaptive TFF</td>
      </tr>
      <tr>
          <td>ShadeBoost (Spec II)</td>
          <td>Saturation +10, Brightness +3, Contrast +2</td>
      </tr>
      <tr>
          <td>FXAA + PCRTC antiblur</td>
          <td>Ligado global</td>
      </tr>
      <tr>
          <td>Upscale</td>
          <td>4x SSAA</td>
      </tr>
  </tbody>
</table>
<p>Quem tá usando o Spec II Mod precisa atentar pro CRC: os HD packs do GT4 vanilla foram feitos pro <code>77E61C8A</code> (USA), então no Spec II com CRC <code>4CE521F2</code> eu linko os assets via symlink na pasta CRC-suffixed que o PCSX2 procura. O pnach de widescreen também foi renomeado pra bater com o CRC do Spec II. Sem esse cuidado, o jogo roda mas os mods não carregam.</p>
<p><strong>Lembretes in-game:</strong> igual ao GT3, o widescreen pnach só libera a opção, você ainda precisa entrar em Options dentro do jogo e trocar pra 16:9. E importante: <strong>mude o modo de vídeo pra progressive 480p</strong> (também em Options). O default é interlaced 480i, que fica péssimo em tela moderna. O Spec II Mod tem suporte nativo a progressive, é só ligar.</p>
<p>Se eu pudesse recomendar um único GT da série pra alguém começar hoje, seria esse. Se você gosta de corrida e nunca jogou GT4 full, tá perdendo.</p>
<h3>Enthusia Professional Racing<span class="hx:absolute hx:-mt-20" id="enthusia-professional-racing"></span>
    <a href="#enthusia-professional-racing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/enthusia.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>Enthusia é o jogo esquecido da Konami de 2005. Saiu na sombra do GT4 e morreu no balcão. É uma pena, porque a <strong>física do Enthusia era mais realista que a do GT4</strong>. Peso do carro, transferência de massa, comportamento em limite, tudo mais próximo do comportamento real. A comunidade sim-racing dos anos 2000 sabia disso e o jogo tem culto hoje.</p>
<p>O problema do Enthusia era o sistema de Enthusia Points: você ganhava pontos por andar limpo, perdia por bater, e a progressão do jogo dependia desses pontos. Muita gente achou punitivo. Eu achei perfeito. Forçava você a dirigir com cabeça.</p>
<p>Em PCSX2 com retexture e widescreen, o jogo fica muito bom. Recomendação forte pra quem quer algo diferente dos Gran Turismo.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disco</td>
          <td>Enthusia Professional Racing (SLUS-20967)</td>
      </tr>
      <tr>
          <td>Widescreen pnach</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>Retexture</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>Upscale</td>
          <td>4x SSAA</td>
      </tr>
  </tbody>
</table>
<h3>Ridge Racer V<span class="hx:absolute hx:-mt-20" id="ridge-racer-v"></span>
    <a href="#ridge-racer-v" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/ridge%20racer%20v.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>Ridge Racer é sua própria coisa. Não é simcade, é arcade puro. Drift tudo, colinha dinâmica, trilha sonora eletrônica, cores saturadas. Ridge Racer V foi o jogo de lançamento do PS2 no Japão e por isso tem aquele cheiro de &ldquo;mostra o que o console pode fazer&rdquo;, com cenários colossais e sensação de velocidade que GT nunca tentou.</p>
<p>No PCSX2 ele roda bem, com uma caveira: textura de carro às vezes flicka em hardware renderer (issues <a href="https://github.com/PCSX2/pcsx2/issues/3639"target="_blank" rel="noopener">#3639</a> e <a href="https://github.com/PCSX2/pcsx2/issues/13729"target="_blank" rel="noopener">#13729</a> do PCSX2). Software renderer resolve mas mata performance. Eu fico no hardware e aceito o flicker ocasional.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disco</td>
          <td>Ridge Racer V (SLUS-20002)</td>
      </tr>
      <tr>
          <td>Widescreen pnach</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>No-interlace pnach</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>Upscale</td>
          <td>4x SSAA</td>
      </tr>
  </tbody>
</table>
<h3>Colin McRae Rally 3, 4, 5 (PS2)<span class="hx:absolute hx:-mt-20" id="colin-mcrae-rally-3-4-5-ps2"></span>
    <a href="#colin-mcrae-rally-3-4-5-ps2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Versões de PS2 dos Colin McRae rodam bem no PCSX2 com configs default. CMR3 e CMR04 são bons. CMR05 (o último antes do rebrand pra Dirt) é o melhor dos três tecnicamente. Mas de novo: <strong>se você tá em PC, procura a versão nativa</strong>. Os ports de PS2 eram relativamente inferiores aos PC da época, rodavam em resolução menor, e o PC moderno com repack vai dar 144Hz fácil.</p>
<p>Fica o registro no PCSX2 pra completude da coleção.</p>
<h2>PS3 no RPCS3<span class="hx:absolute hx:-mt-20" id="ps3-no-rpcs3"></span>
    <a href="#ps3-no-rpcs3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Gran Turismo 5<span class="hx:absolute hx:-mt-20" id="gran-turismo-5"></span>
    <a href="#gran-turismo-5" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%205.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>GT5 é controverso. Polyphony prometeu mundos e entregou um jogo meio fragmentado: modelos Premium lindos ao lado de Standard que eram basicamente carros do GT4 em HD, sistema de níveis que alguns amaram e outros detestaram, online que foi pra lama quando o serviço PSN desligou, modo Top Gear que era estranho. A crítica na época foi mista, e eu lembro de comunidades inteiras brigando sobre se o jogo era decepção ou genial.</p>
<p>Pra mim, é genial. GT5 tem o Course Maker (edita tua própria pista baseada em seções reais), o Nurburgring 24h real com chuva dinâmica, o Moon Rover (sim, modo de dirigir na Lua), Red Bull X-Challenge com Vettel, e a experiência de endurance single-player é ridiculamente grande. Quem entrou no jogo a fundo sabe que tem conteúdo pra ano inteiro.</p>
<p>No RPCS3 em 2026, GT5 roda <strong>muito bem</strong>. Estável, sem os bugs graves de anos passados. É o GT mais estável que eu tenho emulado hoje.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Serial</td>
          <td>BCUS98114 (US) ou BCES00569 (EU)</td>
      </tr>
      <tr>
          <td>Resolution Scale</td>
          <td>300%</td>
      </tr>
      <tr>
          <td>Shader Precision</td>
          <td>Ultra</td>
      </tr>
      <tr>
          <td>Force High Precision Z</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>SPU XFloat</td>
          <td>Accurate</td>
      </tr>
      <tr>
          <td>Multithreaded RSX</td>
          <td>Ligado</td>
      </tr>
      <tr>
          <td>WCB / RCB</td>
          <td>Desligados</td>
      </tr>
  </tbody>
</table>
<p>A combinação Shader Precision Ultra + Force High Precision Z mata o dithering típico do RSX que fazia GT5 parecer granulado em RPCS3 antigamente. Sem esses dois, o asfalto parece ter ruído constante. Com eles, o jogo fica limpo.</p>
<h3>Gran Turismo 6<span class="hx:absolute hx:-mt-20" id="gran-turismo-6"></span>
    <a href="#gran-turismo-6" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/gran%20turismo%206.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>GT6 é um dos meus preferidos da série. Saiu em 2013 pra um PS3 no final de vida, enquanto a concorrência já olhava pra PS4. Vendeu mal. Muita gente não jogou. E é uma pena, porque GT6 pegou a base do GT5, cortou a gordura (Standards foram reduzidos), adicionou mais Premiums, sessões em circuito no Moon melhores, meteorologia dinâmica em mais pistas, refinamento de física. É basicamente o GT5 polido.</p>
<p>Em conteúdo, GT6 é comparável ao GT4. Muitos jogadores da velha guarda botam o GT4 acima por causa do charme da era PS2. Eu fico entre os dois e vou mais pro GT6 em dia normal, GT4 em dia nostálgico.</p>
<p>Em RPCS3, GT6 tem uma pegadinha grave: <strong>patches 1.06 em diante regridem visualmente</strong>. Superfícies pretas em carros no menu garagem, flicker em cockpit view, tela inteira piscando em certas pistas. A versão que funciona bem em 2026 é <strong>travada em v1.05</strong>. O script <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/scripts/extract_ps3_dlc.py"target="_blank" rel="noopener"><code>extract_ps3_dlc.py</code></a> do repo tem um ceiling por título específico pra pinar GT6 em 1.05 mesmo quando o PSN cuspa 1.22 como &ldquo;mais recente&rdquo;.</p>
<p>Além disso, Force CPU Blit é mandatório. Sem ele, tem flicker de tela inteira em menu. O trade-off é que o retrovisor fica permanentemente preto. Eu prefiro perder o retrovisor a ter o flicker. Quem quer retrovisor pode ligar Write Color Buffers, mas aí a resolução cai pra 720p nativo.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Serial</td>
          <td>BCES01893 + variantes regionais</td>
      </tr>
      <tr>
          <td>Versão travada</td>
          <td>v1.05 (patches 1.06+ causam black surfaces)</td>
      </tr>
      <tr>
          <td>Resolution Scale</td>
          <td>300% (menus) ou 200% (gameplay pesada)</td>
      </tr>
      <tr>
          <td>Force CPU Blit</td>
          <td>Ligado (mandatório, mata flicker)</td>
      </tr>
      <tr>
          <td>WCB</td>
          <td>Desligado (trade-off: retrovisor preto)</td>
      </tr>
      <tr>
          <td>Shader Precision</td>
          <td>Ultra</td>
      </tr>
  </tbody>
</table>
<p>Em 2026, com essas configs, GT6 tá no meu rodízio. Não é perfeito, mas é jogável o suficiente pra eu passar uma tarde fazendo endurance.</p>
<h3>Ridge Racer 7<span class="hx:absolute hx:-mt-20" id="ridge-racer-7"></span>
    <a href="#ridge-racer-7" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Ridge Racer 7 foi lançamento do PS3 em 2006 e é o último mainline da franquia. Arcade igual Ridge Racer V, drift-first, trilha sonora eletrônica, câmera cinemática. Pra PS3 da época era uma demo tecnológica bonita.</p>
<p>No RPCS3 ele roda, mas com caveats: requer Write Color Buffers ligado pra não ter problemas de iluminação, e o preset recomendado na compat DB é diferente do meu preset GT. Eu uso config per-game dedicada pra ele. Não é meu favorito, mas tá lá pra completude.</p>
<p>Sem vídeo pra esse, gravo depois.</p>
<h2>Xbox 360 no Xenia Canary<span class="hx:absolute hx:-mt-20" id="xbox-360-no-xenia-canary"></span>
    <a href="#xbox-360-no-xenia-canary" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui tá a parte que mais me deu dor de cabeça nesse domingo. Xbox 360 no Linux é território hostil. Xenia roda via Wine, cada jogo tem tweaks específicos, title updates tem que vir do archive.org, e Forza Motorsport 4 especificamente tinha problemas sérios até pouco tempo atrás.</p>
<h3>Forza Motorsport 4 (o GOAT)<span class="hx:absolute hx:-mt-20" id="forza-motorsport-4-o-goat"></span>
    <a href="#forza-motorsport-4-o-goat" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20motorsport%204.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>FM4 é, pra muita gente, o melhor Forza Motorsport já lançado. Polyphony fez Gran Turismo, Turn 10 fez Forza, e FM4 é o pico da era 360. Modelos de carro belíssimos pra época, pistas bem construídas, física sólida sem ser simulador puro, Autovista mode com narração do Jeremy Clarkson, trilha sonora épica, e aquela sensação de Xbox 360 sendo usado no limite.</p>
<p>FM4 no Xenia <strong>era um pesadelo até recentemente</strong>. Shadow com artefato severo, texture pop-in constante no carro, textura de pista chiada e pixelada, céu com brilho completamente errado (ficava branco-estourado em vez do azul profundo original), e o pior: crash de áudio frequente, onde o XMA (codec proprietário do Xbox 360) travava e o som virava ruído agudo até congelar o jogo. Eu passei horas nesse domingo mexendo em config, testando builds, procurando issue no xenia-canary, testando diferentes title updates.</p>
<p>O que destravou:</p>
<ul>
<li><strong>Build Xenia Canary mais recente</strong> (o <code>master</code> tá parado, Canary acompanha desenvolvimento ativo)</li>
<li><strong>Render Target Path:</strong> <code>rtv</code> (o default funciona bem pra FM4)</li>
<li><strong>Title Update 1.0.17.0</strong> instalado via Xenia Manager (versão importa, mais velha dá regressão em sombras)</li>
<li><strong>GPU Vulkan com ICD NVIDIA forçado</strong> (no meu setup híbrido com AMD iGPU, tinha que forçar a 5090)</li>
</ul>
<p>Depois disso, FM4 rodou. Não é perfeito. Ainda tem algum crash esporádico de áudio (<a href="https://github.com/xenia-canary/xenia-canary/issues/161"target="_blank" rel="noopener">xenia-canary issue #161</a> aberta). Mas é jogável, e eu passei umas duas horas do domingo no Top Gear Test Track só apreciando o Koenigsegg CCX. Valeu cada hora de debug.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Xenia Canary recente</td>
      </tr>
      <tr>
          <td>Render Target Path</td>
          <td><code>rtv</code> (default)</td>
      </tr>
      <tr>
          <td>Title Update</td>
          <td>1.0.17.0</td>
      </tr>
      <tr>
          <td>Wine Prefix</td>
          <td>dedicado via Xenia Manager</td>
      </tr>
      <tr>
          <td>GPU</td>
          <td>NVIDIA via <code>VK_ICD_FILENAMES</code> hardforce</td>
      </tr>
  </tbody>
</table>
<h3>Forza Motorsport 3<span class="hx:absolute hx:-mt-20" id="forza-motorsport-3"></span>
    <a href="#forza-motorsport-3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20motorsport%203.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>FM3 é uma das entradas boas da série, entre o charme áspero do FM2 e o pico técnico do FM4. Tentar emular foi surpreendentemente chato, porque eu tava usando a ISO errada.</p>
<p>Existem dois releases do FM3: o <strong>retail</strong> (disco original vendido em loja) e o <strong>Ultimate Edition</strong> (com todas as DLCs empacotadas num segundo disco). Eu tava tentando rodar o Ultimate, que combina dois discos numa ISO híbrida que o Xenia não parseia direito. Troquei pro retail padrão + DLCs instaladas separadamente via Title Update, e bingo, o jogo rodou na primeira.</p>
<p>Moral: se teu FM3 não tá rodando no Xenia, verifica se é retail. Ultimate dá problema.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Versão</td>
          <td>Retail (NÃO Ultimate Edition)</td>
      </tr>
      <tr>
          <td>Build</td>
          <td>Xenia Canary recente</td>
      </tr>
      <tr>
          <td>Render Target Path</td>
          <td><code>rtv</code></td>
      </tr>
      <tr>
          <td>DLCs</td>
          <td>Instaladas via Xenia Manager separadamente</td>
      </tr>
  </tbody>
</table>
<h3>Forza Motorsport 2<span class="hx:absolute hx:-mt-20" id="forza-motorsport-2"></span>
    <a href="#forza-motorsport-2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20motorsport%202.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>FM2 é da era 2007, primeira-gen do 360. Sistemas menos refinados, modelagem mais básica, mas aquele charme de Forza original que alguns preferem à sofisticação do FM4. No Xenia rodou praticamente out-of-the-box. Nenhum tweak especial.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Xenia Canary</td>
      </tr>
      <tr>
          <td>Render Target Path</td>
          <td>default</td>
      </tr>
  </tbody>
</table>
<h3>Forza Horizon 2<span class="hx:absolute hx:-mt-20" id="forza-horizon-2"></span>
    <a href="#forza-horizon-2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20horizon%202.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>FH2 foi o Forza Horizon mais polido da geração 360 (existe também a versão Xbox One, que é melhor tecnicamente, mas o 360 tem seu charme por causa do mapa menor e da progressão mais focada). Mundo aberto no sul da Europa, trilha sonora icônica, festival Horizon no seu pico.</p>
<p>No Xenia rodou sem pedir nada extra. Como o FM2.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Xenia Canary</td>
      </tr>
      <tr>
          <td>Render Target Path</td>
          <td>default</td>
      </tr>
  </tbody>
</table>
<h3>Forza Horizon 1 (com XE Mod)<span class="hx:absolute hx:-mt-20" id="forza-horizon-1-com-xe-mod"></span>
    <a href="#forza-horizon-1-com-xe-mod" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20horizon%201%20xe%20mod.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>FH1 original roda OK no Xenia, mas tem uma comunidade que mantém o <strong>Forza Horizon 1 XE Mod</strong>, que melhora IA do tráfego, rebalanceia progressão, adiciona carros que ficaram de fora, corrige bugs conhecidos, e traz uns quality-of-life tweaks na UI. Eu testei o XE Mod nesse domingo e vale a pena. O jogo parece mais vivo e progressão mais justa.</p>
<p>Detalhes da instalação do XE Mod estão no site do projeto. Basicamente é patch sobre a ISO original + alguns title updates customizados.</p>
<p>Pra comparação, aqui é o FH1 <strong>sem</strong> o mod, rodando puro:</p>
<video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/forza%20horizon%201.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>A diferença mais visível é no tráfego e na variedade de situações que aparecem, mas o XE Mod tem muita coisa que você só percebe depois de algumas horas.</p>
<table>
  <thead>
      <tr>
          <th>Config</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Xenia Canary</td>
      </tr>
      <tr>
          <td>XE Mod</td>
          <td>Aplicado sobre ISO original</td>
      </tr>
      <tr>
          <td>Title Updates</td>
          <td>Version pinada pelo XE Mod</td>
      </tr>
  </tbody>
</table>
<h3>Project Gotham Racing 3 e 4<span class="hx:absolute hx:-mt-20" id="project-gotham-racing-3-e-4"></span>
    <a href="#project-gotham-racing-3-e-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>PGR3 foi lançamento do 360 em 2005 e continua lindo hoje. Pistas urbanas (Nova York, Tokyo, Nurburgring, Las Vegas), kudos system, câmera cinemática. PGR4 adicionou clima dinâmico (chuva e neve) e motos (algumas pessoas adoram, eu prefiro fingir que não existe).</p>
<p>PGR3 no Xenia Canary roda razoavelmente bem com configs default. PGR4 precisa de tweak específico: <strong><code>render_target_path_vulkan = &quot;fsi&quot;</code></strong> no config. Sem isso, algumas pistas quebram com artefato visual severo (barras verdes atravessando a tela em corridas de neve).</p>
<p>E PGR4 no NVIDIA tem um bug conhecido de áudio: decoding XMA gera garbage intermitente (issue aberta no xenia-canary, sem resolução). Não é game-breaking, mas incomoda.</p>
<table>
  <thead>
      <tr>
          <th>Jogo</th>
          <th>Config relevante</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PGR3</td>
          <td>Default</td>
      </tr>
      <tr>
          <td>PGR4</td>
          <td><code>render_target_path_vulkan = &quot;fsi&quot;</code>, aceitar bug de áudio XMA</td>
      </tr>
  </tbody>
</table>
<h3>Ridge Racer 6<span class="hx:absolute hx:-mt-20" id="ridge-racer-6"></span>
    <a href="#ridge-racer-6" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Ridge Racer 6 foi o Ridge Racer do 360, pula o 7 que ficou em PS3. Arcade puro, igual Ridge Racer V do PS2. No Xenia Canary roda com config default, sem tweak especial. É divertido pra sessão curta. Sem vídeo dele no artigo, mas tá no catálogo rodando.</p>
<h2>PS4 no shadPS4: o estado do emulador<span class="hx:absolute hx:-mt-20" id="ps4-no-shadps4-o-estado-do-emulador"></span>
    <a href="#ps4-no-shadps4-o-estado-do-emulador" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://shadps4.net/"target="_blank" rel="noopener">shadPS4</a> <strong>ainda não tem release estável</strong>. O projeto tá em desenvolvimento ativo, main branch muda várias vezes por semana, e &ldquo;o que funciona hoje&rdquo; pode regredir num rebase amanhã. Dito isso, <strong>muitos jogos já bootam e rodam bem o suficiente</strong>. Lista da comunidade tá no <a href="https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues"target="_blank" rel="noopener">compatibility tracker</a> e cresce toda semana.</p>
<p>A diferença entre shadPS4 e os outros emuladores desse artigo é o tipo de esforço. PCSX2, RPCS3, Xenia são projetos maduros onde cada jogo tem uma página de wiki razoavelmente atualizada. shadPS4 é o oposto: você lê um guia do Reddit, tenta reproduzir, descobre que o guia usa um fork específico (não o main oficial), ou um commit de dois meses atrás, ou um firmware com <code>sys_modules</code> linkados de jeito particular, ou patches XML aplicados numa ordem específica que o autor não documentou. Reproduzir setup de shadPS4 a partir de vídeo YouTube é tentar resolver Rubik&rsquo;s Cube com venda.</p>
<p>Mas isso não significa que você não deve tentar. Significa que você precisa tratar shadPS4 como brincadeira, não como workflow. E se você tiver paciência, dá pra colher resultados impressionantes.</p>
<h2>Driveclub: o impossível, finalmente possível<span class="hx:absolute hx:-mt-20" id="driveclub-o-impossível-finalmente-possível"></span>
    <a href="#driveclub-o-imposs%c3%advel-finalmente-poss%c3%advel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><video
  controls
  preload="metadata"
  playsinline
  class="html5-video"
  style="width:100%; max-width:960px; display:block; margin:1.25em auto; border-radius:8px; background:#000;"
  >
  <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/videos/driveclub.mp4" type="video/mp4">
  Your browser does not support HTML5 video.
</video>

<p>Driveclub é meu Moby Dick da emulação. É o único jogo de PS4 que eu quero muito rodar. Saiu em 2014 pela Evolution Studios, teve lançamento catastrófico (servers online colapsaram no dia 1), foi patchado durante dois anos até virar um dos melhores jogos de corrida da geração, e foi descontinuado pela Sony quando a Evolution foi fechada em 2016. Não tem port pra PC. Não tem remaster. Não tem sequel. Só existe no PS4.</p>
<p>E hoje, depois de literalmente horas de debug, <strong>está rodando aqui</strong>. Menu principal carrega, intro roda, corridas começam, controle responde, áudio funciona. Não é perfeito (SDR dim porque o jogo foi tonemapado pra HDR TV, fica a 30 FPS nativo, dither ocasional em superfícies brilhantes na NVIDIA), mas é <strong>jogável</strong>. Eu dei pole position em uma corrida enquanto validava o setup pra gravar o vídeo acima.</p>
<p>Chegar até aqui exigiu desarmar várias armadilhas. Vale documentar todas, porque a maioria dos guias online não menciona nenhuma.</p>
<h3>Armadilha 1: o FMOD loop error que não era FMOD<span class="hx:absolute hx:-mt-20" id="armadilha-1-o-fmod-loop-error-que-não-era-fmod"></span>
    <a href="#armadilha-1-o-fmod-loop-error-que-n%c3%a3o-era-fmod" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando você tenta bootar Driveclub no shadPS4 direto da PKG retail, o log estoura um loop infinito de:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/app0/audio/fmodstudio/masterbank.bank failed, file does not exist</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A primeira reação é investigar FMOD. Tem gente na internet que passou horas mexendo em sceFont, libtbb, sceNgs2, convencidos que era problema de áudio ou lib HLE. <strong>Nada disso é a causa</strong>. O arquivo <code>masterbank.bank</code> literalmente não existe no disco, porque ele tá empacotado dentro dos arquivos <code>gameNNN.dat</code> num formato de archive customizado da Evolution Studios que o VFS do shadPS4 não sabe ler.</p>
<p>A única ferramenta pública que parse esse formato é o <strong><a href="https://github.com/Nenkai/DriveClubFS"target="_blank" rel="noopener">DriveClubFS do Nenkai</a></strong>. É uma app .NET que walk o <code>game.ndx</code> (índice) e decomprime os <code>.dat</code> em arquivos soltos. Você extrai a PKG com <a href="https://github.com/shadps4-emu/ShadPKG"target="_blank" rel="noopener">ShadPKG</a>, depois passa o DriveClubFS por cima, e fica com 5343 arquivos soltos (~25 GB). Aí o shadPS4 consegue ler.</p>
<p>Armadilha dentro da armadilha: o projeto DriveClubFS tá em <code>net9.0</code>, e o Arch tem <code>net8</code> e <code>net10</code>. Precisa retarget o csproj pra <code>net10.0</code> antes de buildar. O comando exato tá documentado no <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a> do repo.</p>
<h3>Armadilha 2: v1.00 vs v1.28<span class="hx:absolute hx:-mt-20" id="armadilha-2-v100-vs-v128"></span>
    <a href="#armadilha-2-v100-vs-v128" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Driveclub teve patches até 1.28. A intuição é usar a versão mais recente. Errado. <strong>Só a v1.00 funciona</strong> com o DriveClubFS. Quando você tenta mesclar a base v1.00 com o patch v1.28, o DriveClubFS crasha com <code>EndOfStreamException</code> no arquivo 12 de 8018. A v1.28 tem um formato de <code>.dat</code> que o DriveClubFS (v1.1.0 atual) não processa.</p>
<p>A consequência é aceitar v1.00 como ponto de entrada. Sem patches, sem conteúdo adicional, sem os mapas do Japão, sem VR-sourced tracks que vieram no último update de 2016. É o jogo básico. Em troca, você tem o jogo rodando.</p>
<h3>Armadilha 3: o patch XML de 60fps corrompe o v1.00<span class="hx:absolute hx:-mt-20" id="armadilha-3-o-patch-xml-de-60fps-corrompe-o-v100"></span>
    <a href="#armadilha-3-o-patch-xml-de-60fps-corrompe-o-v100" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Tem um <code>Driveclub.xml</code> patch famoso que faz o jogo rodar a 60fps. Os byte offsets dele foram feitos pra v1.28 eboot. Se você aplica no v1.00, ele corrompe código ao vivo, e o jogo crasha logo após boot.</p>
<p>Solução: desabilitar o patch XML (renomear pra <code>.disabled-for-v1.0</code>). Você fica a 30 FPS nativo. Isso é aceitar o trade-off. Se aparecer um patch de 60fps compatível com v1.00 no futuro, legal, até lá, 30 FPS é o que tem.</p>
<h3>Armadilha 4: o <code>fontlib</code> PR que parecia ser a solução e não era<span class="hx:absolute hx:-mt-20" id="armadilha-4-o-fontlib-pr-que-parecia-ser-a-solução-e-não-era"></span>
    <a href="#armadilha-4-o-fontlib-pr-que-parecia-ser-a-solu%c3%a7%c3%a3o-e-n%c3%a3o-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O crash <code>0x29</code> durante boot fez o Claude sugerir que era problema de font HLE. Eu gastei duas a três horas nisso. Fui atrás do <a href="https://github.com/shadps4-emu/shadPS4/pull/3772"target="_blank" rel="noopener">PR #3772</a> que adiciona fontlib, compilei fork do shadPS4 com ele, dump de fontes SST* da PS4 (que requer hardware jailbroken, descobri isso depois), experimento de substituição das fontes com Adobe Source Han Sans renomeadas pros nomes que o shadPS4 espera. Tudo isso ignorado em main: font HLE já tinha sido mergido no main via <a href="https://github.com/shadps4-emu/shadPS4/pull/2761"target="_blank" rel="noopener">PR #2761</a> em novembro de 2025, e o crash <code>0x29</code> não era font afinal.</p>
<p>Moral: <strong>usa o main branch stock</strong>. Nada de fork, nada de compilar, nada de PR não mergido. O stock do CI é suficiente.</p>
<h3>Armadilha 5: TOML silenciosamente ignorado<span class="hx:absolute hx:-mt-20" id="armadilha-5-toml-silenciosamente-ignorado"></span>
    <a href="#armadilha-5-toml-silenciosamente-ignorado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O shadPS4 migrou de TOML pra JSON em config per-game em late 2025. Se você tem um <code>CUSA00003.toml</code> antigo na pasta <code>custom_configs/</code>, ele é silenciosamente ignorado, sem warning no log. Você pensa que sua config tá ativa, na prática tá rodando tudo default.</p>
<p>Solução: deleta qualquer TOML e usa <code>CUSA00003.json</code>. As configs per-game que funcionam pra Driveclub são específicas e detalhadas, documentei todas no <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a> do repo.</p>
<h3>Armadilha 6: controle não detectado sem hidapi hints<span class="hx:absolute hx:-mt-20" id="armadilha-6-controle-não-detectado-sem-hidapi-hints"></span>
    <a href="#armadilha-6-controle-n%c3%a3o-detectado-sem-hidapi-hints" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Qt Launcher do shadPS4 chama um <code>Shadps4-sdl.AppImage</code> interno. Sem env vars específicas do SDL hidapi, o controle (no meu caso 8BitDo Ultimate 2) não é detectado, mesmo funcionando em outros emuladores. O fix é wrapar o AppImage num shell que exporta <code>SDL_JOYSTICK_HIDAPI=1</code> e variantes específicas por plataforma (PS4, PS5, Xbox), antes de executar o original.</p>
<p>Plugar o controle <strong>antes</strong> de abrir o Qt Launcher também ajuda. Hot-plug funciona durante o jogo, não antes.</p>
<h3>O que finalmente funcionou<span class="hx:absolute hx:-mt-20" id="o-que-finalmente-funcionou"></span>
    <a href="#o-que-finalmente-funcionou" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Depois de todas essas armadilhas, a receita é:</p>
<table>
  <thead>
      <tr>
          <th>Componente</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>shadPS4</td>
          <td>main branch, commit <code>90b75ea</code> ou mais recente, stock AppImage do CI</td>
      </tr>
      <tr>
          <td>Driveclub</td>
          <td><strong>v1.00 base PKG apenas</strong> (sem patches)</td>
      </tr>
      <tr>
          <td>Extração</td>
          <td><a href="https://github.com/shadps4-emu/ShadPKG"target="_blank" rel="noopener">ShadPKG</a> → <a href="https://github.com/Nenkai/DriveClubFS"target="_blank" rel="noopener">DriveClubFS</a> (retargetado pra net10.0)</td>
      </tr>
      <tr>
          <td>Estrutura</td>
          <td>loose files, 5343 arquivos, ~25 GB (pode deletar os <code>.dat</code> originais depois)</td>
      </tr>
      <tr>
          <td>Config per-game</td>
          <td>JSON em <code>custom_configs/CUSA00003.json</code> (não TOML)</td>
      </tr>
      <tr>
          <td><code>readbacks_mode</code></td>
          <td>0 (mandatório, <a href="https://github.com/shadps4-emu/shadPS4/issues/3210"target="_blank" rel="noopener">issue #3210</a>)</td>
      </tr>
      <tr>
          <td><code>vblank_frequency</code></td>
          <td>60</td>
      </tr>
      <tr>
          <td><code>gpu_id</code></td>
          <td>0 (NVIDIA dGPU hardforced)</td>
      </tr>
      <tr>
          <td><code>Driveclub.xml</code> patch</td>
          <td><strong>Desabilitado</strong> (não compatível com v1.00)</td>
      </tr>
      <tr>
          <td>Controle</td>
          <td>Wrapper AppImage com <code>SDL_JOYSTICK_HIDAPI=1</code> + variantes</td>
      </tr>
      <tr>
          <td>Qt Launcher</td>
          <td><code>checkForUpdates=false</code> pra evitar dialog rate-limit do GitHub</td>
      </tr>
  </tbody>
</table>
<p>Tudo isso tá encapsulado no <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a> do repo, com o passo a passo exato de extração, config e wrapper.</p>
<h3>Quão jogável tá hoje<span class="hx:absolute hx:-mt-20" id="quão-jogável-tá-hoje"></span>
    <a href="#qu%c3%a3o-jog%c3%a1vel-t%c3%a1-hoje" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Honestamente bom. O vídeo acima é gameplay real, a 30 FPS estáveis, com física respondendo, áudio sincronizado, HUD correto. O que falta:</p>
<ul>
<li><strong>SDR dim:</strong> Driveclub foi tonemapado pra HDR TV de PS4. Sem HDR no monitor + sem ShadeBoost interno no shadPS4, o jogo fica visualmente escuro. Parece com o jogo em noite permanente. vkBasalt tentou ajudar e quebrou a imagem em RTX 5090 / Vulkan 1.4. Hyprland <code>screen_shader</code> funciona mas tem seus próprios bugs. <strong>Aceitei o look dim</strong>. É o que aparece nos vídeos da comunidade também.</li>
<li><strong>30 FPS:</strong> patch de 60fps não existe pra v1.00. Aceitei.</li>
<li><strong>Dither em superfícies brilhantes:</strong> shiny reflections têm padrão dither na NVIDIA. Reportadamente é melhor em AMD. Menor.</li>
</ul>
<p><strong>&ldquo;Achievement Unlocked&rdquo;.</strong> Não é um estado perfeito, mas <strong>o jogo roda</strong>, com som, com controle, com jogabilidade, e eu consigo completar corrida inteira sem crash. Isso é um marco pessoal. Eu passei anos tentando fazer esse jogo rodar no Linux e chegava num ponto onde simplesmente desistia. Agora é parte do meu ES-DE, atalho no desktop, tá lá esperando eu sentar pra jogar.</p>
<p>Se você quer replicar, o repo tá público. Boa sorte. Vai precisar de paciência.</p>
<h2>&ldquo;Achievement Unlocked&rdquo;<span class="hx:absolute hx:-mt-20" id="achievement-unlocked"></span>
    <a href="#achievement-unlocked" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu venho tentando fazer esses jogos funcionarem, em algum nível, desde que cada respectivo emulador saiu de alpha. GT2 eu tô rodando desde ePSXe. GT4 eu mexo desde PCSX2 0.9.6. FM4 eu tentei fazer rodar em Xenia duas vezes em anos anteriores e desisti nas duas. E Driveclub eu olhei pra cara do shadPS4 três vezes antes desse domingo e engoli em seco.</p>
<p>Hoje fechou. Os dois monstros que abriram esse artigo (Driveclub no shadPS4 e FM4 com Project Forza Plus no Xenia) estão rodando, junto com todo o resto da coleção de Gran Turismo, Forza, Ridge Racer, Colin McRae e Enthusia. Tudo num setup único, reproduzível, automatizado. Agora eu faço parte do <strong>grupo bem pequeno de pessoas que conseguiu brute force emular Driveclub no Linux</strong>, e isso sozinho já tornou o domingo inesquecível.</p>
<p>A frustração de anos não era falta de informação. Era o OPOSTO. Informação demais, mal indexada, espalhada em fóruns apagados, vídeos YouTube que ficaram datados, threads Reddit com soluções pra versão do emulador de dois anos atrás que não valem hoje, wiki de projeto desatualizada em metade das páginas, e mods com requisitos MUITO específicos de CRC, versão, patch, path, setting. Cada vez que eu sentava pra fazer um desses jogos rodar direito, eu ficava três horas lendo material conflitante antes de escrever a primeira linha de config. Pior ainda quando o material bom estava enterrado entre diagnósticos errados, tipo o FMOD loop do Driveclub que todo mundo investigava como se fosse problema de áudio quando na real era formato de archive proprietário da Evolution Studios.</p>
<p>O que mudou nesse último ano é que Claude Code consegue ser meu agregador. Eu peço pro Claude ir na issue tracker do Xenia, na wiki do PCSX2, no subreddit do RPCS3, nos PRs do shadPS4, comparar todas as informações com a versão atual do emulador instalada, cruzar com os logs que eu tô vendo, e me devolver a combinação que funciona hoje. Ele lê source code quando precisa, entende por que <code>Force CPU Blit</code> muda comportamento no GT6, acha qual title update do FM4 foi o que consertou o shadow bug, descobre que o DriveClubFS precisa de retarget pra .NET 10 antes de compilar. O que antes levava três fins de semana pesquisando agora leva uma tarde de execução assistida.</p>
<p>Esse domingo é o resultado consolidado de todo esse trabalho. <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">distrobox-gaming</a> é a versão desse conhecimento virada código, reprodutível, automatizada. Rodando numa máquina fresca, um <code>ansible-playbook site.yml</code> me põe de volta nesse estado em menos de duas horas. Menos de 10 comandos separam &ldquo;Arch Linux vanilla&rdquo; de &ldquo;Driveclub rodando&rdquo;.</p>
<p>Se alguém quiser contribuir, testar em outro hardware, reportar bug ou mandar config que ficou faltando, o repo tá aberto. Quanto mais gente testar em setups diferentes, mais resiliente o projeto fica. Se você melhorar a situação do Driveclub (ShadeBoost interno, DLC funcionando em v1.00, patch de 60fps compatível com v1.00, dump de fontes que não precise hardware jailbroken), manda PR que eu abraço.</p>
<p>Por enquanto, eu vou fazer uma corrida na Islândia no Driveclub. Boa noite de domingo.</p>
]]></content:encoded><category>gaming</category><category>emulation</category><category>linux</category><category>distrobox</category><category>racing</category><category>gran-turismo</category><category>forza</category></item><item><title>LLM Benchmarks Parte 2: Vale Combinar Múltiplos Modelos no Mesmo Projeto? Claude + GLM??</title><link>https://www.akitaonrails.com/2026/04/18/llm-benchmarks-parte-2-multiplos-modelos/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/18/llm-benchmarks-parte-2-multiplos-modelos/</guid><pubDate>Sat, 18 Apr 2026 14:00:00 GMT</pubDate><description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Sim, o título é clickbait. A resposta é não, não vale. Continue usando Claude Code com Opus 4.6 ou 4.7. Detalhes abaixo.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Escrevi algumas semanas atrás um &lt;a href="https://www.akitaonrails.com/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/"&gt;benchmark detalhado de LLMs pra coding&lt;/a&gt; comparando 33 modelos open source e comerciais num mesmo teste: construir um app Rails com RubyLLM. A conclusão foi que só 4 modelos geraram código que funciona de primeira (os dois Claudes, GLM 5 e GLM 5.1), e que pra quem não quer perder tempo corrigindo alucinação de API, Claude Opus via Claude Code continua sendo a escolha mais racional apesar do preço.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>TL;DR:</strong> Sim, o título é clickbait. A resposta é não, não vale. Continue usando Claude Code com Opus 4.6 ou 4.7. Detalhes abaixo.</p>
<hr>
<p>Escrevi algumas semanas atrás um <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">benchmark detalhado de LLMs pra coding</a> comparando 33 modelos open source e comerciais num mesmo teste: construir um app Rails com RubyLLM. A conclusão foi que só 4 modelos geraram código que funciona de primeira (os dois Claudes, GLM 5 e GLM 5.1), e que pra quem não quer perder tempo corrigindo alucinação de API, Claude Opus via Claude Code continua sendo a escolha mais racional apesar do preço.</p>
<p>Esse artigo é continuação. Vou manter aquele benchmark atualizado à medida que modelos novos saem (Opus 4.7, Qwen 3.6, GPT 5.4 via Codex já entraram), e esse aqui aborda uma pergunta diferente que aparece toda semana no meu feed: <strong>e se eu combinar dois modelos no mesmo projeto? Opus pra planejar, GLM pra executar. Dá certo?</strong></p>
<p>A resposta curta é não, não dá. A resposta longa é o resto desse artigo.</p>
<h2>Antes: uma palavra sobre Opus 4.7<span class="hx:absolute hx:-mt-20" id="antes-uma-palavra-sobre-opus-47"></span>
    <a href="#antes-uma-palavra-sobre-opus-47" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem gente no Reddit dizendo que Opus 4.7 é um downgrade absurdo do 4.6, que regrediu, que &ldquo;piorou pra coding&rdquo;. Fico desconfiado sempre que vejo essa narrativa de &ldquo;tá tudo ficando pior&rdquo;. Eu tenho centenas de horas com Opus 4.6 e venho testando o 4.7 desde que saiu há poucos dias, e a qualidade tá igual ou melhor que 4.6 em tarefas não-triviais onde eu tenho referência de como o 4.6 se comportava.</p>
<p>Quando você vê alguém reclamando que &ldquo;4.7 tá horrível&rdquo;, peça o prompt exato, o repo, o contexto. Na maioria das vezes a pessoa não consegue reproduzir, ou o repo tem CLAUDE.md mal escrito, ou o task é subjetivo demais pra ter métrica. &ldquo;Senti que ficou pior&rdquo; não é dado. Eu mesmo fui pego por essa sensação em uma sessão do Opus 4.7 que tinha contaminado o contexto com muita documentação desatualizada, e o culpado era minha config, não o modelo.</p>
<p>Nos benchmarks que rodei esta semana, Opus 4.7 no opencode entregou Tier 1 limpo, mesmo nível do Opus 4.6 baseline. Vou mostrar abaixo que nos testes que ele rodou via Claude Code aconteceu uma coisa mais estranha, e aí a culpa provavelmente é do harness, não do modelo. Fica o recado.</p>
<h2>O que foi feito de novo no benchmark<span class="hx:absolute hx:-mt-20" id="o-que-foi-feito-de-novo-no-benchmark"></span>
    <a href="#o-que-foi-feito-de-novo-no-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">benchmark</a> ganhou suporte pra testar combinações de modelos nos três harnesses principais:</p>
<ol>
<li><strong>Claude Code</strong> (<code>claude -p --output-format stream-json</code>) — suporta sub-agentes declarados em <code>.claude/agents/*.md</code> que o Opus pode delegar via ferramenta <code>Task</code></li>
<li><strong>opencode</strong> — tem um sistema próprio de sub-agentes que podem rodar em modelos diferentes</li>
<li><strong>Codex CLI</strong> — ganhou suporte a sub-agentes em TOML via <code>-c agents.&lt;nome&gt;.config_file=...</code></li>
</ol>
<p>Em cima dessas três plataformas, configurei 7 combinações:</p>
<table>
  <thead>
      <tr>
          <th>Runner</th>
          <th>Modelo principal</th>
          <th>Sub-agente</th>
          <th>Ideia</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Code</td>
          <td>Opus 4.7</td>
          <td>—</td>
          <td>Baseline, só Opus sozinho</td>
      </tr>
      <tr>
          <td>Claude Code</td>
          <td>Opus 4.7</td>
          <td>Sonnet 4.6</td>
          <td>Opus planeja, Sonnet executa</td>
      </tr>
      <tr>
          <td>Claude Code</td>
          <td>Opus 4.7</td>
          <td>Haiku 4.5</td>
          <td>Opus planeja, Haiku (menor) executa</td>
      </tr>
      <tr>
          <td>opencode</td>
          <td>Opus 4.7</td>
          <td>GLM 5.1</td>
          <td>Opus + GLM (econômico + bom)</td>
      </tr>
      <tr>
          <td>opencode</td>
          <td>Opus 4.7</td>
          <td>Qwen 3.6 local</td>
          <td>Opus + modelo local grátis</td>
      </tr>
      <tr>
          <td>Codex</td>
          <td>GPT 5.4 xHigh</td>
          <td>GPT 5.4 medium</td>
          <td>Raciocínio alto planeja, menor executa</td>
      </tr>
      <tr>
          <td>Codex</td>
          <td>GPT 5.4 xHigh</td>
          <td>GPT 5.4 low</td>
          <td>Raciocínio alto planeja, mínimo executa</td>
      </tr>
  </tbody>
</table>
<p>Cada um roda o mesmo prompt: construir um app Rails com RubyLLM, Tailwind, Stimulus, Turbo Streams, testes Minitest, Brakeman, RuboCop, Dockerfile, docker-compose. O mesmo prompt do benchmark original.</p>
<h3>Como habilitar multi-model em cada harness<span class="hx:absolute hx:-mt-20" id="como-habilitar-multi-model-em-cada-harness"></span>
    <a href="#como-habilitar-multi-model-em-cada-harness" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Antes de mostrar o que deu errado, vale entender como cada harness expõe o sub-agente e quem decide chamá-lo. A mecânica é parecida nos três, mas os detalhes importam.</p>
<h4>Claude Code<span class="hx:absolute hx:-mt-20" id="claude-code"></span>
    <a href="#claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Claude Code lê automaticamente arquivos em <code>.claude/agents/*.md</code> do diretório do projeto. Cada arquivo é uma definição de agente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">name: sonnet-coder
</span></span><span class="line"><span class="cl">description: Claude Sonnet 4.6 for concrete coding execution. Use PROACTIVELY for any code change where the plan is already clear. Opus should plan and delegate; Sonnet should execute. Only skip delegation for cross-file architectural decisions.
</span></span><span class="line"><span class="cl">model: claude-sonnet-4-6
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You are a focused coding agent. The parent (Opus) has already decided
</span></span><span class="line"><span class="cl">the approach — your job is to execute cleanly.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Follow the provided instructions precisely. Don&#39;t re-plan.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Prefer editing existing files over creating new ones.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Match the existing codebase style.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Keep changes minimal.
</span></span><span class="line"><span class="cl">- Default to no comments.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O frontmatter YAML tem três campos obrigatórios: <code>name</code> (o handle que o modelo principal usa), <code>model</code> (qual modelo roda o agente), e <code>description</code> (a descrição que o modelo principal lê pra decidir se delega). O corpo do arquivo é o system prompt que o sub-agente recebe quando invocado.</p>
<p>Pra invocar, o Opus usa a ferramenta nativa <code>Task(subagent_type=&quot;sonnet-coder&quot;, prompt=&quot;...&quot;)</code>. Claude Code cobra tokens no modelo do sub-agente, não no modelo principal.</p>
<h4>opencode<span class="hx:absolute hx:-mt-20" id="opencode"></span>
    <a href="#opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>opencode usa um arquivo de config JSON (pode ser <code>opencode.json</code> padrão ou um custom via <code>--config</code>). Os agentes ficam numa chave <code>agents</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;model&#34;</span><span class="p">:</span> <span class="s2">&#34;openrouter/anthropic/claude-opus-4.7&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;agents&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;coder&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;model_id&#34;</span><span class="p">:</span> <span class="s2">&#34;zai/glm-5.1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;provider&#34;</span><span class="p">:</span> <span class="s2">&#34;zai&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Use proactively for concrete coding execution...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;prompt&#34;</span><span class="p">:</span> <span class="s2">&#34;You are a focused coding agent. The parent...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada entrada tem <code>model_id</code>, <code>provider</code>, <code>description</code> e <code>prompt</code>. O modelo principal (definido no topo) invoca o agente via ferramenta <code>task</code>, passando o nome (<code>coder</code> no exemplo) e instruções específicas.</p>
<h4>Codex CLI<span class="hx:absolute hx:-mt-20" id="codex-cli"></span>
    <a href="#codex-cli" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Codex usa arquivos TOML por agente, passados via flags <code>-c</code> no comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># .codex-coder.toml</span>
</span></span><span class="line"><span class="cl"><span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;coder&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">model</span> <span class="p">=</span> <span class="s2">&#34;gpt-5.4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">reasoning_effort</span> <span class="p">=</span> <span class="s2">&#34;medium&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Use proactively for concrete coding execution...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">prompt</span> <span class="p">=</span> <span class="s2">&#34;You are a focused coding agent. The parent (xhigh)...&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E invoca:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">codex <span class="nb">exec</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dangerously-bypass-approvals-and-sandbox <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c <span class="nv">model_reasoning_effort</span><span class="o">=</span>xhigh <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c agents.coder.config_file<span class="o">=</span>.codex-coder.toml <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p <span class="s2">&#34;&lt;prompt principal&gt;&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O modelo principal ganha acesso à ferramenta <code>spawn_agent</code> pra invocar o <code>coder</code>. Codex permite configurar <code>reasoning_effort</code> diferente entre principal e sub-agente, que é justamente o que os dois variants <code>multi_balanced</code> e <code>multi_faster</code> testam.</p>
<h3>Quem decide qual modelo roda cada tarefa<span class="hx:absolute hx:-mt-20" id="quem-decide-qual-modelo-roda-cada-tarefa"></span>
    <a href="#quem-decide-qual-modelo-roda-cada-tarefa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nos três harnesses, a decisão de delegar é tomada pelo <strong>modelo principal em tempo de execução</strong>, não por regra programática. Não tem heurística determinística do tipo &ldquo;se o arquivo é maior que X linhas, chame o sub-agente&rdquo;. O que existe é o modelo principal lendo a descrição do sub-agente e julgando, a cada passo, se a tarefa atual se encaixa.</p>
<p>Isso significa três coisas:</p>
<ol>
<li>
<p><strong>A descrição do sub-agente é o único botão de controle</strong>. Se você escreve &ldquo;use PROACTIVELY for X&rdquo; sem caveats, o modelo tende a delegar mais. Se você bota &ldquo;skip for Y&rdquo;, ele tende a não delegar em Y.</p>
</li>
<li>
<p><strong>O modelo principal é conservador por default</strong>. Em todos os três, o treinamento atual favorece não delegar quando a tarefa exige contexto cross-file ou decisão arquitetural. Greenfield Rails app é exatamente esse tipo de tarefa.</p>
</li>
<li>
<p><strong>Não dá pra forçar delegação via config</strong>. Você pode escrever uma descrição agressiva, mas se o modelo julgar que a tarefa não se encaixa, ele ignora. Não tem flag tipo <code>--force-subagent</code> nos três harnesses. A decisão é do modelo, não do operador.</p>
</li>
</ol>
<p>Isso é importante pra entender o resultado que vem a seguir.</p>
<h2>A descoberta que mata o argumento<span class="hx:absolute hx:-mt-20" id="a-descoberta-que-mata-o-argumento"></span>
    <a href="#a-descoberta-que-mata-o-argumento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Abri os logs de cada run esperando ver delegação acontecendo. Tools como <code>Task</code> (Claude Code) ou <code>spawn_agent</code> (Codex) deviam aparecer no ndjson cada vez que o modelo principal chamasse o sub-agente.</p>
<p>Em 7 runs, <strong>a ferramenta de delegação foi chamada zero vezes</strong>. Nenhum Opus chamou o Sonnet. Nenhum Opus chamou o Haiku. Nenhum Opus chamou o GLM 5.1 ou o Qwen 3.6 local. Nenhum GPT xHigh chamou o GPT medium ou low.</p>
<p>Todos os modelos principais fizeram o trabalho inteiro sozinhos, ignorando o sub-agente que estava registrado e visível pra eles. Os sub-agentes foram lidos, parseados, listados, e nunca invocados. É como contratar um assistente e deixar ele sentado na mesa o dia inteiro enquanto você faz tudo.</p>
<p>Por que isso aconteceu? Acho que tem duas camadas de explicação.</p>
<h3>A parte técnica<span class="hx:absolute hx:-mt-20" id="a-parte-técnica"></span>
    <a href="#a-parte-t%c3%a9cnica" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os modelos principais liam a descrição dos sub-agentes e decidiam que a tarefa não se encaixava. As descrições tipicamente diziam &ldquo;use proativamente pra executar código concreto&rdquo; com uma ressalva &ldquo;evite pra decisões arquiteturais cross-file&rdquo;. Só que um app Rails inteiro é decisão arquitetural cross-file. Controller depende de service, service depende de initializer, view depende de partial, todos dependem de como os testes fazem mock do LLM. Não tem parte isolada que dê pra passar pro coador menor sem perder contexto.</p>
<p>Eu poderia ter escrito a descrição do sub-agente com tom mais imperativo, forçando delegação. Mas isso seria trapacear pra conseguir um resultado. O ponto do teste é ver o que o modelo faz livremente, não o que ele faz forçado. E livremente, ele não delegou.</p>
<h3>A parte de gestão<span class="hx:absolute hx:-mt-20" id="a-parte-de-gestão"></span>
    <a href="#a-parte-de-gest%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Delegação tem custo de coordenação. Isso é conhecimento básico de project management, não é novidade. Quando você terceiriza uma tarefa, você tem que:</p>
<ul>
<li>Escrever especificação clara pro outro executor</li>
<li>Aguardar o resultado</li>
<li>Revisar</li>
<li>Pedir ajustes se tiver diferença entre o que você queria e o que veio</li>
<li>Reintegrar no resto do trabalho</li>
</ul>
<p>Com humanos sênior terceirizando pra júniors, esse custo existe e é real. A produtividade não escala linearmente com o número de executores. Dobrar o time não dobra a velocidade. Em muitos casos, a terceirização custa mais tempo do que teria custado fazer sozinho.</p>
<p>Com LLMs acontece a mesma coisa, agravada por uma característica específica: o planejamento do Opus raramente é perfeito de primeira. Nunca é. O Opus lê o prompt, monta um plano, começa a implementar, descobre um problema (biblioteca que não tem aquela versão, método que não existe como ele imaginou, teste que falha por motivo que ele não previu), ajusta o plano, tenta de novo. Esse loop de &ldquo;planejar → tentar → ajustar&rdquo; é inerente ao trabalho. Não é uma falha do Opus, é a natureza do desenvolvimento de software.</p>
<p>Agora imagina que você insere um modelo menor na metade desse loop. O Opus planeja, passa pro Qwen executar, o Qwen escreve código que provavelmente alucina API (como vimos no benchmark anterior, Qwen inventa <code>RubyLLM::Client.new</code> que não existe), o Opus recebe o código de volta, descobre que tá errado, tem que fazer sub-plano pra corrigir, passa de novo pro Qwen, que inventa outra coisa, e assim vai. O overhead de comunicação e correção explode.</p>
<p>Por isso os próprios modelos, sem serem forçados, decidiram não delegar. Eles sabem que o custo de coordenação é maior que o benefício, especialmente pra uma tarefa coesa como construir um app Rails do zero.</p>
<h2>Comentários por run<span class="hx:absolute hx:-mt-20" id="comentários-por-run"></span>
    <a href="#coment%c3%a1rios-por-run" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Mesmo sem delegação acontecer, os 7 runs deram resultados diferentes. Vou comentar cada um.</p>
<h3>Claude Code: Opus 4.7 sozinho<span class="hx:absolute hx:-mt-20" id="claude-code-opus-47-sozinho"></span>
    <a href="#claude-code-opus-47-sozinho" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>11 minutos, $6.74, 24 testes, 1742 arquivos. Resultado <strong>Tier 3</strong> (quebrado). O Opus nesse run alucinou o método <code>chat.complete</code> do RubyLLM, que não existe, e usou <code>chat.add_message(role:, content:)</code> com keyword args em vez de hash posicional. Mesma alucinação típica que outros modelos têm, agora no próprio Opus. Estranho, porque o mesmo Opus 4.7 no opencode entregou código Tier 1 correto no mesmo prompt.</p>
<h3>Claude Code: Opus 4.7 + Sonnet 4.6<span class="hx:absolute hx:-mt-20" id="claude-code-opus-47--sonnet-46"></span>
    <a href="#claude-code-opus-47--sonnet-46" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>10 minutos, $5.13, 18 testes, 1829 arquivos. Resultado <strong>Tier 2</strong> (primeira mensagem roda, multi-turn quebra). Melhor que o baseline do Opus sozinho, mas ainda tem o bug do keyword-args no <code>add_message</code>. Zero delegações pro Sonnet.</p>
<h3>Claude Code: Opus 4.7 + Haiku 4.5<span class="hx:absolute hx:-mt-20" id="claude-code-opus-47--haiku-45"></span>
    <a href="#claude-code-opus-47--haiku-45" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>15 minutos, $7.83, 34 testes, 1984 arquivos. Resultado <strong>Tier 3</strong>, mesma alucinação do Opus sozinho. O maior volume de testes (34!) todos passam, porque os fakes de teste mockam a API alucinada que o próprio Opus inventou. Testes vazios de significado.</p>
<p>Esse é o ponto que vale sublinhar: Opus 4.7 no Claude Code escreveu 34 testes que passam e nenhum deles prova que o código funciona. A API alucinada é testada contra uma implementação alucinada. No mundo real, o app crasha na primeira mensagem. Contagem de teste é métrica de vaidade quando o mock é errado.</p>
<h3>opencode: Opus 4.7 + GLM 5.1<span class="hx:absolute hx:-mt-20" id="opencode-opus-47--glm-51"></span>
    <a href="#opencode-opus-47--glm-51" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>19 minutos, $1.10, Tier 1 (funciona de primeira). API correta do RubyLLM. Ambas as fases (build + validação com Docker) completaram limpas. Zero chamadas pro GLM 5.1, o Opus fez tudo.</p>
<h3>opencode: Opus 4.7 + Qwen 3.6 local<span class="hx:absolute hx:-mt-20" id="opencode-opus-47--qwen-36-local"></span>
    <a href="#opencode-opus-47--qwen-36-local" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>30 minutos, $1.10 (só Opus cobrado, Qwen local é grátis), Tier 1. Mesma qualidade do anterior. Zero chamadas pro Qwen 3.6 rodando na 5090.</p>
<h3>Codex: xHigh planejando, medium executando<span class="hx:absolute hx:-mt-20" id="codex-xhigh-planejando-medium-executando"></span>
    <a href="#codex-xhigh-planejando-medium-executando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>21 minutos, ~$11, Tier 1. A run multi-agent mais cara da lista pelo lado Codex, mas curiosamente foi a que gerou melhor código, e corrigiu o bug do <code>add_message</code> que o benchmark anterior tinha encontrado no GPT 5.4 sozinho. Mas zero delegações, todo o trabalho foi do xHigh.</p>
<h3>Codex: xHigh planejando, low executando<span class="hx:absolute hx:-mt-20" id="codex-xhigh-planejando-low-executando"></span>
    <a href="#codex-xhigh-planejando-low-executando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>20 minutos, ~$10, Tier 2. Voltou a fazer o bug do keyword-args. Mais barato que o anterior mas gerou código pior. Zero delegações pro low também.</p>
<h2>O mesmo modelo, runs diferentes, resultados diferentes<span class="hx:absolute hx:-mt-20" id="o-mesmo-modelo-runs-diferentes-resultados-diferentes"></span>
    <a href="#o-mesmo-modelo-runs-diferentes-resultados-diferentes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui tá a história real desse benchmark, que fica mais clara quando você agrupa por modelo em vez de por combinação.</p>
<p>Como os sub-agentes não rodaram em nenhum caso, na prática cada combinação &ldquo;multi-model&rdquo; virou só mais um run do modelo principal. Isso me deu algo que eu não tinha no benchmark anterior: <strong>múltiplos runs do mesmo modelo no mesmo prompt</strong>. Vou comparar.</p>
<h3>GPT 5.4 xHigh: três runs<span class="hx:absolute hx:-mt-20" id="gpt-54-xhigh-três-runs"></span>
    <a href="#gpt-54-xhigh-tr%c3%aas-runs" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">benchmark da semana passada</a>, GPT 5.4 via Codex com xHigh tinha rodado uma vez só, com resultado Tier 2 (primeira mensagem funciona, multi-turn quebra por causa de <code>chat.add_message(role:, content:)</code> com keyword args em vez de hash posicional).</p>
<p>Esta semana rodei mais dois, com configurações de sub-agente diferentes (que não foram usadas, mas a presença altera o comportamento do principal, como mostrei acima):</p>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th>Tier</th>
          <th style="text-align: right">Tokens</th>
          <th style="text-align: right">Custo</th>
          <th>API correta?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>xHigh sozinho (semana passada)</td>
          <td>2</td>
          <td style="text-align: right">7.6M</td>
          <td style="text-align: right">~$16</td>
          <td>Bug no <code>add_message</code></td>
      </tr>
      <tr>
          <td>xHigh + medium subagent</td>
          <td>1</td>
          <td style="text-align: right">5.44M</td>
          <td style="text-align: right">~$11</td>
          <td><strong>Corrigiu o bug</strong></td>
      </tr>
      <tr>
          <td>xHigh + low subagent</td>
          <td>2</td>
          <td style="text-align: right">4.28M</td>
          <td style="text-align: right">~$10</td>
          <td>Bug voltou</td>
      </tr>
  </tbody>
</table>
<p>O mesmo modelo, mesmo prompt, três runs. Um deles escreveu <code>chat.add_message(message)</code> com hash posicional (Tier 1, funciona em multi-turn). Os outros dois escreveram com keyword args (Tier 2, quebra na segunda mensagem).</p>
<p>Nenhum sub-agente foi chamado nos multi variants. A única coisa que mudou entre os três runs foi o texto da descrição do sub-agente disponível (ou ausência dele). E mesmo assim, aquele GPT 5.4 &ldquo;acertou&rdquo; a API numa run e &ldquo;errou&rdquo; nas outras duas.</p>
<h3>Claude Opus 4.7: seis runs<span class="hx:absolute hx:-mt-20" id="claude-opus-47-seis-runs"></span>
    <a href="#claude-opus-47-seis-runs" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com o Opus foi ainda mais instrutivo. Seis runs diferentes, o mesmo modelo, o mesmo prompt, resultados espalhados entre Tier 1 e Tier 3:</p>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th>Harness</th>
          <th>Tier</th>
          <th style="text-align: right">Tempo</th>
          <th style="text-align: right">Custo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Opus 4.7 baseline</td>
          <td>opencode</td>
          <td>1</td>
          <td style="text-align: right">18.2m</td>
          <td style="text-align: right">$1.10</td>
      </tr>
      <tr>
          <td>Opus 4.7 + GLM 5.1</td>
          <td>opencode</td>
          <td>1</td>
          <td style="text-align: right">10.3m</td>
          <td style="text-align: right">$1.10</td>
      </tr>
      <tr>
          <td>Opus 4.7 + Qwen 3.6 local</td>
          <td>opencode</td>
          <td>1</td>
          <td style="text-align: right">19.4m</td>
          <td style="text-align: right">$1.10</td>
      </tr>
      <tr>
          <td>Opus 4.7 sozinho</td>
          <td>Claude Code</td>
          <td>3</td>
          <td style="text-align: right">11.0m</td>
          <td style="text-align: right">$6.74</td>
      </tr>
      <tr>
          <td>Opus 4.7 + Sonnet</td>
          <td>Claude Code</td>
          <td>2</td>
          <td style="text-align: right">10.1m</td>
          <td style="text-align: right">$5.13</td>
      </tr>
      <tr>
          <td>Opus 4.7 + Haiku</td>
          <td>Claude Code</td>
          <td>3</td>
          <td style="text-align: right">14.7m</td>
          <td style="text-align: right">$7.83</td>
      </tr>
  </tbody>
</table>
<p>As três runs de Opus no opencode: Tier 1 consistente. API correta do RubyLLM, funciona em multi-turn, código limpo.</p>
<p>As três runs de Opus no Claude Code: uma Tier 2 e duas Tier 3. Código que alucinou <code>chat.complete</code> (método que não existe) ou errou a assinatura do <code>add_message</code>.</p>
<p>Mesmo modelo. Mesmo prompt. Harness diferente = resultado diferente.</p>
<h3>O que isso significa<span class="hx:absolute hx:-mt-20" id="o-que-isso-significa"></span>
    <a href="#o-que-isso-significa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Duas leituras possíveis:</p>
<p><strong>A leitura preguiçosa:</strong> &ldquo;Opus 4.7 no Claude Code regrediu, trocar pro 4.6&rdquo; ou &ldquo;opencode é melhor que Claude Code&rdquo;. Ambas seriam conclusões erradas de um benchmark tão pequeno.</p>
<p><strong>A leitura honesta:</strong> benchmark de um run só, ou mesmo três runs, não é suficiente pra afirmar nada sobre qualidade absoluta de um modelo. A variância é grande, o contexto carregado pelo harness (CLAUDE.md, tool schemas, agent registries) mexe com o &ldquo;mental model&rdquo; que o modelo ativa, e o resultado pode oscilar entre tiers.</p>
<p>No benchmark anterior, eu tive a sorte (ou o azar) de ter runs consistentes o bastante pra as hierarquias do tipo &ldquo;Claude/GLM funcionam, Kimi/DeepSeek/Qwen inventam API&rdquo; se manterem. Mas mesmo lá, a variância de um run só pra outro é real. Se eu rodar o Kimi K2.5 dez vezes, talvez dois ou três desses runs tenham acertado a API. Não testei isso, mas é plausível.</p>
<p>Esse benchmark reforça o ponto: os rankings do artigo anterior valem como sinal, não como prova. &ldquo;Funciona de primeira 80% das vezes&rdquo; é diferente de &ldquo;sempre funciona&rdquo;. Pra uso em produção, você quer modelo que é robusto à variância, que não tem 20% de chance de voltar alucinação. Hoje, os únicos modelos que me atendem esse critério são Claude Opus e Claude Sonnet, em qualquer harness. GLM 5.1 chega perto mas ainda não tenho sample grande.</p>
<p>Quer dizer que Opus 4.7 &ldquo;piorou&rdquo;? Não. Quer dizer que Claude Code &ldquo;é pior que opencode&rdquo;? Não. Quer dizer que benchmark de run único sobre greenfield Rails não capta variância de modelo. É importante saber disso antes de tirar conclusões fortes.</p>
<h2>Tempo de execução: multi-model é mais lento?<span class="hx:absolute hx:-mt-20" id="tempo-de-execução-multi-model-é-mais-lento"></span>
    <a href="#tempo-de-execu%c3%a7%c3%a3o-multi-model-%c3%a9-mais-lento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vale medir uma pergunta adjacente: se o sub-agente nunca rodou, os runs multi-model foram mais lentos que os baselines sozinhos? Minha intuição inicial era que sim, já que sem paralelismo entre sessões o modelo principal faz o trabalho todo serialmente. O dado conta outra história.</p>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th style="text-align: right">Tempo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Code Opus alone</td>
          <td style="text-align: right">11.0m</td>
      </tr>
      <tr>
          <td>Claude Code Opus + Sonnet</td>
          <td style="text-align: right">10.1m</td>
      </tr>
      <tr>
          <td>Claude Code Opus + Haiku</td>
          <td style="text-align: right">14.7m</td>
      </tr>
      <tr>
          <td>opencode Opus baseline</td>
          <td style="text-align: right">18.2m</td>
      </tr>
      <tr>
          <td>opencode Opus + GLM 5.1</td>
          <td style="text-align: right">10.3m</td>
      </tr>
      <tr>
          <td>opencode Opus + Qwen 3.6</td>
          <td style="text-align: right">19.4m</td>
      </tr>
      <tr>
          <td>Codex xHigh baseline</td>
          <td style="text-align: right">21.9m</td>
      </tr>
      <tr>
          <td>Codex xHigh + medium</td>
          <td style="text-align: right">21.2m</td>
      </tr>
      <tr>
          <td>Codex xHigh + low</td>
          <td style="text-align: right">20.2m</td>
      </tr>
  </tbody>
</table>
<p>Alguns multi-model foram <strong>mais rápidos</strong> que o baseline sozinho. opencode com GLM 5.1 foi quase metade do tempo do opencode sozinho (10m vs 18m). Claude Code com Sonnet foi 1 minuto mais rápido que Opus puro. Codex multi-agent variants ficaram um pouco mais rápidos que xHigh sozinho.</p>
<p>Outros foram mais lentos: Claude Code com Haiku levou 15m (4m a mais que baseline). opencode com Qwen 3.6 ficou em 19m (igual baseline, provavelmente penalizado por overhead do llama-swap mesmo sem invocar o modelo).</p>
<p>Não tem padrão consistente de &ldquo;multi-model é sempre mais lento&rdquo; ou &ldquo;sempre mais rápido&rdquo;. O que aconteceu, olhando os tool calls e contagem de testes, é mais interessante: <strong>o modelo principal muda o próprio comportamento quando vê que tem sub-agente disponível, mesmo sem chamar ele</strong>.</p>
<ul>
<li>Claude Code Opus sozinho: 24 testes, 11m</li>
<li>Claude Code Opus + Sonnet: 18 testes, 10m (menos testes, mais rápido)</li>
<li>Claude Code Opus + Haiku: 34 testes, 15m (mais testes, mais lento)</li>
</ul>
<p>O padrão: quando o sub-agente existe na descrição como &ldquo;executor&rdquo;, o modelo principal às vezes produz output mais enxuto, como se estivesse &ldquo;deixando trabalho pra depois&rdquo;. Quando o sub-agente descreve execução mais cara (Haiku como &ldquo;high-volume execution&rdquo;), o modelo parece assumir que pode se dar ao luxo de escrever mais testes porque &ldquo;o executor barato vai cuidar&rdquo;. Em nenhum caso o executor é chamado. Mas a presença do sub-agente influencia o planejamento do modelo principal.</p>
<p>É um efeito sutil, tipo placebo de delegação. O modelo não delega, mas comporta como se fosse delegar. Isso pode ser bom (output mais focado) ou ruim (cobertura de teste menor que o baseline). Não é algo que você controla, é comportamento emergente do modelo lendo a descrição do sub-agente.</p>
<h3>Então vale configurar Haiku só pro efeito placebo?<span class="hx:absolute hx:-mt-20" id="então-vale-configurar-haiku-só-pro-efeito-placebo"></span>
    <a href="#ent%c3%a3o-vale-configurar-haiku-s%c3%b3-pro-efeito-placebo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dá pra ser tentado a pensar: &ldquo;se Opus escreveu mais código e mais testes com Haiku configurado, então vale a pena configurar Haiku como sub-agente mesmo que ele nunca rode, só pelo placebo&rdquo;. Os números dizem que não.</p>
<p>Comparando Opus sozinho vs Opus com Haiku configurado, ambos no Claude Code:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th style="text-align: right">Opus sozinho</th>
          <th style="text-align: right">Opus + Haiku</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tempo</td>
          <td style="text-align: right">11.0m</td>
          <td style="text-align: right">14.7m</td>
      </tr>
      <tr>
          <td>Custo</td>
          <td style="text-align: right">$6.74</td>
          <td style="text-align: right">$7.83</td>
      </tr>
      <tr>
          <td>Testes</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">34</td>
      </tr>
      <tr>
          <td>Tier de qualidade</td>
          <td style="text-align: right">3 (quebrado)</td>
          <td style="text-align: right">3 (quebrado, mesma alucinação)</td>
      </tr>
  </tbody>
</table>
<p>Com Haiku configurado, Opus gastou 3.7 minutos a mais, $1.09 a mais, e escreveu 10 testes a mais. O tier de qualidade ficou igual. A mesma alucinação de <code>chat.complete</code> apareceu nos dois runs. Os 10 testes extras mockam a mesma API alucinada, então não provam nada que os 24 originais já não provavam. Mais código, não código melhor.</p>
<p>Placebo de delegação pode mover quantidade, mas não corrige erro factual. E com sample de 1 run cada, nem o aumento de quantidade é confiável, porque variância entre runs de Opus sozinho também é alta (provavelmente um outro run de Opus sozinho ia dar 30+ testes por acaso).</p>
<p><strong>Conclusão prática:</strong> não configure sub-agente &ldquo;de mentira&rdquo; só pra tentar manipular o modelo principal. O custo em tokens/tempo é certo, o benefício é especulativo. Opus sozinho, sem sub-agente, continua sendo a configuração default recomendada. Sub-agentes só valem se você tem caso de uso real com delegação que funcione (e vimos aqui que greenfield não é esse caso).</p>
<h3>Hipótese &ldquo;multi-model é mais lento&rdquo; não se sustenta<span class="hx:absolute hx:-mt-20" id="hipótese-multi-model-é-mais-lento-não-se-sustenta"></span>
    <a href="#hip%c3%b3tese-multi-model-%c3%a9-mais-lento-n%c3%a3o-se-sustenta" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>De toda forma, voltando à pergunta original: não, não dá pra dizer que multi-model sem delegação é consistentemente mais lento. Às vezes é, às vezes é mais rápido, depende do modelo e da descrição do sub-agente. O que dá pra dizer é que a presença do sub-agente muda o comportamento do principal de forma imprevisível, e isso por si só é argumento contra configurações multi-model sem necessidade clara.</p>
<h2>Duas descobertas inesperadas<span class="hx:absolute hx:-mt-20" id="duas-descobertas-inesperadas"></span>
    <a href="#duas-descobertas-inesperadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fora do assunto principal (multi-model não funcionou), dois padrões apareceram.</p>
<h3>Primeira: o harness influencia a qualidade do código, não só o custo<span class="hx:absolute hx:-mt-20" id="primeira-o-harness-influencia-a-qualidade-do-código-não-só-o-custo"></span>
    <a href="#primeira-o-harness-influencia-a-qualidade-do-c%c3%b3digo-n%c3%a3o-s%c3%b3-o-custo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O mesmo Opus 4.7 produziu Tier 1 no opencode e Tier 2/Tier 3 no Claude Code, no mesmo prompt. Isso é novo. Até onde eu sei, é a primeira evidência em benchmark de que o harness (a CLI que envelopa o modelo) pode degradar correção factual, não só custo.</p>
<p>A hipótese é que Claude Code carrega 6-11 milhões de tokens de cache por run (CLAUDE.md, schemas de tools, registro de agentes, etc.), contra ~210 mil do opencode. Esse volume de contexto parece puxar o Opus na direção de um &ldquo;mental model&rdquo; genérico de SDK OpenAI, onde <code>chat.complete</code> faz sentido, em vez do mental model específico do gem RubyLLM. É especulação, não consigo provar. Mas a diferença de Tier entre os dois harnesses rodando o mesmo modelo é concreta.</p>
<p>Isso <strong>não significa</strong> que opencode é melhor que Claude Code pra uso diário. No meu dia a dia, Claude Code com Opus é superior ao opencode em quase tudo: integração com editor, gestão de contexto em sessões longas, tool support nativo, qualidade de planejamento multi-step. O benchmark tem um recorte estreito (greenfield Rails app, prompt bem específico, sem iteração humana) que não reflete o uso real.</p>
<p>O que o dado diz é: a variância entre harnesses é real e mensurável. Vale ter em mente quando você tá avaliando um modelo.</p>
<h3>Segunda: custo de Claude Code vs opencode<span class="hx:absolute hx:-mt-20" id="segunda-custo-de-claude-code-vs-opencode"></span>
    <a href="#segunda-custo-de-claude-code-vs-opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Rodando o mesmo Opus 4.7 no mesmo prompt:</p>
<ul>
<li>Claude Code: $5 a $8 por run</li>
<li>opencode: $1.10 por run</li>
</ul>
<p>Claude Code custa 5 a 7 vezes mais por run no mesmo modelo. A diferença é o cache-read: Claude Code carrega 6-11M tokens de contexto por run, opencode carrega ~210K. Tem razão técnica legítima (tool schemas, TodoWrite, agent registries, CLAUDE.md, integração com editor), mas o overhead é real e aparece direto na conta de quem paga por token.</p>
<p>Aqui cabe uma orientação mais refinada, porque isso muda o cálculo dependendo do seu modelo de consumo.</p>
<p><strong>Se você tem Pro ou Max:</strong> use Claude Code. Ponto. Assinatura cobre os tokens, você ganha o conjunto completo de features (tool support nativo, skills, agentes, Plan mode, contexto melhor em sessão longa). Não tem motivo pra trocar.</p>
<p><strong>Se você paga por token direto via API, o cálculo muda com o volume.</strong></p>
<p>Pra uso leve (algumas centenas de dólares por mês): opencode com Opus é mais barato e, nesse benchmark específico, chegou em Tier 1 enquanto Claude Code ficou em Tier 2/3. Funciona bem pra pipeline automatizado, CI, benchmark, agente server-side.</p>
<p>Pra uso pesado (milhares de dólares por mês no API): não faz sentido ficar no per-token. A assinatura Max 20x por $200/mês cobre volume grande e inclui Claude Code. Pra vibe-coder pesado, Max sai mais barato que Opus no API por uma margem ampla. Aí você volta pro primeiro bucket, com Claude Code.</p>
<p><strong>Opencode é melhor, independente de custo, pra:</strong></p>
<ul>
<li>Uso headless ou automatizado (CI, benchmarks, agentes em servidor)</li>
<li>Setup multi-provider onde você quer o mesmo harness batendo em OpenRouter, Z.ai, llama-swap local</li>
<li>Quando você precisa de output em JSON estruturado (<code>--format json</code>)</li>
<li>Comparativos neutros entre modelos</li>
</ul>
<p><strong>Claude Code é melhor, independente de custo, pra:</strong></p>
<ul>
<li>Sessões de coding interativas com humano no loop</li>
<li>Projetos com CLAUDE.md, skills, MCP custom</li>
<li>Trabalho onde Plan mode do Opus faz diferença</li>
<li>Sessões longas iterativas onde contexto acumulado ajuda</li>
</ul>
<p>A leitura honesta: esse benchmark mede um cenário estreito (greenfield, one-shot, sem humano iterando). Pra trabalho de verdade do dia a dia, Claude Code com Max continua sendo a recomendação pra 99% das pessoas. O ganho de custo do opencode aparece num nicho específico (pipeline automatizado ou uso API abaixo do break-even do Max). A maioria não tá nesse nicho.</p>
<h2>O mito do &ldquo;Opus planeja, Qwen executa&rdquo;<span class="hx:absolute hx:-mt-20" id="o-mito-do-opus-planeja-qwen-executa"></span>
    <a href="#o-mito-do-opus-planeja-qwen-executa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Volta e meia aparece gente no Twitter falando que monta pipeline onde Opus faz o plano técnico detalhado, e um modelo menor (Qwen, GLM 5, Haiku, Sonnet) executa. &ldquo;Economia de tokens, mesma qualidade, todo mundo ganha&rdquo;.</p>
<p>Não funciona. Ou melhor, funciona pra demo, não funciona pra projeto real.</p>
<p>O problema mais grave é que <strong>o plano nunca é perfeito de primeira</strong>. Código nunca é one-shot. Você implementa, descobre um problema, ajusta. Com um modelo grande só, esse ajuste é feito pelo próprio modelo em tempo real. Com dois modelos, cada ajuste precisa voltar pro planner, ser reprocessado, novo plano escrito, novo executor invocado. O loop é mais lento.</p>
<p>Depois tem a questão do <strong>conhecimento factual de API</strong>. Se o plano do Opus diz &ldquo;use RubyLLM pra chamar OpenRouter&rdquo;, o Opus sabe que é <code>RubyLLM.chat(model:).ask(msg).content</code>. O Qwen menor lê o plano e implementa com a API que ele acha que existe, que pode ser <code>RubyLLM::Client.new.complete</code>. O plano não corrige isso porque o plano não contém o conhecimento factual do gem. Só o próprio modelo que sabe aquela API sabe implementar corretamente.</p>
<p>E tem o <strong>custo de coordenação</strong>, que explode com iteração. Cada round de &ldquo;plano → executa → falha → re-plano → executa de novo&rdquo; custa mais tokens que simplesmente deixar o modelo grande fazer tudo em uma sessão. Você paga em tokens de planejamento E em tokens de código errado que precisa ser reescrito.</p>
<p>Em teoria, multi-model faz sentido. Na prática, é trabalho pra mostrar em thread do Twitter com animação bonita, não workflow de quem entrega código.</p>
<h2>Quando multi-model pode fazer sentido<span class="hx:absolute hx:-mt-20" id="quando-multi-model-pode-fazer-sentido"></span>
    <a href="#quando-multi-model-pode-fazer-sentido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não quero parecer absolutista. Tem cenários onde multi-model é a escolha certa.</p>
<p>O principal é <strong>tarefas genuinamente paralelas e desacopladas</strong>. Migração de API em 30 arquivos idênticos, por exemplo: cada arquivo segue o mesmo pattern, não tem dependência entre eles. Opus poderia supervisionar 20 sub-agentes fazendo a mesma transformação em 20 arquivos diferentes. Nesse caso, o custo de coordenação é amortizado pelo paralelismo.</p>
<p>Outro caso é <strong>tarefas que têm fase de pesquisa pesada seguida de fase de implementação direta</strong>. Opus faz o spike arquitetural com exploração de código legado, depois delega a implementação mecânica pro modelo menor.</p>
<p>Um exemplo real que passei semana passada: <a href="/2026/04/09/20-anos-de-blog-o-ano-em-que-a-ia-finalmente-me-deixou-traduzir-tudo/">traduzi 700+ posts e todas as legendas de vídeo do blog pra inglês</a> usando Claude Code. Esgotei o Max 20x e estourei mais $1120 de uso extra no mês. Tradução é exatamente o tipo de tarefa que teria se beneficiado de multi-model: cada post é independente do outro, não tem dependência cross-file, o planejamento arquitetural é nenhum, só tradução em lote. Opus orquestrando + Sonnet executando a tradução de cada arquivo teria cortado o custo pela metade, fácil. Não me ocorreu na hora, rodei tudo no Opus. A lição que tiro é: pra tarefa genuinamente paralela, multi-model com Sonnet como executor faz sentido, e eu perdi uma oportunidade clara de economizar.</p>
<p>Mas nenhum dos casos acima é &ldquo;greenfield Rails app&rdquo;. Aplicação nova do zero é o pior cenário pra multi-model porque cada parte depende de todas as outras partes. Os modelos não são estúpidos, eles reconhecem isso e recusam a delegação.</p>
<h2>A regra de bolso segue a mesma<span class="hx:absolute hx:-mt-20" id="a-regra-de-bolso-segue-a-mesma"></span>
    <a href="#a-regra-de-bolso-segue-a-mesma" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra 90% do trabalho de programação do dia a dia, minha recomendação continua:</p>
<ul>
<li>Claude Code + Opus (4.6 ou 4.7)</li>
<li>Se custo for crítico e você tá ok com plugar em OpenRouter, GLM 5.1 é o segundo lugar confortável</li>
<li>Se você tem GPU boa (5090 ou equivalente), Qwen 3.6 35B local é aceitável pra tarefas simples, com caveats</li>
</ul>
<p>Multi-model? Só pra casos específicos onde o paralelismo é genuíno. Pra projeto normal, é overhead desnecessário.</p>
<h2>Benchmarks não são verdade absoluta<span class="hx:absolute hx:-mt-20" id="benchmarks-não-são-verdade-absoluta"></span>
    <a href="#benchmarks-n%c3%a3o-s%c3%a3o-verdade-absoluta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse benchmark mede uma coisa específica: greenfield Rails app, prompt bem determinado, sem iteração humana, em runners automatizados. É uma fatia estreita do uso real.</p>
<p>Se você quer saber qual combinação funciona pro SEU workflow, pros SEUS tipos de projeto, pras SUAS expectativas de qualidade, não confie no meu benchmark. Rode o seu. O <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">código tá todo no GitHub</a>, o harness é extensível, você troca o prompt e já tem seu próprio comparativo.</p>
<p>O que eu espero que esse trabalho contribua é metodologia, não resposta definitiva. &ldquo;Claude é melhor que Qwen&rdquo; depende do que você tá fazendo. &ldquo;Multi-model não funciona&rdquo; depende do tipo de tarefa. Benchmark serve pra restringir o espaço de especulação com dado concreto, não pra fechar a discussão.</p>
<p>Enquanto isso, se alguém te disser que combinou Claude + GLM e ficou mágico, pede o código, pede o prompt, pede o repo. Na maioria das vezes a pessoa mediu uma coisa bem diferente, ou tem uma tarefa bem específica onde essa combinação cabe. Não generalize a partir de tweet.</p>
]]></content:encoded><category>llm</category><category>benchmark</category><category>claude</category><category>ai</category><category>vibecoding</category></item><item><title>Omarchy no Thinkpad T14 Gen 6: Mini-Review e Setup Completo</title><link>https://www.akitaonrails.com/2026/04/18/omarchy-no-thinkpad-t14-gen-6/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/18/omarchy-no-thinkpad-t14-gen-6/</guid><pubDate>Sat, 18 Apr 2026 08:30:00 GMT</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 fechado ao lado do carregador USB-C" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Comprei um Lenovo Thinkpad T14 Gen 6 e instalei Omarchy em cima. Não é minha máquina principal, nem pretende ser. É companion: um notebook pra abrir em cima da mesa do escritório de impressão 3D, dar um SSH no desktop, chamar Claude Code, acessar arquivos no NAS, debugar rede pelo ethernet sem ficar procurando adaptador USB e cabo longo. Esse artigo cobre a escolha do hardware, o setup de Omarchy em cima, as customizações específicas pra notebook e pra esse Thinkpad em particular, e as decisões de arquitetura que podem não ser óbvias.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 fechado ao lado do carregador USB-C"  loading="lazy" /></p>
<p>Comprei um Lenovo Thinkpad T14 Gen 6 e instalei Omarchy em cima. Não é minha máquina principal, nem pretende ser. É companion: um notebook pra abrir em cima da mesa do escritório de impressão 3D, dar um SSH no desktop, chamar Claude Code, acessar arquivos no NAS, debugar rede pelo ethernet sem ficar procurando adaptador USB e cabo longo. Esse artigo cobre a escolha do hardware, o setup de Omarchy em cima, as customizações específicas pra notebook e pra esse Thinkpad em particular, e as decisões de arquitetura que podem não ser óbvias.</p>
<h2>Mini-review do Thinkpad T14 Gen 6<span class="hx:absolute hx:-mt-20" id="mini-review-do-thinkpad-t14-gen-6"></span>
    <a href="#mini-review-do-thinkpad-t14-gen-6" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-turned-off.jpg" alt="Thinkpad T14 Gen 6 aberto, tela desligada, mostrando o teclado e o selo do Intel Core Ultra 5"  loading="lazy" /></p>
<p>Vamos tirar do caminho: esse não é o notebook dos sonhos. Se eu fosse escolher por estética, pegaria um Asus Zenbook S 14 com tela OLED. Se fosse por portabilidade, o Thinkpad T14s com casca de alumínio, que é mais leve e muito mais bonito. O T14 regular tem tela IPS 1920x1200 de 14&quot;, 400 nits, 60Hz, sem HDR. Dá pro gasto, mas está longe da tela do Zenbook. A casca é de plástico com um acabamento emborrachado, o que deixa ele resistente a arranhão, mas não é premium. Revisões da <a href="https://www.notebookcheck.net/AMD-Ryzen-AI-meets-classic-ThinkPad-Lenovo-ThinkPad-T14-Gen-6-AMD-laptop-review.1222690.0.html"target="_blank" rel="noopener">NotebookCheck</a> e da <a href="https://www.xda-developers.com/lenovo-thinkpad-t14-gen-6-review/"target="_blank" rel="noopener">XDA Developers</a> batem na mesma tecla: bom custo, boa conectividade, desempenho adequado pra escritório, mas a tela é datada.</p>
<p>O que compensa:</p>
<ul>
<li>Seleção de portas. HDMI full-size. Ethernet gigabit. USB-A, USB-C Thunderbolt 4, carrega pelo USB-C (não é carregador proprietário). Pra um companion de debug, isso é exatamente o que eu queria. O Zenbook S 14, por ser mais fino, corta portas.</li>
<li>Sensor de digital funcional no Linux (Goodix MOC, funciona com libfprint em kernel 6.11+). Isso eu uso, detalho mais à frente.</li>
<li>Teclado Thinkpad. Curso de tecla de 1.5mm, trackpoint, layout clássico. Não é o melhor teclado do mundo em 2026, mas é confiável e durável.</li>
<li>Carcaça rugged. Vai cair, vai arranhar, vai viajar na mala. Se eu colocasse um Zenbook OLED ou um Macbook Pro em cima da mesa do escritório de impressão 3D, ao lado da impressora, com poeira de PLA voando, eu ficaria nervoso. O Thinkpad pode levar porrada.</li>
</ul>
<p>Se eu quisesse máquina de jogo, pegaria um Asus Zephyrus G14, meu favorito de notebook gamer. Se eu quisesse máquina de trabalho criativo, pegaria o <a href="https://www.asus.com/laptops/for-home/zenbook/zenbook-duo-ux8406/"target="_blank" rel="noopener">Asus Zenbook Duo (UX8406)</a> com duas telas OLED verticais, ótimo pra edição de vídeo e modelagem 3D. Tenho grana pra qualquer Macbook Pro ou Mac Studio, e escolhi não ir por esse caminho. Explico melhor na próxima seção.</p>
<h2>Meu caso de uso<span class="hx:absolute hx:-mt-20" id="meu-caso-de-uso"></span>
    <a href="#meu-caso-de-uso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-fastfetch.jpg" alt="Thinkpad aberto numa bancada do escritório de impressão 3D, com fastfetch mostrando Omarchy no terminal"  loading="lazy" /></p>
<p>Meu PC principal é um desktop com Ryzen 9 7950X3D, 96 GB de RAM, RTX 5090 de 32 GB. É onde eu trabalho, experimento com modelos locais, rodo containers, edito o blog. Pra jogo, tenho um mini-PC separado com RTX 4090. Esses dois atendem 100% do que eu preciso fazer em casa.</p>
<p>O notebook existe pra cobrir o 1% restante: sentar no sofá, levar no escritório de impressão 3D, levar na cozinha, levar em viagem curta. Não é pra substituir o desktop. É pra ter acesso remoto ao desktop quando eu estiver longe dele.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/ssh-to-remote-desktop-on-left.png" alt="Hyprland com dois terminais lado a lado: à esquerda SSH pro desktop principal (96 GB, RTX 5090), à direita o próprio Thinkpad. Mesma UI, duas máquinas."  loading="lazy" /></p>
<p>Na prática fica assim: abro o notebook, dou split no Hyprland, janela da esquerda dá SSH no desktop principal, janela da direita é o próprio notebook. Mesmo Omarchy nos dois, mesmos atalhos, mesmo bash. O notebook vira extensão do desktop, não um ambiente paralelo que eu tenho que reconfigurar na cabeça toda vez que alterno.</p>
<h3>SSH fora de casa: Tailscale<span class="hx:absolute hx:-mt-20" id="ssh-fora-de-casa-tailscale"></span>
    <a href="#ssh-fora-de-casa-tailscale" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dentro da rede local, SSH é trivial, o notebook fala com o desktop via IP interno. Fora de casa é outra história. Meu IP doméstico é dinâmico, abrir porta 22 pra internet é péssima ideia, e mesmo com DDNS e port forwarding você está colocando SSH na cara da internet pra qualquer scanner achar.</p>
<p>A solução que uso é <a href="https://tailscale.com/"target="_blank" rel="noopener">Tailscale</a>. Pra quem não conhece: Tailscale é uma mesh VPN baseada no WireGuard, que cria uma rede privada entre seus dispositivos (o &ldquo;tailnet&rdquo;). Cada máquina roda o agente, autentica uma vez, e passa a ter um IP fixo na rede privada (tipo 100.x.y.z). O tráfego entre seus próprios dispositivos vai direto peer-to-peer, cifrado pelo WireGuard. Não passa por servidor central da Tailscale, eles só coordenam NAT traversal. Resultado: do meu notebook num café em qualquer lugar do mundo, eu dou <code>ssh hal9000</code> e caio no desktop de casa como se estivesse na mesma rede.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/tailscale-machines.png" alt="Painel admin da Tailscale mostrando as duas máquinas do meu tailnet, hal9000 (desktop) e hal9666 (thinkpad), ambas com SSH habilitado"  loading="lazy" /></p>
<p>Existem opções mais sofisticadas: <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> com Zero Trust pra expor serviços publicamente com autenticação SSO, headscale self-hosted, WireGuard cru com config manual, Nebula, OpenVPN. Cada uma tem seu caso de uso. Se você precisa expor serviços pra terceiros, controlar acesso granular por identidade, rodar a infraestrutura inteira em casa sem depender de terceiros, essas opções ganham. No meu caso, é só notebook falando com desktop, por períodos curtos (não é trabalho de uma semana inteira, é debug rápido de fim de semana), então Tailscale grátis resolve. O tier free aceita até 100 dispositivos e 3 usuários, muito mais do que eu preciso.</p>
<p>Setup é o mais simples possível:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># No Arch/Omarchy</span>
</span></span><span class="line"><span class="cl">sudo pacman -S tailscale
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tailscaled.service
</span></span><span class="line"><span class="cl">sudo tailscale up --ssh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A flag <code>--ssh</code> ativa o <a href="https://tailscale.com/kb/1193/tailscale-ssh/"target="_blank" rel="noopener">Tailscale SSH</a>, que faz autenticação via identidade da tailnet em vez de chave SSH local. Uma vez fez login via browser no tailscale, cada máquina registrada pode entrar nas outras com base em política de ACL definida no painel admin. Zero key management.</p>
<p>Repito no desktop, faço login com a mesma conta, e pronto. As duas máquinas aparecem no painel (hal9000 e hal9666 na screenshot acima, com SSH habilitado nas duas). Do notebook: <code>ssh hal9000</code>. Do desktop pro notebook: <code>ssh hal9666</code>. Sem port forward, sem IP público, sem expor porta 22 pra internet. Se o notebook for roubado, eu removo ele do tailnet num clique.</p>
<p>Um detalhe prático: como o tailnet dá nome estável, eu adicionei entradas no <code>~/.ssh/config</code> pra usar esses nomes curtos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Host hal9000
  HostName hal9000
  User akitaonrails</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Assim <code>ssh hal9000</code> funciona de qualquer lugar que tenha Tailscale conectado. É a solução mais próxima de &ldquo;it just works&rdquo; que já vi pra SSH remoto.</p>
<h3>Por que não um Mac<span class="hx:absolute hx:-mt-20" id="por-que-não-um-mac"></span>
    <a href="#por-que-n%c3%a3o-um-mac" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu não tenho uso pro macOS. Como dev, eu vivo melhor em Linux nativo. Toda ferramenta que eu preciso tem versão Linux de primeira, e no macOS eu teria versão de segunda via Homebrew. Não faço iOS, então não preciso do XCode. Pra mobile uso Flutter ou Hotwire Native, que rodam em qualquer OS. iTerm2 e Ghostty no Mac são bons, mas Alacritty, Kitty e o próprio Ghostty no Linux me atendem igual. Todo software bom chega primeiro no Linux, daí é portado pros outros. Arch com AUR cobre tudo num único <code>yay</code>.</p>
<p>Pra trabalho criativo, faz anos que não faço profissionalmente. DaVinci Resolve Studio no Linux é superior ao Final Cut Pro. Krita ou Affinity Photo substituem Photoshop na maioria dos casos. Clip Studio Paint no Android é superior ao Procreate. Eu simplesmente não tenho um workflow que depende da Apple, e a App Store me irrita.</p>
<p>Pra jogo, Mac é terrível. Apple Silicon tem GPU boa pra algumas coisas, mas a biblioteca de jogo pro macOS nativo é pífia comparada com Windows ou Linux. Game Porting Toolkit existe, CrossOver existe, mas pra quem joga sério, não serve. Eu não vou jogar no Thinkpad, pra isso tenho o desktop principal e o mini-PC com RTX 4090, mas se um dia bater vontade de rodar um Hollow Knight ou um indie num trem, é só instalar Steam e deixar o Proton fazer o trabalho. Linux virou plataforma de jogo de verdade nos últimos anos, com Proton/DXVK rodando quase todo o catálogo da Steam (checa <a href="https://www.protondb.com/"target="_blank" rel="noopener">ProtonDB</a>). Inclusive documentei recentemente como <a href="/2026/04/11/distrobox-de-emulacao-com-claude-code/">rodei minha biblioteca de emulação em distrobox</a> sem poluir o sistema host. Com Mac, essas opções não existem.</p>
<p>Outro argumento que sempre aparece: &ldquo;mas um Mac Mini M4 ou Mac Studio com 128GB de memória unificada roda modelos grandes localmente, é o substituto do ChatGPT&rdquo;. Já testei essa tese e escrevi um <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">benchmark detalhado</a>. A conclusão: hardware local caro pra rodar LLM é hobby de fim de semana, não é ferramenta de produção. Os modelos open source que cabem não entregam a qualidade que o Claude Opus entrega. Eu tenho um <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">home server com AMD Strix Halo e 96 GB de RAM unificada</a>, rodei modelos por dezenas de horas, e no meu fluxo real de programação eles servem pra tarefas simples. Pras tarefas complexas, Claude Opus. Antes de gastar os $4000 num Mac Studio com a justificativa de rodar modelos locais, faça o teste de verdade. Você provavelmente vai voltar a pagar Opus depois.</p>
<h2>Por que Omarchy<span class="hx:absolute hx:-mt-20" id="por-que-omarchy"></span>
    <a href="#por-que-omarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu uso Omarchy no desktop há meses e documentei o caminho numa <a href="/tags/omarchy/">série de artigos</a>. O <a href="/2025/08/29/new-omarchy-2-0-install/">Omarchy 2.0</a> tem instalador próprio com LUKS, Btrfs, Limine, snapper e SDDM já configurados. Escrevi sobre <a href="/2025/09/12/omarchy-2-0-instale-com-a-iso-do-omarchy/">usar a ISO oficial</a>, sobre as <a href="/2025/09/07/omarchy-2-0-zsh-configs/">customizações de ZSH</a> com atuin, starship, secrets bem organizados, sobre <a href="/2025/09/07/omarchy-2-0-mise-pra-organizar-ambientes-de-desenvolvimento/">Mise pra múltiplas linguagens</a>, sobre <a href="/2025/09/07/omarchy-2-0-lazyvim-lazyextras/">LazyVim e LazyExtras</a>, sobre <a href="/2025/09/09/omarchy-2-0-entendendo-ssh-e-yubikeys/">SSH e Yubikeys</a>, sobre <a href="/2025/09/09/omarchy-2-0-tuis/">TUIs modernas</a>. Também tem o <a href="/2026/01/21/omarchy-3-setup-de-dual-gpus-com-amd-e-nvidia/">Omarchy 3 com dual GPU AMD + NVIDIA</a> e o <a href="/2026/01/09/omarchy-3-um-dos-melhores-agentes-pra-programacao-crush/">Crush</a> como agente de coding.</p>
<p>Pra quem não conhece: Omarchy é Arch Linux puro com uma camada de cosmética em cima do Hyprland/Wayland. Pré-configurado com defaults sãos. Eu podia montar tudo isso do zero, já fiz isso várias vezes na vida, mas por que refazer trabalho que alguém já fez bem feito? Instalo Omarchy, faço os tweaks que são meus em cima, e tenho Arch customizado numa fração do tempo.</p>
<p>Um ponto que eu levanto toda vez que recomendo Omarchy: a documentação é excelente. Tem um <a href="https://manuals.omamix.org/2/the-omarchy-manual"target="_blank" rel="noopener">manual oficial</a> que cobre desde instalação até customização de tema, keybindings, Hyprland, Waybar, tudo. Se você vem de Ubuntu ou Fedora e tem receio de Arch, esse manual resolve o grosso das dúvidas. Pra quem nunca tocou em Hyprland, abre ele e vai tangenciando. Não é um README jogado no GitHub, é manual de verdade, com capítulos e índice.</p>
<p>A história desse artigo começa num fio anterior. Eu migrei meu home server recentemente, trocando um servidor Ubuntu antigo por um <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Minisforum MS-S1 com openSUSE MicroOS configurado com Claude Code</a>. Esse foi o primeiro experimento sério de deixar o Claude Code guiar uma migração de infra inteira, com containers, NFS, serviços, networking. Deu certo. E deixou no ar a ideia: por que não usar a mesma abordagem pra configurar um notebook novo?</p>
<p>Foi o que fiz. Peguei o Thinkpad, baixei a ISO do Omarchy mais recente, gravei num pendrive, instalei por cima do Windows. Do zero ao desktop funcional, menos de uma hora. Dali pra frente, tudo é tweaking, que eu fui documentando conforme fazia.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-claude-terminal.jpg" alt="Claude Code rodando no terminal do Thinkpad, pronto pra começar a configurar o notebook"  loading="lazy" /></p>
<p>Vou detalhar as duas camadas de customização: o que precisa pra um notebook qualquer, e o que é específico desse Thinkpad.</p>
<h2>Configs específicos de notebook<span class="hx:absolute hx:-mt-20" id="configs-específicos-de-notebook"></span>
    <a href="#configs-espec%c3%adficos-de-notebook" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um notebook tem problemas que um desktop não tem: bateria, suspensão, tampa, brilho, trackpad. Essas são as partes que Omarchy default não cobre do jeito que eu quero.</p>
<h3>Power management com TLP<span class="hx:absolute hx:-mt-20" id="power-management-com-tlp"></span>
    <a href="#power-management-com-tlp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O default do Omarchy vem com <code>power-profiles-daemon</code>. Troquei por TLP, que dá controle granular de CPU scaling, thresholds de bateria e perfil dinâmico baseado em AC vs bateria.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S tlp tlp-rdw
</span></span><span class="line"><span class="cl">sudo systemctl mask power-profiles-daemon.service
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tlp.service</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>mask</code> é necessário porque o upower traz o <code>power-profiles-daemon</code> de volta se você só der <code>disable</code>.</p>
<p>Thresholds de carga: 60% pra começar, 85% pra parar. O notebook fica a maior parte do tempo plugado na mesa do escritório, e manter a bateria em 100% 24/7 detona a capacidade ao longo do tempo. Com 60/85, a bateria passa a maior parte do tempo na faixa saudável de lítio e ainda sobra capacidade útil decente.</p>
<p>Perfis: <code>balanced</code> com <code>balance_power</code> na bateria, <code>performance</code> no AC. O <code>low-power</code> do TLP era agressivo demais no Core Ultra 5 235U, a resposta de janela e terminal ficava perceptível. Balanced dá a melhor relação consumo/responsividade pra uso normal.</p>
<p>Tem uma pegadinha: <code>tlp auto</code> depois de <code>tlp ac</code> nem sempre reaplica o perfil de bateria. Criei um pequeno script ligado ao <code>Super+Ctrl+P</code> que lê o estado atual e chama <code>tlp bat</code> ou <code>tlp ac</code> explicitamente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">profile</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> unknown<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">on_ac</span><span class="o">=</span><span class="k">$(</span>cat /sys/class/power_supply/AC*/online 2&gt;/dev/null <span class="p">|</span> head -1 <span class="o">||</span> <span class="nb">echo</span> 0<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$profile</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;performance&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$on_ac</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp auto &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;Plugged in — back to AC auto&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp bat &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;On battery — normal profile&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: off&#34;</span> <span class="s2">&#34;</span><span class="nv">$label</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  sudo /usr/bin/tlp ac &gt;/dev/null
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: on&#34;</span> <span class="s2">&#34;Forcing AC profile&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No waybar, um módulo custom <code>custom/perf</code> mostra o estado atual (󰓅 PERF, 󰌪 ECO, ou vazio pra balanced) e aceita clique pra alternar. O script de output é bem curto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">p</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">  performance<span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;󰓅 PERF&#39;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  low-power<span class="o">)</span>   <span class="nb">printf</span> <span class="s1">&#39;󰌪 ECO&#39;</span>  <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  balanced<span class="p">|</span><span class="s2">&#34;&#34;</span><span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;&#39;</span>       <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  *<span class="o">)</span>           <span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Suspend, hibernate, e tampa<span class="hx:absolute hx:-mt-20" id="suspend-hibernate-e-tampa"></span>
    <a href="#suspend-hibernate-e-tampa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso aqui é o que mudou em relação aos notebooks que eu usava em 2015. O T14 Gen 6 fecha a tampa, suspende, e acorda quando você abre de novo. Sem bug, sem delay estranho, sem precisar dar login de novo no meio da sessão gráfica. O hyprlock pega depois do suspend, aceita digital ou senha, e volta pro desktop em segundos. Isso é o comportamento que a gente tem na Apple há anos e que no Linux era uma aventura. Em 2026, em hardware moderno com kernel 6.11+, simplesmente funciona.</p>
<p>Mem sleep mode é s2idle (sem deep sleep). Hibernação habilitada via swapfile Btrfs de ~30 GB e <code>resume=/dev/mapper/root resume_offset=...</code> no kernel cmdline do Limine. Raramente uso, mas está lá.</p>
<p>O hypridle tem timeouts agressivos pra notebook:</p>
<ul>
<li>2.5 min → screensaver</li>
<li>5 min → lock</li>
<li>5.5 min → DPMS off + keyboard backlight off</li>
</ul>
<p>Depois de trancado, passa mais 5 min e a tela apaga de vez. No unlock, restaura brilho de tela e de teclado pro nível anterior. Essas timings são muito mais curtas que as do desktop (20-40 min lá). A diferença de bateria ao longo do dia é mensurável.</p>
<h3>Brilho e backlight de teclado<span class="hx:absolute hx:-mt-20" id="brilho-e-backlight-de-teclado"></span>
    <a href="#brilho-e-backlight-de-teclado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>brightnessctl</code> controla os dois. Fn+Space alterna o backlight do teclado em três níveis. Hypridle salva o nível atual antes de apagar e restaura no retorno. Comando que uso no script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brightnessctl -sd <span class="s1">&#39;*::kbd_backlight&#39;</span> <span class="nb">set</span> <span class="m">0</span>    <span class="c1"># salva e desliga</span>
</span></span><span class="line"><span class="cl">brightnessctl -rd <span class="s1">&#39;*::kbd_backlight&#39;</span>          <span class="c1"># restaura</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Touchpad<span class="hx:absolute hx:-mt-20" id="touchpad"></span>
    <a href="#touchpad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>touchpad {
    natural_scroll = true
    clickfinger_behavior = true
    disable_while_typing = true
    scroll_factor = 0.4
}
gesture = 3, horizontal, workspace</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>clickfinger_behavior</code> troca o clique de 2 dedos como botão direito (mais confortável que acertar a zona inferior direita). <code>disable_while_typing</code> é palm rejection básico. Três dedos horizontais alternam workspaces, o gesto mais útil no Hyprland.</p>
<h2>Configs específicos do Thinkpad<span class="hx:absolute hx:-mt-20" id="configs-específicos-do-thinkpad"></span>
    <a href="#configs-espec%c3%adficos-do-thinkpad" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui entra o que é particular desse modelo. Algumas coisas o Linux moderno já cobre sozinho, outras exigem configuração específica.</p>
<h3>Sensor de digital<span class="hx:absolute hx:-mt-20" id="sensor-de-digital"></span>
    <a href="#sensor-de-digital" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Goodix MOC <code>27c6:6594</code>, funciona com libfprint 1.94.9+ em kernel 6.11+. Pacote é <code>fprintd</code>.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S fprintd
</span></span><span class="line"><span class="cl">fprintd-enroll                        <span class="c1"># dedo indicador direito por default</span>
</span></span><span class="line"><span class="cl">fprintd-enroll -f left-index-finger   <span class="c1"># outro dedo</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pro <code>sudo</code> aceitar digital, adiciono <code>auth sufficient pam_fprintd.so</code> acima da linha do <code>pam_unix.so</code> em <code>/etc/pam.d/sudo</code>. Com <code>sufficient</code>, se a digital passar, autentica direto. Se falhar ou se eu digitar ESC, cai pro prompt de senha. Isso vale a pena de verdade: dezenas de vezes por dia, <code>sudo pacman -Syu</code> ou <code>sudo systemctl restart algo</code>, e é só encostar o dedo.</p>
<p>No hyprlock, uso a configuração nativa do próprio hyprlock, não o PAM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>auth {
    fingerprint {
        enabled = true
        ready_message = Scan fingerprint or type password
        present_message = Scanning...
        retry_delay = 250
    }
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A razão é que o caminho PAM dá prompt duplo. Configurado nativo, o hyprlock aceita digital ou senha, o que vier primeiro desbloqueia.</p>
<p>No SDDM (login inicial), deixei senha apenas. O motivo: a senha de login destrava o keyring do GNOME, e a digital não consegue fornecer plaintext pra isso. Depois que o keyring está destrancado, o hyprlock pode usar digital sem problema.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/sudo-asks-for-fingerprinting.png" alt="Prompt do sudo pedindo leitura de digital"  loading="lazy" /></p>
<p>Na prática, o sudo fica assim. Encosta o dedo, autentica, segue.</p>
<h3>Teclado brasileiro do Thinkpad<span class="hx:absolute hx:-mt-20" id="teclado-brasileiro-do-thinkpad"></span>
    <a href="#teclado-brasileiro-do-thinkpad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O teclado brasileiro do Thinkpad tem uma peculiaridade chata. A tecla <code>/?</code> fica na posição que seria o Ctrl direito (keycode 97), não na posição AB11 tradicional do ABNT2 (keycode 89). Se você usar o layout padrão <code>br(abnt2)</code>, essa tecla fica inacessível. Literalmente não imprime nada.</p>
<p>A solução é o variant <code>br(thinkpad)</code> que existe em <code>/usr/share/X11/xkb/symbols/br</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>xkb_symbols &#34;thinkpad&#34; {
    include &#34;br(abnt2)&#34;
    name[Group1]=&#34;Portuguese (Brazil, IBM/Lenovo ThinkPad)&#34;;
    key &lt;RCTL&gt; { [ slash, question, degree, questiondown ] };
};</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kb_layout = br
kb_variant = thinkpad
kb_model = thinkpad60</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E system-wide pra TTY/X11:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo localectl set-keymap br-abnt2
</span></span><span class="line"><span class="cl">sudo localectl set-x11-keymap br thinkpad thinkpad60</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Escrevi um pequeno Python que lê scancodes brutos de <code>/dev/input/event*</code> pra diagnosticar essas esquisitices. Útil quando uma tecla decide não funcionar e você precisa descobrir se é hardware, kernel, ou xkb.</p>
<h3>Input method: fcitx5<span class="hx:absolute hx:-mt-20" id="input-method-fcitx5"></span>
    <a href="#input-method-fcitx5" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Junto com o layout eu instalo fcitx5. Input method, pra quem não conhece, é a camada que transforma sequência de teclas em caracteres. É onde entram deadkeys (til pra nasalizar, acento agudo, circunflexo), composição de caracteres que não estão no teclado (ç, Ç, letras acentuadas em maiúscula), suporte a emoji. Em app Qt ou GTK, o input method também cuida dos menus de contexto pra cedilha e acentos.</p>
<p>Pacotes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed fcitx5 fcitx5-configtool fcitx5-gtk fcitx5-qt</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E as variáveis de ambiente pros toolkits encontrarem o fcitx5. Criei <code>~/.config/environment.d/fcitx.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>INPUT_METHOD=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
SDL_IM_MODULE=fcitx</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>environment.d</code> do systemd é carregado antes das sessões gráficas, então Brave, Alacritty, VS Code e qualquer GTK/Qt pegam automaticamente. Habilito autostart no Hyprland:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>exec-once = fcitx5 -d</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Digitando <code>~a</code> vira <code>ã</code>, <code>'e</code> vira <code>é</code>, <code>ç</code> funciona como deveria em toda aplicação. Num teclado Thinkpad brasileiro, isso é a diferença entre conseguir escrever em português com naturalidade ou ficar catando letra.</p>
<h3>Áudio SOF<span class="hx:absolute hx:-mt-20" id="áudio-sof"></span>
    <a href="#%c3%a1udio-sof" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Codec Realtek ALC3306/ALC287 via Sound Open Firmware. Sem o <code>sof-firmware</code>, o módulo de kernel carrega mas o DSP nunca boota e o PipeWire cai silenciosamente pra <code>auto_null</code>. Resultado: você acha que o alto-falante está no mudo, mas na verdade o PipeWire não tem device nenhum.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed sof-firmware alsa-ucm-conf pipewire pipewire-pulse wireplumber</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se precisar forçar SOF em vez do HDA legacy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;options snd-intel-dspcfg dsp_driver=3&#34;</span> <span class="p">|</span> sudo tee /etc/modprobe.d/alsa.conf</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Reload sem reboot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo modprobe -r snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">sudo modprobe snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">systemctl --user restart wireplumber pipewire pipewire-pulse</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Firmware updates pelo fwupd<span class="hx:absolute hx:-mt-20" id="firmware-updates-pelo-fwupd"></span>
    <a href="#firmware-updates-pelo-fwupd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/bios-update.jpg" alt="Firmware update da Lenovo (ME Corp) rodando no boot"  loading="lazy" /></p>
<p><code>fwupdmgr update</code> funciona, mas com Limine tem uma pegadinha: o fwupd tenta escrever em <code>/boot/EFI/systemd/</code> ou <code>/boot/EFI/arch/</code>, que não existem. O workaround:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mkdir -p /boot/EFI/arch
</span></span><span class="line"><span class="cl">sudo fwupdmgr update -y --no-reboot-check
</span></span><span class="line"><span class="cl">fwupdmgr get-history  <span class="c1"># deve mostrar &#34;Success&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>HiDPI / fractional scaling<span class="hx:absolute hx:-mt-20" id="hidpi--fractional-scaling"></span>
    <a href="#hidpi--fractional-scaling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Painel 14&quot; em 1920x1200 numa resolução do Hyprland livre. O auto 1.5x do Omarchy ficava chunky demais. Fixei em 1.333x:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>env = GDK_SCALE,1
monitor=,preferred,auto,1.3333,vrr,2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Efetivo 1440x900. GTK com <code>GDK_SCALE=1</code> renderiza 1:1 com o Hyprland (não magnifica duplo). VRR mode 2 só em fullscreen, porque painéis LCD tendem a cintilar em conteúdo estático com VRR ativa.</p>
<p>Flags do Brave e Chromium pra renderizar bem nessa escala:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>--ozone-platform=wayland
--enable-features=WaylandFractionalScaleV1,UseOzonePlatform,VaapiVideoDecoder,VaapiVideoEncoder
--enable-gpu-rasterization</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O VAAPI faz diferença real na bateria em YouTube.</p>
<h2>Tunando Omarchy pra ser meu<span class="hx:absolute hx:-mt-20" id="tunando-omarchy-pra-ser-meu"></span>
    <a href="#tunando-omarchy-pra-ser-meu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Omarchy vem com defaults bons. O que eu ajusto é no topo deles, sem tocar em <code>~/.local/share/omarchy/</code> (que é clobbered pelo <code>omarchy-update</code>). Toda customização fica em <code>~/.config/</code>.</p>
<h3>Infra: Btrfs, snapshots, Snapper<span class="hx:absolute hx:-mt-20" id="infra-btrfs-snapshots-snapper"></span>
    <a href="#infra-btrfs-snapshots-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Omarchy já vem com Btrfs e subvolumes separados:</p>
<table>
  <thead>
      <tr>
          <th>Subvolume</th>
          <th>Mount</th>
          <th>Entra nos snapshots?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>@</code></td>
          <td><code>/</code></td>
          <td>sim</td>
      </tr>
      <tr>
          <td><code>@home</code></td>
          <td><code>/home</code></td>
          <td>não</td>
      </tr>
      <tr>
          <td><code>@log</code></td>
          <td><code>/var/log</code></td>
          <td>não</td>
      </tr>
      <tr>
          <td><code>@pkg</code></td>
          <td><code>/var/cache/pacman/pkg</code></td>
          <td>não</td>
      </tr>
  </tbody>
</table>
<p><code>@home</code> separado significa que <code>~/.cache</code>, <code>~/.config/BraveSoftware</code>, etc. não incham snapshots do root. Snapshot é de sistema, não de perfil.</p>
<p>Swap: zram de 4 GB com prioridade 100 (hit primeiro), mais swapfile de 30 GB com prioridade 0 (habilita hibernação, dimensionado pra RAM).</p>
<p>O stack de snapshot: Snapper tira snapshot antes e depois de cada <code>pacman -Syu</code> via snap-pac. <code>limine-snapper-sync</code> escreve esses snapshots no menu do Limine, então dá pra bootar num snapshot anterior pra reverter. Se algo quebra depois de update, você segura uma tecla no boot, escolhe o snapshot pré-update, boota read-only pra verificar, e se estiver bom, faz <code>snapper rollback</code>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/btrfs-snapshots-config.png" alt="Subvolumes Btrfs separados pra que snapshots do root não inchem com cache de usuário"  loading="lazy" /></p>
<p>Omarchy deixa o <code>snapper-cleanup.timer</code> e o <code>snapper-boot.timer</code> desabilitados por default. Habilitei os dois e configurei retenção pra caber num SSD de 1 TB sem explodir:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>TIMELINE_LIMIT_HOURLY=10
TIMELINE_LIMIT_DAILY=0
TIMELINE_LIMIT_WEEKLY=1
TIMELINE_LIMIT_MONTHLY=1
NUMBER_LIMIT=50</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Um detalhe que custa debug pra quem esquece: Docker e Ollama escrevem gigabytes em <code>/var/lib/docker</code> e <code>/var/lib/ollama</code>. Se isso cai dentro de <code>@</code>, os snapshots catastrofizam. Cada imagem Docker ou modelo Ollama baixado triplica em tamanho. Criei subvolumes aninhados pros dois, com <code>chattr +C</code> pra desligar CoW:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo btrfs subvolume create /var/lib/docker
</span></span><span class="line"><span class="cl">sudo chattr +C /var/lib/docker
</span></span><span class="line"><span class="cl">sudo btrfs subvolume create /var/lib/ollama</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso tem que ser feito ANTES de o Docker ou Ollama escrever dados. Se já tem dados lá, precisa migrar.</p>
<h3>NFS pro NAS Synology<span class="hx:absolute hx:-mt-20" id="nfs-pro-nas-synology"></span>
    <a href="#nfs-pro-nas-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Meu Synology expõe três volumes. No desktop, monto normal. No notebook, tem que ser mais defensivo. O notebook anda, esquece a rede, conecta em WiFi público. Fstab do notebook:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>nfs4  _netdev,noauto,nofail,x-systemd.automount,x-systemd.idle-timeout=10min,x-systemd.mount-timeout=15s,noatime,nodiratime,nconnect=4,actimeo=10,soft,timeo=30,retrans=2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Diferença crítica em relação ao desktop: <code>soft</code> com timeouts curtos (não fica pendurado pra sempre), <code>x-systemd.idle-timeout=10min</code> (auto-unmount quando ocioso), sem <code>network-online.target</code> no require (não atrasa boot). Resultado prático: <code>cd /mnt/gigachad</code> em casa monta lazy, fora de casa falha rápido sem trancar shell.</p>
<p>Outro detalhe importante: meu user no notebook tem UID 1026, que bate com a permissão do share no Synology. O Linux default cria user em 1000, o Synology impõe identidade via UID no fio. Se os UIDs não batem, você não consegue ler os arquivos, ou ainda pior, escreve como nobody. Rodei de TTY (com usuário deslogado) os <code>usermod</code>/<code>groupmod</code> pra remapear o user pra 1026/1026 e fiz <code>chown -R</code> em <code>/home</code>.</p>
<h3>Hardening de WiFi público<span class="hx:absolute hx:-mt-20" id="hardening-de-wifi-público"></span>
    <a href="#hardening-de-wifi-p%c3%bablico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Notebook vai sair de casa. Num aeroporto ou café, eu não quero anunciar hostname, não quero MAC rastreável, não quero serviço escutando porta aberta.</p>
<p><code>/etc/NetworkManager/conf.d/00-macrandomize.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[device]
wifi.scan-rand-mac-address=yes

[connection]
wifi.cloned-mac-address=stable
ethernet.cloned-mac-address=stable
connection.stable-id=${CONNECTION}/${BOOT}

ipv6.ip6-privacy=2
ipv6.addr-gen-mode=stable-privacy</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>MAC aleatório por scan (anti-fingerprinting passivo). MAC clonado estável por SSID (o portal cativo não te re-pergunta login toda vez) mas diferente entre redes. IPv6 com endereços temporários e interface ID derivado de segredo estável, não do MAC (sem vazar EUI-64).</p>
<p><code>/etc/systemd/resolved.conf.d/hardening.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Resolve]
LLMNR=no
MulticastDNS=no</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mata broadcast de hostname em LLMNR e mDNS. Também desabilito avahi:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl disable --now avahi-daemon.service avahi-daemon.socket</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>UFW firewall:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw default deny incoming
</span></span><span class="line"><span class="cl">sudo ufw default allow outgoing
</span></span><span class="line"><span class="cl">sudo ufw --force enable</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>SSH server já é off por default no Omarchy. Nada escutando. Mesmo que o firewall vaze, não tem superfície.</p>
<p>Tudo isso entra num único ritual de setup inicial.</p>
<h3>Persistência de SSH agent (keyring + keychain)<span class="hx:absolute hx:-mt-20" id="persistência-de-ssh-agent-keyring--keychain"></span>
    <a href="#persist%c3%aancia-de-ssh-agent-keyring--keychain" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se você usa SSH sério, quer digitar a passphrase uma vez por boot e ter <code>ssh</code>, <code>git push</code>, <code>scp</code> mudos pelo resto do dia. No Arch atual isso tem uma pegadinha: o <code>gnome-keyring</code> 50 deixou de oferecer componente SSH, e o substituto (<code>gcr-ssh-agent</code>) é um agente plain em memória, sem persistência de passphrase. A caixa &ldquo;lembrar esta chave&rdquo; que você viu em guias antigos simplesmente não existe mais.</p>
<p>A combinação que funciona tem três peças:</p>
<ol>
<li><code>gcr-ssh-agent.socket</code> gerenciado pelo systemd serve o <code>SSH_AUTH_SOCK</code> em <code>$XDG_RUNTIME_DIR/gcr/ssh</code></li>
<li><code>pam_gnome_keyring</code> no login SDDM destrava o keyring com a senha de login (usado por apps GUI tipo Brave, não pelo SSH)</li>
<li><code>keychain</code> (wrapper) mantém um <code>ssh-agent</code> vivo entre logouts, sobrescreve o <code>SSH_AUTH_SOCK</code> pra apontar pra esse agent persistente, e cacheia o PID em <code>~/.keychain/&lt;host&gt;-sh</code></li>
</ol>
<p>Primeiro, instalo o keyring:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed gnome-keyring seahorse keychain</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois, edito <code>/etc/pam.d/sddm</code> pra o login password destravar o keyring automaticamente. Adiciono no topo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-auth      optional    pam_gnome_keyring.so</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E no final:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-session   optional    pam_gnome_keyring.so    auto_start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O hífen na frente (<code>-auth</code>, <code>-session</code>) marca como opcional, se o módulo não carregar, não quebra login.</p>
<p>Pino o <code>SSH_AUTH_SOCK</code> pra sessões gráficas e TTY via <code>~/.config/environment.d/ssh-agent.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>SSH_AUTH_SOCK=%t/gcr/ssh</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>%t</code> resolve pra <code>$XDG_RUNTIME_DIR</code>, tipo <code>/run/user/1026</code>. Habilito o socket do gcr e o linger do usuário (pra user daemons sobreviverem ao logout):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now gcr-ssh-agent.socket
</span></span><span class="line"><span class="cl">sudo loginctl enable-linger <span class="nv">$USER</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, no <code>~/.config/bash/init.sh</code> o keychain inicia ou reusa o agent:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v keychain <span class="p">&amp;</span>&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="o">[[</span> -r ~/.ssh/id_ed25519 <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>keychain --eval --quiet ~/.ssh/id_ed25519<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>--eval</code> emite atribuições shell (<code>SSH_AUTH_SOCK</code>, <code>SSH_AGENT_PID</code>) apontando pro agent do keychain, que passa por cima do gcr setado pelo environment.d. <code>--quiet</code> cala o banner depois que a chave está carregada.</p>
<p>Fluxo na prática: boot → primeiro terminal → keychain pede passphrase uma vez → chave fica carregada. Logout → login de novo → novos shells re-atacham no mesmo agent (via <code>~/.keychain/&lt;host&gt;-sh</code>) → zero prompt. <code>ssh-add -l</code> confirma que a chave está lá, <code>echo $SSH_AUTH_SOCK</code> confirma que está no caminho do keychain.</p>
<p>Quando você vai digitar a passphrase de novo: reboot, <code>ssh-add -D</code>, ou <code>keychain --clear</code>. Em uso normal, uma vez por dia.</p>
<h3>Bash em vez de ZSH<span class="hx:absolute hx:-mt-20" id="bash-em-vez-de-zsh"></span>
    <a href="#bash-em-vez-de-zsh" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No desktop uso ZSH. No notebook fui de Bash pra alinhar com o default do Omarchy, sem dupla de camadas modulares pra manter. <code>~/.bashrc</code> é um symlink pra <code>~/.config/bash/bashrc</code>, que faz source dos defaults do Omarchy primeiro e depois empilha as minhas customizações:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> ~/.local/share/omarchy/default/bash/rc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/envs.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/aliases.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/mounts.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/init.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/secrets    <span class="c1"># gitignored, chmod 600</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>envs.sh</code> tem o que é meu: OpenRouter base URL, Ollama apontando pro GPU box da rede local (192.168.0.14), AWS region, analytics do Hugo, configs de zoxide e SSH agent. <code>aliases.sh</code> tem os atalhos de TLP, um alias pra <code>shell-gpt</code> via Docker, e functions pra hardenar o PATH quando rodo <code>makepkg</code> ou <code>yay</code> (prevenir injeção de binário via user config malicioso).</p>
<p><code>init.sh</code> faz o trabalho de integração. Atuin com bind manual do Ctrl-R (pra deixar a seta pra cima com o history-search padrão do bash, que eu uso mais). Keychain carregando <code>~/.ssh/id_ed25519</code> uma vez por boot e reusando nos shells seguintes (sem ter que re-autenticar SSH toda hora). Blesh se estiver instalado (autosuggestion estilo ZSH pra Bash). E uma função que manda o PROMPT_COMMAND ajustar o título da janela com o pwd atual e o comando em execução:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">__title_idle<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">__title_busy<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">cmd</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASH_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;__title_&#34;</span>* <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="s2">&#34;PROMPT_COMMAND&#34;</span>* <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s — %s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="p">-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle; </span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;__title_busy&#39;</span> DEBUG</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No idle, título mostra só o pwd. Com um comando rodando, <code>trap DEBUG</code> pega o <code>BASH_COMMAND</code> em execução e atualiza o título. Integra com o <code>hyprland/window</code> do waybar pra mostrar tudo lá.</p>
<p>Pro toolbelt Rust moderno, a lista:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed <span class="se">\
</span></span></span><span class="line"><span class="cl">  eza bat fd ripgrep sd git-delta dust procs bottom duf tokei hyperfine <span class="se">\
</span></span></span><span class="line"><span class="cl">  zoxide atuin tldr starship</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>eza</code> substitui <code>ls</code>. <code>bat</code> substitui <code>cat</code> com syntax highlight. <code>fd</code> substitui <code>find</code>. <code>ripgrep</code> substitui <code>grep</code>. <code>sd</code> substitui <code>sed</code>. <code>delta</code> pluga no git pra diff side-by-side colorido. <code>dust</code> pro <code>du</code> visual. <code>procs</code> pro <code>ps</code>. <code>btm</code> (bottom) pro <code>top</code>. <code>duf</code> pro <code>df</code>. <code>tokei</code> conta linhas de código. <code>hyperfine</code> pra benchmark de comando. <code>zoxide</code> é <code>cd</code> com memória (muito útil). <code>atuin</code> é history do shell com sync criptografado (aponto pro meu home server via <code>sync_address = &quot;http://192.168.0.90:8888&quot;</code>). <code>starship</code> é o prompt. <code>tldr</code> é man page em 10 linhas.</p>
<p>O <code>atuin key</code> tem que ser backupeado offline. Se você perde, perde o histórico criptografado. Salvei no meu Bitwarden self-hosted (Vaultwarden), que eu documentei no <a href="/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">artigo sobre Bitwarden self-hosted</a>.</p>
<p>O git passa diff pelo delta:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[core]        pager = delta
[interactive] diffFilter = delta --color-only
[delta]       navigate side-by-side line-numbers hyperlinks, light=false
[merge]       conflictstyle = zdiff3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Hyprland e Waybar<span class="hx:absolute hx:-mt-20" id="hyprland-e-waybar"></span>
    <a href="#hyprland-e-waybar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Customizações visuais e de UX que fiz em cima do default.</p>
<p>No <code>hypr/looknfeel.conf</code>, gaps menores (2/5 vs 5/10 do default), animação de slide entre workspaces, VFR ligado (reduz consumo quando a tela está estática), <code>allow_session_lock_restore</code> (se o hyprlock crasha, volta pra tela de lock em vez de jogar pro desktop).</p>
<p>No <code>hypr/bindings.conf</code>:</p>
<table>
  <thead>
      <tr>
          <th>Binding</th>
          <th>Ação</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Super+B</code></td>
          <td>Brave</td>
      </tr>
      <tr>
          <td><code>Super+L</code></td>
          <td>Lock screen (default era layout toggle)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+L</code></td>
          <td>Layout toggle (movi pra cá)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+P</code></td>
          <td>Toggle TLP perf mode</td>
      </tr>
  </tbody>
</table>
<p>No Waybar, três módulos custom que recomendo:</p>
<p><strong>Título de janela</strong>: o módulo <code>hyprland/window</code> mostra o que tá focado. No bash, o PROMPT_COMMAND atualiza o título pra algo como &ldquo;~/Projects/blog — hugo server&rdquo;. Então no waybar aparece o diretório e o comando em execução, sem eu precisar olhar pro terminal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---window-title-on-waybar.png" alt="Waybar mostrando workspaces à esquerda e o título da janela focada com o pwd e comando em execução"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---another-window-title-on-waybar.png" alt="Outro exemplo do título da janela no waybar, dessa vez mostrando “Claude Code”"  loading="lazy" /></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;hyprland/window&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{title}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;max-length&#34;</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rewrite&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;(.{47}).+&#34;</span><span class="p">:</span> <span class="s2">&#34;$1…&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Perf mode</strong>: o <code>custom/perf</code> executa um script que lê o estado do TLP e mostra um ícone:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;custom/perf&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;exec&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-waybar.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;interval&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-toggle.sh&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Vazio em balanced, 󰓅 PERF em performance, 󰌪 ECO em low-power. Clique alterna. Útil pra ver rapidamente em que modo a máquina está.</p>
<p><strong>Claudebar</strong>: integrei o <a href="https://github.com/alfredopiquet/claudebar"target="_blank" rel="noopener">claudebar</a> num módulo custom que mostra a % de uso da sessão do Claude Code e o gasto extra inline no waybar. Sem ter que abrir outra janela.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---power-profile-and-claudebar-next-to-traybar.png" alt="Claudebar mostrando 9%, 1h16m restantes e $1120.74 de uso extra, seguido do indicador PERF e o tray"  loading="lazy" /></p>
<p>O clock:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;clock&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A %d %B - %H:%M}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format-alt&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A W%V %Y - %H:%M}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Exibe &ldquo;quinta 17 abril - 14:32&rdquo; por padrão, clique alterna pra &ldquo;quinta W16 2026 - 14:32&rdquo; com número da semana do ano, útil pra planejar artigos e tarefas.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---better-date-time-on-waybar-center.png" alt="Clock do waybar no centro com data e hora em português"  loading="lazy" /></p>
<h3>Atuin self-hosted<span class="hx:absolute hx:-mt-20" id="atuin-self-hosted"></span>
    <a href="#atuin-self-hosted" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Atuin roda no meu home server em 192.168.0.90:8888. Criei uma conta separada pro notebook (<code>akitaonrails-thinkpad</code>), não misturo histórico com desktop de propósito. <code>atuin sync</code> roda a cada 5 minutos e todo histórico é criptografado antes de ir pro server. O server não vê os comandos, só vê bytes criptografados.</p>
<h2>Conclusão: escolhendo notebook pra Linux<span class="hx:absolute hx:-mt-20" id="conclusão-escolhendo-notebook-pra-linux"></span>
    <a href="#conclus%c3%a3o-escolhendo-notebook-pra-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se tem uma coisa que eu aprendi nessa jornada: escolher notebook pra rodar Linux confortavelmente não é trivial. Precisa checar a <a href="https://wiki.archlinux.org/title/Laptop"target="_blank" rel="noopener">ArchWiki</a> pelo modelo específico antes de comprar. A do <a href="https://wiki.archlinux.org/title/Lenovo_ThinkPad_T14s_%28AMD%29_Gen_6"target="_blank" rel="noopener">T14s Gen 6 AMD</a>, por exemplo, cataloga cada peculiaridade de hardware e como contornar. Sem essa referência, você descobre os problemas instalando.</p>
<p>Regra que sigo: nunca comprar modelo recém-lançado. Deixar passar uns 6 a 12 meses depois do lançamento. Isso dá tempo pra comunidade iron out os bugs, pros drivers entrarem no mainline, pra ArchWiki ter uma página decente, pra libfprint incluir o sensor de digital, pro kernel cobrir o WiFi e a câmera IR. Comprar de primeira é assumir o papel de tester beta.</p>
<p>Marcas com trilha Linux decente: Lenovo (especialmente Thinkpad, eles oferecem Ubuntu e Fedora pré-instalados), Dell (a linha XPS e Latitude), Asus (Zenbook e ROG têm suporte razoável), Framework (fábrica direto pra Linux). Mac qualquer geração recente é cortado do Linux mainline ou via Asahi com caveats.</p>
<p>O Thinkpad T14 Gen 6 não é o notebook mais bonito que eu podia ter comprado. Mas é rugged, tem as portas certas, o sensor de digital funciona, e a carcaça plástica aceita cair, arranhar, viajar na mala sem eu ficar nervoso. Pra servir de companion de debug remoto, é o que eu precisava. Se esse é o seu perfil de uso também, recomendo. Se você quer OLED e metal, vai de Zenbook ou de T14s. Cada premium tem seu compromisso, nenhum notebook Linux em 2026 é perfeito.</p>
<p>Tudo que descrevi aqui ficou versionado num repo privado meu. Se eu precisar reinstalar amanhã, rodo meia dúzia de passos em ordem e volto ao mesmo lugar. Esse é o ponto todo de manter configuração em Git: notebook é commodity, config é minha.</p>
]]></content:encoded><category>omarchy</category><category>thinkpad</category><category>archlinux</category><category>hyprland</category><category>linux</category><category>homeserver</category></item><item><title>Por que as LLMs não te dão o resultado esperado | Por que eu prefiro Claude Code hoje</title><link>https://www.akitaonrails.com/2026/04/15/como-falar-com-o-claude-code-efetivamente/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/15/como-falar-com-o-claude-code-efetivamente/</guid><pubDate>Wed, 15 Apr 2026 13:00:00 GMT</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/hero-gpt-vs-claude.png" alt="Ilustração editorial comparando GPT-coding e Claude-coding: de um lado um ambiente com geração rápida e repertório amplo, do outro uma arquitetura estruturada com árvore de conceitos e depuração contextual" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Toda vez que eu entro numa discussão online sobre LLMs eu escuto alguma variação da mesma ladainha. &amp;ldquo;O Claude não performou tão bem quanto o GPT pra mim&amp;rdquo;. &amp;ldquo;O GPT fez um trabalho muito melhor que o Claude, vou cancelar minha assinatura do Claude&amp;rdquo;. &amp;ldquo;Pra mim o Kimi ou o MiniMax já dão conta, não preciso pagar nada&amp;rdquo;. Anedota atrás de anedota de &amp;ldquo;funciona pra mim&amp;rdquo; versus &amp;ldquo;não funciona pra mim&amp;rdquo;. Isso me soa muito estranho.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/hero-gpt-vs-claude.png" alt="Ilustração editorial comparando GPT-coding e Claude-coding: de um lado um ambiente com geração rápida e repertório amplo, do outro uma arquitetura estruturada com árvore de conceitos e depuração contextual"  loading="lazy" /></p>
<p>Toda vez que eu entro numa discussão online sobre LLMs eu escuto alguma variação da mesma ladainha. &ldquo;O Claude não performou tão bem quanto o GPT pra mim&rdquo;. &ldquo;O GPT fez um trabalho muito melhor que o Claude, vou cancelar minha assinatura do Claude&rdquo;. &ldquo;Pra mim o Kimi ou o MiniMax já dão conta, não preciso pagar nada&rdquo;. Anedota atrás de anedota de &ldquo;funciona pra mim&rdquo; versus &ldquo;não funciona pra mim&rdquo;. Isso me soa muito estranho.</p>
<p>Eu já benchmarquei boa parte dos modelos open source e comerciais relevantes no <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">post sobre testar LLMs</a>, então não é que eu esteja falando sem base. E além dos benchmarks, eu estou com mais de 500 horas acumuladas usando Claude Code e Codex em projetos reais. Em ritmo de 16 horas por dia, há dois meses e meio direto, gerando algo na ordem de 400 mil linhas de código efetivas.</p>
<p>E olha: em nenhum desses dois, Claude ou Codex, eu vi o modelo sair do trilho, fazer coisa que eu não pedi, ou ser incapaz de entregar o que eu queria. Nunca. Quando o modelo de fato não dava conta, ele me dizia isso antes, não simplesmente inventava. Então quando eu escuto alguém me contando que &ldquo;o Claude fez merda&rdquo;, minha primeira pergunta é sempre a mesma: o que você pediu pra ele, exatamente?</p>
<h2>O falso problema<span class="hx:absolute hx:-mt-20" id="o-falso-problema"></span>
    <a href="#o-falso-problema" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A resposta que o ecossistema está dando pra esse suposto descontrole é adicionar mais camada. Surgiu Spec Driven Development, surgiram templates de prompt com 15 seções, surgiram frameworks inteiros pra forçar o LLM a fazer mais perguntas antes de começar. Eu até respeito a ideia, mas acho que ela trata sintoma, não causa.</p>
<p>Eu pratico o que chamo de <a href="/2026/03/05/37-dias-de-imers%C3%A3o-em-vibe-coding-conclus%C3%A3o-quanto-a-modelos-de-neg%C3%B3cio/">Agile Vibe Coding</a>: aplicar técnicas de XP (pair programming, test-driven, feedback curto, refactor contínuo) em cima do prompting normal. Não preciso de framework. Não preciso de template de 3 páginas. Preciso das mesmas coisas que sempre foram necessárias pra trabalhar em software em equipe: saber o que eu quero, saber o que eu não quero, saber como validar que chegou lá.</p>
<h2>O problema real é um só: ninguém sabe se comunicar<span class="hx:absolute hx:-mt-20" id="o-problema-real-é-um-só-ninguém-sabe-se-comunicar"></span>
    <a href="#o-problema-real-%c3%a9-um-s%c3%b3-ningu%c3%a9m-sabe-se-comunicar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu tenho um post antigo de 2013 chamado <a href="/2013/11/02/off-topic-programadores-sao-pessimos-comunicadores-udp-vs-tcp/">Programadores são péssimos comunicadores (UDP vs TCP)</a>. Leia se ainda não leu, porque o problema que eu descrevo lá em 2013 é exatamente o que está explodindo agora que todo mundo está pilotando LLM. Nada mudou. A tecnologia ficou mais poderosa, mas as pessoas continuam as mesmas.</p>
<p>A coisa funciona assim: você tem um monte de informação na sua cabeça. Contexto de projeto, histórico, restrição de stack, preferência pessoal, coisa que já deu errado no passado, combinado que foi feito numa reunião mês retrasado. E aí você entra na conversa, seja com colega humano ou com LLM, e dispara o pedido assumindo que tudo aquilo que está na sua cabeça também está na cabeça do outro lado. &ldquo;É óbvio, todo mundo sabe disso&rdquo;. Aí você escreve &ldquo;faça como eu estou dizendo&rdquo;, só que o que você está dizendo é na verdade &ldquo;faça como eu estou pensando&rdquo;. E você nem percebe que são coisas diferentes.</p>
<p>Desenvolvedor é péssimo comunicador. Gestor também é péssimo comunicador, e é exatamente por isso que a maior parte do tempo útil de uma semana corporativa é desperdiçada em reunião inútil. Ninguém chega ao ponto na hora certa, ninguém alinha expectativa, o resultado vem aquém do esperado, e a resposta gerencial padrão pra isso sempre é: &ldquo;mais do mesmo&rdquo;. Mais reunião, mais planilha, mais relatório. Só que se a comunicação era ruim em volume 1, vai continuar ruim em volume 5. O problema é qualidade. Volume não resolve qualidade ruim.</p>
<h2>Como eu realmente falo com Claude ou Codex<span class="hx:absolute hx:-mt-20" id="como-eu-realmente-falo-com-claude-ou-codex"></span>
    <a href="#como-eu-realmente-falo-com-claude-ou-codex" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu trato qualquer LLM exatamente como eu trataria um par humano numa sessão de pair programming. Sem firula, sem formulário, sem spec de 10 páginas. Mas com disciplina de comunicação. Deixa eu te mostrar um exemplo real, de semana passada.</p>
<p>Eu tenho cerca de 12 TB de ROMs acumuladas no meu NAS, em <code>/mnt/terachad/Emulators</code>, divididas em duas árvores (<code>ROMS/</code> e <code>ROMS2/</code>) que vieram de conjuntos diferentes ao longo dos últimos 10 e tantos anos. São mais de 400 mil arquivos no total. Romsets sem descompactar, <code>.7z</code>, <code>.rar</code>, bundles gigantes de CDI/GDI, nome de arquivo inconsistente, duplicata pra todo lado. Eu queria consolidar tudo isso por plataforma numa nova árvore <code>ROMS_FINAL/</code>, seguindo nomenclatura padrão (No-Intro / Redump / TOSEC), pra quando eu rodar Screenscraper depois o match ser direto. Esse era o <strong>objetivo</strong>, e eu declaro logo de cara.</p>
<p>Mas eu não paro aí. Eu digo também o que eu <strong>NÃO</strong> quero. &ldquo;Nunca deduplica por nome de arquivo, só por sha1+tamanho, nome mente demais nesse mundo aqui&rdquo;. &ldquo;Romset de Neo Geo depende do emulador que vai consumir, então o pacote de MAME, o bundle do FBNeo e o cart do Darksoft MVS são três coisas incompatíveis, guarda uma cópia canônica de cada&rdquo;. &ldquo;Mesma ideia pra NAOMI: romset do MAME não é o mesmo arquivo do GDI&rdquo;. &ldquo;Saturn tem versão USA, Japão e Europa, quero manter cópia de cada região, região não é duplicata&rdquo;. Se eu omitir isso, o modelo não tem como saber, porque esse conhecimento está na minha cabeça e não no código. Se eu não der, ele vai assumir o default mais razoável dele, que pode ser o oposto do que eu preciso.</p>
<p>Depois eu entro em <strong>detalhe de método</strong>. &ldquo;Cria um diretório <code>docs/</code> pra virar base de conhecimento viva e <code>docs/scripts/</code> com as etapas separadas em arquivos numerados (<code>01_walk_and_hash.py</code>, <code>02_classify.py</code>, etc). Cada etapa tem que ser idempotente pra eu poder reexecutar se travar ou se eu precisar retomar do meio. O estado de progresso mora num catálogo SQLite, não em variável em memória&rdquo;. Isso é alinhar o jeito que eu trabalho, não microgerenciar o modelo. Eu sei que uma operação dessas vai ter problema, e eu quero ter como voltar sem perder as horas anteriores de hash e classificação.</p>
<p>Aí, mesmo depois dele me apresentar o plano, eu continuo pensando no que pode dar errado. &ldquo;Zero deleção sob <code>ROMS/</code> e <code>ROMS2/</code>. Quem não for promovido pra <code>ROMS_FINAL/</code> simplesmente fica onde estava. A única coisa que pode ser apagada em todo o pipeline são arquivos temporários de extração que ficou pela metade. Além disso, a fase que de fato executa os moves só pode rodar depois que eu aprovar a fase de planejamento manualmente, cria um arquivo de flag <code>docs/.phase4-approved</code> e faz a fase 5 recusar iniciar sem ele&rdquo;. Isso é o equivalente a fazer commit antes de um refactor grande, mais um gate humano entre planejar e aplicar. Eu estou blindando contra erro meu, dele, ou de ambos.</p>
<p>Quando o modelo começa a rodar, <strong>eu não saio da sala</strong>. Fico pedindo status. Reparo que a ETA do hash tá longa demais pro tamanho do problema, aí interrompo: &ldquo;acho que dá pra paralelizar, tenho CPU sobrando e 10GbE pra falar com o NAS, e o Synology com NFS aguenta isso de boa. Sobe o paralelismo, testa, verifica que continua estável e que a ordem das transações no SQLite não quebra&rdquo;. Esse tipo de intervenção é pair programming de verdade, não automação cega.</p>
<h2>A estrutura por trás disso<span class="hx:absolute hx:-mt-20" id="a-estrutura-por-trás-disso"></span>
    <a href="#a-estrutura-por-tr%c3%a1s-disso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Repare no padrão. Eu não digo &ldquo;resolva esse problema&rdquo;. Eu digo &ldquo;resolva esse problema, <strong>dessa e dessa forma</strong>, e <strong>não desse e desse jeito</strong>, e quando terminar, <strong>valide X e Y</strong>&rdquo;. Ou seja, eu comunico quatro coisas, não uma:</p>
<p>Primeiro, <strong>o que eu quero</strong>. O objetivo final em linguagem clara. Segundo, <strong>o jeito que eu quero que seja feito</strong>, em linhas gerais, deixando espaço pra ele sugerir solução melhor se tiver, porque ele realmente costuma ter. Terceiro, <strong>o que eu não quero</strong>. Essa é a parte que mais gente pula, e é a mais crítica, porque aqui é onde mora todo o pressuposto não-verbalizado que vira bug depois. Quarto, <strong>como a gente valida que deu certo</strong>. Qual é o resultado esperado, qual é o teste, qual é o sinal de &ldquo;pronto&rdquo;.</p>
<p>E essa quarta parte é traiçoeira. A maioria dos clientes com quem trabalhei nos últimos 20 anos não sabia dizer qual era o resultado esperado. Porque é fácil querer algo, e difícil saber como medir que esse algo chegou. Sem medida de sucesso, expectativa quebra por definição, porque não existia expectativa concreta pra começar. Esse é um dos principais motivos pra projeto de consultoria dar errado, e é idêntico no mundo dos agentes de IA.</p>
<p>Quando eu entrego esses quatro blocos pro modelo, ele quase nunca falha. E quando ele realmente não consegue, porque a tarefa é impossível dada as minhas restrições ou porque falta informação que eu esqueci de passar, ele não tenta adivinhar. Ele me responde: &ldquo;dadas as suas restrições, não consigo seguir porque X ou Y&rdquo;. Aí eu ajusto, ou eu relaxo a restrição, ou eu percebo que eu mesmo não sabia o que queria. Tudo funciona.</p>
<h2>Depois que o contexto está firme, pergunte em vez de mandar<span class="hx:absolute hx:-mt-20" id="depois-que-o-contexto-está-firme-pergunte-em-vez-de-mandar"></span>
    <a href="#depois-que-o-contexto-est%c3%a1-firme-pergunte-em-vez-de-mandar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma virada que eu sempre faço depois dos primeiros prompts bem detalhados. Nos turnos iniciais eu despejo muito: objetivo, restrição, método, validação, todo o contexto que o modelo precisa pra entender o terreno. É muito detalhe, é trabalho meu, e eu já expliquei por que não tem como ser diferente.</p>
<p>Mas uma vez que esse chão ficou firme, eu paro de prescrever solução e começo a pedir sugestão. Em vez de dizer &ldquo;implementa dessa e dessa forma, usando essa biblioteca, com esse padrão&rdquo;, eu mudo pra &ldquo;dado tudo que a gente já conversou, qual seria a melhor abordagem aqui? Pesquisa online se precisar, compara as opções, e me apresenta a solução que você acharia mais adequada pro objetivo&rdquo;.</p>
<p>Essa é a parte que mais gente não faz, e é justamente aí que o LLM ganha autonomia útil. Ele tem muito mais vocabulário do que eu em uma porção de assuntos. Ele leu a internet inteira; eu leio o que dá tempo. Se eu prescrevo linha a linha, eu estou jogando essa vantagem fora. Se eu estruturo o contexto e pergunto, ele volta com opção que eu não tinha considerado, comparando trade-off, às vezes melhor do que minha ideia original, e aí eu escolho.</p>
<p>A chave é simples: <strong>perguntar bem exige ter contextualizado bem antes</strong>. Pergunta seca, sem chão, volta com resposta genérica ou com a primeira ideia que colou. Pergunta apoiada em cima de contexto já estabelecido volta com proposta de verdade, com comparação entre caminhos, com referência. O modelo está pronto pra esse tipo de contribuição, só que só entrega isso depois que você gastou o tempo de montar o palco.</p>
<p>E olha: esse método não foi inventado pra LLM. É o mesmo jeito que eu trato qualquer desenvolvedor humano, sênior ou não. Dou o contexto, dou o objetivo, explico o que eu considero importante, e aí pergunto &ldquo;qual seria o melhor caminho?&rdquo; em vez de sair ditando solução. A única diferença é que o LLM devolve em segundos o que um humano levaria dias pra te trazer. O método de conversar é o mesmo.</p>
<h2>Não é spec de 10 páginas<span class="hx:absolute hx:-mt-20" id="não-é-spec-de-10-páginas"></span>
    <a href="#n%c3%a3o-%c3%a9-spec-de-10-p%c3%a1ginas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E aqui vem uma parte importante: isso que eu descrevo não é um formalismo. Não é uma spec longa, não é um documento em Confluence, não é um template. É só como eu converso com qualquer pessoa que precisa entregar algo pra mim. Aprendi a fazer isso tocando projeto, gerenciando terceirizado, integrando time que não era meu, e levando porrada quando minha comunicação foi ruim. Com o tempo vira segunda natureza.</p>
<p>Se eu não ligo muito pro resultado final, tipo experimento rápido, brincadeira de fim de semana, coisa descartável, eu encurto drasticamente. Sei que minha expectativa pode quebrar, e tá tudo bem, o custo de um bug aqui é baixo. Mas se o resultado importa de verdade, eu invisto o tempo necessário pra dar ao outro lado (humano ou IA) a melhor chance possível de entregar o que eu quero. A saída é proporcional ao tempo que você gasta no input. Se você não está disposto a explicar direito o que quer, não reclame quando vier errado.</p>
<p>Pra ilustrar: este post aqui que você está lendo foi escrito pelo Claude, com base num prompt meu. Abaixo está a captura de tela do que eu digitei. Repare: sem pressuposto omitido, objetivo claro, referências embutidas, restrições explícitas, nível de detalhe suficiente pra ele não precisar adivinhar.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/prompt-example.png" alt="Captura de tela do prompt que usei pra pedir pro Claude Code escrever este artigo: objetivo, contexto, referências aos posts anteriores, restrições e resultado esperado, tudo declarado de forma direta"  loading="lazy" /></p>
<p>Mas olha, isso aqui é só o primeiro prompt. Ele não é o único. Enquanto o Claude escrevia, eu continuei acompanhando, enviando correção, acrescentando ponto que eu esqueci de botar no primeiro prompt, apontando erro factual que eu vi no texto gerado, e ajustando tom. &ldquo;Ah, esqueci de te falar que também é importante abordar X&rdquo;. &ldquo;Não, essa referência aqui está desatualizada, o Sora da OpenAI foi descontinuado, conserta&rdquo;. &ldquo;Lê o que a gente já deixou documentado lá em <code>/mnt/terachad/Emulators/docs/</code> pra ver se dá pra melhorar o exemplo das ROMs&rdquo;. Inclusive essa própria observação que você está lendo agora virou um prompt no meio do caminho, que foi:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/prompt-followup.png" alt="Captura de tela do prompt de acompanhamento pedindo pro Claude ler os docs reais da organização de ROMs e explicar também que o prompt do blog não é um só, é iterativo"  loading="lazy" /></p>
<p>E assim foi seguindo o papo até a hora do commit. Inclusive quando eu já tinha pedido pra humanizar, traduzir pro inglês e dar push, eu ainda interrompi no último segundo porque bati o olho numa palavra inglesa gratuita que eu queria corrigir antes de subir. A conversa é essa:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/prompt-anglicism.png" alt="Captura de tela do momento em que eu já havia autorizado a tradução e o commit, mas interrompi antes pra pedir pra corrigir o anglicismo “figurar as coisas do nada” pra algo mais natural em português"  loading="lazy" /></p>
<p>Isso é pair programming de verdade. Ninguém senta em frente ao colega, solta uma tarefa de 15 linhas, levanta e vai embora esperando mágica. Você fica ali, acompanha a execução, vê o código nascer, sugere ajuste, pega erro enquanto ainda é barato consertar, adiciona contexto que você lembrou agora. O prompt inicial é o ponto de partida, não o contrato final. Agile Vibe Coding na veia: ciclos curtos, feedback rápido, correção contínua.</p>
<p>Isso não é spec formal. É conversa que continua durante o trabalho, não antes dele. E é assim que vai ser sempre, comigo e com qualquer bom profissional.</p>
<h2>&ldquo;Akita, você escreve detalhe demais, a IA não devia descobrir isso sozinha?&rdquo;<span class="hx:absolute hx:-mt-20" id="akita-você-escreve-detalhe-demais-a-ia-não-devia-descobrir-isso-sozinha"></span>
    <a href="#akita-voc%c3%aa-escreve-detalhe-demais-a-ia-n%c3%a3o-devia-descobrir-isso-sozinha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a pergunta que eu sei que vai aparecer, então deixa eu responder antes. Não, a IA não vai descobrir sozinha. Não existe versão futura do Claude, do GPT, do Gemini, do que for, que vai adivinhar o que está na sua cabeça. Contexto não se gera por osmose. Se a informação não existe no código, nos docs, ou na minha pergunta, ela simplesmente não existe pro modelo. Ponto.</p>
<p>Romset de Neo Geo ser diferente por emulador? Isso é conhecimento de domínio. Saturn ter região separada? Conhecimento de domínio. Meu NAS ter 10GbE pra aguentar paralelismo agressivo? Contexto de ambiente. Ter <code>docs/</code> com catálogo SQLite pra sobreviver a crash? Decisão de engenharia minha. Nada disso o modelo tem como &ldquo;descobrir&rdquo;. Tudo isso é trabalho meu de trazer pra conversa. E se eu não estou disposto a gastar meu tempo pra conhecer esses detalhes, por que alguém, ou alguma coisa, faria isso por mim de graça?</p>
<p>A regra é simples: <strong>a qualidade do que te entregam é diretamente proporcional ao esforço que você colocou em pedir</strong>. Isso nunca foi diferente. Quem já contratou terceirizado sabe: pedido vago, escopo mal definido, &ldquo;o cliente sabe o que quer, só não consegue explicar&rdquo;, é receita garantida pra projeto derrapar. Sempre foi. O LLM é a mesma coisa, só que mais rápido e mais paciente. Pensa nele como terceirizado moderno, não como mágico. Mágico resolve sem você dizer nada. Terceirizado resolve exatamente o que você pediu, do jeito que você pediu, com as informações que você deu. Se você não pediu direito, não recebe direito.</p>
<p>A frustração de quem chega aqui cansado do Claude ou do Codex é quase sempre a mesma: pediu pouco, esperou muito. E quando o resultado veio abaixo, culpou a ferramenta. Nunca a própria pergunta.</p>
<h2>Por isso a IA não vai substituir os bons<span class="hx:absolute hx:-mt-20" id="por-isso-a-ia-não-vai-substituir-os-bons"></span>
    <a href="#por-isso-a-ia-n%c3%a3o-vai-substituir-os-bons" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>É por essa razão que eu digo, com bastante convicção, que agente de IA não vai substituir os bons profissionais. Vai substituir gente que não sabe fazer a própria pergunta direito, que não sabe o que quer, que não sabe validar o resultado, que precisa de alguém pra pensar por ela. E olha, esse tipo de profissional sempre foi substituível, só que agora o substituto é mais barato. O mercado está fazendo a conta.</p>
<p>O bom profissional, ao contrário, virou mais produtivo. Usa o LLM como assistente de pair programming a 2h da manhã, sem reclamar, sem sindicato, sem disputa de ego. Entrega em uma semana o que antes levava um mês. E continua sendo o bom profissional que era, porque a habilidade que importava, ou seja, saber o que pedir, o que não aceitar e como medir, continua sendo 100% dele. A ferramenta só executa.</p>
<h2>Stark + Jarvis = Homem de Ferro<span class="hx:absolute hx:-mt-20" id="stark--jarvis--homem-de-ferro"></span>
    <a href="#stark--jarvis--homem-de-ferro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Deixa eu trazer uma metáfora que eu acho que resume isso tudo. Pensa no Tony Stark, do MCU. Ele tem o Jarvis, provavelmente a IA fictícia mais avançada que a cultura pop produziu nas últimas duas décadas. Controle por voz, entendimento de contexto, planejamento, execução em paralelo, até uma personalidade afiada. Tecnologicamente, Jarvis seria facilmente o melhor agente de IA que você conseguiria imaginar hoje.</p>
<p>E mesmo assim, Jarvis sozinho não constrói uma armadura do Homem de Ferro. Não constrói o reator Arc. Não constrói nada dos equipamentos que fazem o Stark ser o Stark. Toda essa tecnologia avançada, sem um Stark pra conduzir, fica parada. É o gênio do Stark, a visão dele, a teimosia dele, a intuição de engenheiro que aponta o caminho, que faz tudo aquilo virar realidade. Jarvis executa. Stark pensa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/stark-iterating.jpg" alt="Tony Stark iterando em cima do modelo 3D do Mark III numa das suas bancadas, cercado de monitores com variações do design: é literalmente assim que trabalho de engenharia de verdade acontece, com IA ou sem"  loading="lazy" /></p>
<p>Tem outro detalhe dessa história que pouca gente lembra: nem mesmo o Stark, com todo o gênio dele e com o Jarvis do lado, acerta a armadura perfeita na primeira tentativa. Ele passa filme atrás de filme melhorando. A Mark I foi literalmente parafusada numa caverna com sucata. A Mark II já voava, mas congelava em altitude. A Mark III resolveu o gelo, mas pesava demais. E assim vai, cada iteração consertando um defeito específico da anterior, até chegar na Mark LXXXV em Endgame, a 85ª armadura. <strong>Oitenta e cinco</strong>. O arco inteiro do Homem de Ferro no MCU é, no fundo, uma campanha de dez anos de desenvolvimento iterativo em cima de um produto complexo.</p>
<p>Esse é exatamente o modelo mental que eu acho que a gente deveria usar trabalhando com IA hoje. Você não dispara um prompt e espera a solução definitiva sair pronta. Você, no papel do Stark, usa a IA (o seu Jarvis) como acelerador de cada volta do ciclo: propõe, testa, vê o que quebrou, conserta, propõe de novo. A IA acelera cada volta, ela não elimina as voltas. Se alguém trocou iteração por &ldquo;uma resposta mágica final&rdquo;, é porque nunca entendeu que engenharia nunca foi assim, com humano ou sem.</p>
<p>Junta Stark com Jarvis e aí sim você tem Homem de Ferro. Sem um dos dois, falta a metade que importa.</p>
<h2>Claude Code vs Codex: minha preferência hoje (abril de 2026)<span class="hx:absolute hx:-mt-20" id="claude-code-vs-codex-minha-preferência-hoje-abril-de-2026"></span>
    <a href="#claude-code-vs-codex-minha-prefer%c3%aancia-hoje-abril-de-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Só pra não passar batido: hoje em dia eu uso Claude Code e Codex intercalando, mas tenho preferido o Claude Code. Deixa eu explicar o motivo, porque não tem a ver com o LLM em si.</p>
<p>Claude Opus e GPT-5.4 xHigh, pra mim, estão empatadíssimos como modelos. Nas tarefas difíceis, quando um não dá conta, eu troco pro outro e normalmente o outro resolve. Cabeça a cabeça, os dois são fortes. O que separa um do outro hoje é o harness, não o modelo.</p>
<p>E o harness do Claude Code, hoje, é simplesmente superior. Dois motivos concretos: planejamento e execução em paralelo.</p>
<p><strong>Planejamento.</strong> Claude Code quebra tarefa longa em subtarefa, mantém uma to-do list visível que eu posso acompanhar na tela, tenta rodar o que dá pra rodar em paralelo, e não esquece item. Quando ele me diz &ldquo;terminei&rdquo;, eu sei que a lista inteira foi executada, porque está bem ali pra eu conferir. Esse detalhe muda o jogo: eu confio no &ldquo;terminei&rdquo; sem precisar ficar cobrando &ldquo;e aquele outro item, você fez mesmo?&rdquo;.</p>
<p><strong>Execução em paralelo.</strong> Aqui a diferença é clara. Se o Claude Code está no meio de uma tarefa e eu aperto <code>ESC</code> pra pedir outra coisa, ele normalmente <strong>mantém a primeira rodando e começa a segunda em paralelo</strong>, a menos que a nova pergunta exija cancelar a primeira. O Codex, na mesma situação, para a primeira pra atender a segunda, e nem sempre consegue retomar a primeira de onde parou sem eu mandar manualmente. Com Claude Code eu consigo fluir de verdade, abrindo frente nova enquanto as antigas continuam correndo. Com Codex eu preciso ser serial, paciente, e mais intencional em cada pedido, porque interromper sai caro.</p>
<p>Isso não significa que Codex é ruim. Ele é ótimo. Várias vezes quando o Claude Code emperra numa tarefa complicada eu abro o Codex e ele desata na hora. Só que aí eu mudo o jeito de trabalhar: pergunta menor, mais objetiva, uma por vez, espera terminar, parte pra próxima. Funciona, só não é o fluxo que eu prefiro.</p>
<p>Provavelmente o Codex vai equiparar esse lado do harness nos próximos meses, e aí a conversa muda. Mas hoje, 15 de abril de 2026, se eu tenho que escolher um dos dois como ferramenta principal, escolho Claude Code, e a razão é o harness, não porque o LLM da OpenAI seja inferior. Ficou o registro pra quando eu reler isso daqui a seis meses e achar graça.</p>
<h2>É por isso que minha empresa se chama Codeminer 42<span class="hx:absolute hx:-mt-20" id="é-por-isso-que-minha-empresa-se-chama-codeminer-42"></span>
    <a href="#%c3%a9-por-isso-que-minha-empresa-se-chama-codeminer-42" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra fechar com uma referência que eu venho carregando há anos: minha empresa se chama <a href="https://www.codeminer42.com/"target="_blank" rel="noopener">Codeminer 42</a>. O 42 não é número aleatório. É referência direta ao Douglas Adams, do Guia do Mochileiro das Galáxias.</p>
<p>Pra quem não conhece a piada, a história é assim. Uma civilização inteira constrói um supercomputador do tamanho de um planeta pra calcular a Resposta pra Vida, o Universo e Tudo Mais. Depois de milhões de anos de processamento, o computador cospe o resultado final: <strong>42</strong>. E aí dá aquele silêncio constrangedor, porque ninguém lembrava mais qual era a pergunta original. O 42 é um resultado tecnicamente correto de uma pergunta que ninguém soube formular. Por isso, não significa porcaria nenhuma. Muita gente acha que 42 significa alguma coisa profunda. Não significa. É a resposta errada pra pergunta errada.</p>
<p>Essa é a lição mais precisa sobre engenharia que eu já li em ficção. Toda expectativa que se quebra, quebra porque a pergunta estava errada, não porque a resposta foi mal executada. É pra isso que a Codeminer 42 existe, e é exatamente isso que eu pratico no meu dia a dia, com cliente humano ou com LLM. Antes de entregar o que você me pediu, meu trabalho é te fazer descobrir que o que você acha que quer provavelmente está errado, te forçar a repensar as suposições que você trouxe pra conversa, e só depois que a pergunta estiver afinada eu consigo te devolver o melhor resultado possível. Sem essa etapa, tudo que eu te entregar vai ser um 42.</p>
<p>Então da próxima vez que você ler alguém falando que &ldquo;cancelou o Claude porque ele não entrega&rdquo;, ou &ldquo;o GPT é muito melhor&rdquo;, ou o inverso, lembra de olhar com carinho pra pergunta que a pessoa estava fazendo. Nove em cada dez vezes, o problema não é o modelo. É a pergunta. E nove em cada dez vezes, a resposta que essa pessoa recebeu foi 42.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/talk-to-claude/deep-thought-42.webp" alt="O Pensador Profundo do Guia do Mochileiro das Galáxias revelando a Resposta pra Vida, o Universo e Tudo Mais pra multidão que esperou milhões de anos: 42"  loading="lazy" /></p>
]]></content:encoded><category>ai</category><category>claude-code</category><category>vibe-coding</category><category>agile</category><category>xp</category><category>communication</category></item><item><title>Seedance 2.0 finalmente foi lançado: Primeiras Impressões</title><link>https://www.akitaonrails.com/2026/04/15/seedance-2-0-finalmente-foi-lancado-primeiras-impressoes/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/15/seedance-2-0-finalmente-foi-lancado-primeiras-impressoes/</guid><pubDate>Wed, 15 Apr 2026 10:00:00 GMT</pubDate><description>&lt;p&gt;A ByteDance lançou hoje, 15 de abril, a versão pública do &lt;a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener"&gt;Seedance 2.0&lt;/a&gt;. Eles prometeram esse modelo no fim do ano passado, mas o acesso ficou restrito a parceiros comerciais por meses, com vários adiamentos pelo caminho. Hoje finalmente abriu pra qualquer um testar.&lt;/p&gt;
&lt;p&gt;A proposta é forte. É um modelo multimodal de geração de vídeo que aceita simultaneamente texto, até 9 imagens de referência, 3 clipes de vídeo de referência e 3 trilhas de áudio numa mesma chamada. Em cima disso, ele gera saída de até 15 segundos com áudio sincronizado nativo (diálogo, efeitos, música ambiente), suporta extensão de vídeo, edição pontual de personagens e objetos, e tem planejamento de câmera dirigido por prompt. A ByteDance posiciona o modelo pra publicidade comercial, vídeos explicativos, produção de filme, e-commerce e gaming. Ou seja: pra criador de conteúdo que quer escapar do estágio &amp;ldquo;meme aleatório&amp;rdquo; e começar a entregar peça com uso profissional.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A ByteDance lançou hoje, 15 de abril, a versão pública do <a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener">Seedance 2.0</a>. Eles prometeram esse modelo no fim do ano passado, mas o acesso ficou restrito a parceiros comerciais por meses, com vários adiamentos pelo caminho. Hoje finalmente abriu pra qualquer um testar.</p>
<p>A proposta é forte. É um modelo multimodal de geração de vídeo que aceita simultaneamente texto, até 9 imagens de referência, 3 clipes de vídeo de referência e 3 trilhas de áudio numa mesma chamada. Em cima disso, ele gera saída de até 15 segundos com áudio sincronizado nativo (diálogo, efeitos, música ambiente), suporta extensão de vídeo, edição pontual de personagens e objetos, e tem planejamento de câmera dirigido por prompt. A ByteDance posiciona o modelo pra publicidade comercial, vídeos explicativos, produção de filme, e-commerce e gaming. Ou seja: pra criador de conteúdo que quer escapar do estágio &ldquo;meme aleatório&rdquo; e começar a entregar peça com uso profissional.</p>
<p>Pra quem quer ver o modelo em ação antes de continuar lendo, recomendo essa visão geral feita pelo Stefan, do canal <a href="https://www.youtube.com/channel/UCRW08KcTVjXEmBzBsVl7XjA"target="_blank" rel="noopener">Stefan 3D AI Lab</a>, que eu já tinha indicado lá no <a href="/2026/01/23/ai-3d-ja-da-pra-modelar-3d-com-prompts/">post sobre modelagem 3D com IA</a>:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/fv9vA6RCNHU"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>A interface principal é o &ldquo;Photo Studio&rdquo; da Dreamina, onde você escolhe o modo de geração e empilha as referências. Por enquanto eu testei só o modo de multi-referência padrão.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/studio.png" alt="A interface do Photo Studio do Seedance 2.0, com painel de upload de referências e configuração de prompt"  loading="lazy" /></p>
<h2>Teste 1: lip-sync com áudio do podcast<span class="hx:absolute hx:-mt-20" id="teste-1-lip-sync-com-áudio-do-podcast"></span>
    <a href="#teste-1-lip-sync-com-%c3%a1udio-do-podcast" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Meu primeiro teste foi simples. Peguei alguns segundos do bumper de abertura do meu podcast <a href="/tags/themakitachronicles/">The M.Akita Chronicles</a>, que é gerado com IA via <a href="/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/">pipeline ElevenLabs v3</a>, passei o meu novo avatar em estilo anime como imagem de referência, e pedi pro Seedance fazer o personagem sincronizar boca e gesticular em cima daquele áudio.</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test1.mp4" type="video/mp4">
  </video>
  <em>Avatar anime sincronizando boca com áudio do bumper do M.Akita Chronicles</em>
</div>
<p>O resultado é decente. Saiu tudo de uma imagem fixa só. O lip-sync acompanha a fala razoavelmente bem, a expressão tem alguma vida, o gesto da mão entra no momento certo. Mas é o tipo de coisa que rende clipinho curto pro YouTube ou Instagram. Não é animação que você usaria como abertura de um vídeo profissional.</p>
<h2>A função que de fato importa: vídeo de referência<span class="hx:absolute hx:-mt-20" id="a-função-que-de-fato-importa-vídeo-de-referência"></span>
    <a href="#a-fun%c3%a7%c3%a3o-que-de-fato-importa-v%c3%addeo-de-refer%c3%aancia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E aqui eu chego no que eu considero o único uso de verdade desses modelos pra trabalho sério: usar <strong>vídeo como referência</strong>. Texto sozinho nunca vai ser preciso o suficiente. Você pode descrever a cena com o vocabulário que quiser, mudar adjetivo, especificar lente, ângulo, framing, e ainda assim cada geração vai te dar algo um pouco diferente. É loteria. Bom pra meme, péssimo pra produção.</p>
<p>A maioria dos amadores se impressiona com qualquer clipe aleatório que sai bonito de primeira, e enche o feed do X e do Instagram com isso. Só que aquilo não é controle. É sorte. Pra cinema, pra publicidade, pra abertura de programa, pra qualquer coisa onde a próxima cena precisa casar com a anterior, você tem que conseguir dizer pro modelo: &ldquo;faça exatamente esse movimento, com exatamente essa câmera&rdquo;. E é aí que o vídeo de referência muda o jogo, porque ele substitui horas de modelagem 3D, rigging, animação e renderização que custariam fortunas em estúdio.</p>
<p>Pro meu teste, eu reciclei o modelo que já tinha gerado lá no <a href="/2026/01/23/ai-3d-ja-da-pra-modelar-3d-com-prompts/">post sobre AI 3D com Hunyuan e Nano Banana</a>, onde testei o quanto dá pra modelar 3D do nada com prompt e ainda imprimir em 3D. Mas pra uma referência mais cinematográfica eu queria algo com animação pronta. Baixei do Sketchfab o modelo <a href="https://sketchfab.com/3d-models/darth-talon-4338105e09704f359c6afcab7e5fce10"target="_blank" rel="noopener">Darth Talon</a>, que tem uma sequência curta de movimento bem feita, só porque achei legal.</p>
<p>Importei o FBX no Blender, montei câmera e luz básica, e fiz uma renderização rápida no Cycles. Nada bonito, é só um esboço pra servir de referência de movimento e enquadramento.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender.png" alt="O modelo Darth Talon importado no Blender com câmera e iluminação configuradas pra render rápido no Cycles"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender-render.mp4" type="video/mp4">
  </video>
  <em>O render rápido no Cycles que vai virar referência de movimento</em>
</div>
<p>Subi pro Seedance esse render como vídeo de referência junto com três imagens em t-pose do personagem (frente, costas, lado esquerdo) pra dar consistência visual:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin: 1em 0; justify-content: center;">
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/front.jpg" alt="T-pose frontal" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/back.jpg" alt="T-pose de costas" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/left.jpg" alt="T-pose lateral esquerda" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
</div>
<p>Primeira tentativa, com prompt genérico:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test2.mp4" type="video/mp4">
  </video>
  <em>Primeira geração: o personagem ficou consistente, mas o movimento e a câmera não bateram com a referência</em>
</div>
<p>Não ficou ruim. O personagem mantém identidade, a iluminação melhorou bastante em cima do meu render bruto, e o vídeo é coerente. Só que o movimento e a posição de câmera divergiram bastante da referência. O modelo entendeu &ldquo;ali tem um personagem parecido com esse fazendo algo parecido com aquilo&rdquo;, e improvisou o resto.</p>
<p>Lembrei do tutorial do Stefan: o truque é colocar literalmente no prompt algo como <code>Follow exact motion and camera from reference video</code>. Tentei de novo:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test3.mp4" type="video/mp4">
  </video>
  <em>Segunda geração: instrução explícita pra seguir movimento e câmera da referência</em>
</div>
<p>Bem melhor. Está claramente seguindo a coreografia da referência, o ângulo de câmera bate em vários momentos, e a aparência do personagem ficou mais polida que tudo que eu poderia renderizar à mão num esboço de Cycles. Mas ainda assim não é cópia exata do meu render. Em alguns frames o modelo ainda toma liberdade com timing, posição e enquadramento. Existem outras técnicas de prompting e parâmetros pra ajustar, mas eu parei aqui pra registrar essas primeiras impressões enquanto o lançamento está fresco.</p>
<h2>ComfyUI, Runway e o resto do ecossistema<span class="hx:absolute hx:-mt-20" id="comfyui-runway-e-o-resto-do-ecossistema"></span>
    <a href="#comfyui-runway-e-o-resto-do-ecossistema" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Seedance 2.0 também já saiu <a href="https://blog.comfy.org/p/seedance-20-is-now-available-in-comfyui"target="_blank" rel="noopener">como nó do ComfyUI na nuvem</a>, o que ajuda muito quem quer encaixar a geração dentro de pipeline maior sem ficar pulando entre interface web. Pra quem já tem pipeline de geração de imagem montado em ComfyUI, isso é bem prático.</p>
<p>E pra quem quer um caminho com volume sem pensar em crédito por geração, a Runway está oferecendo o Seedance 2.0 dentro do <a href="https://www.mindstudio.ai/blog/seedance-2-0-runway-unlimited-plan"target="_blank" rel="noopener">plano Unlimited</a>, que sai por volta de US$ 76/mês no anual ou US$ 95/mês no mensal. &ldquo;Unlimited&rdquo; no caso significa sem cobrança por geração, mas com prioridade de fila menor em horários de pico e cota de armazenamento de saída. Pra quem precisa iterar muito, esse modelo de assinatura faz bem mais sentido que crédito avulso.</p>
<h2>A questão do preço<span class="hx:absolute hx:-mt-20" id="a-questão-do-preço"></span>
    <a href="#a-quest%c3%a3o-do-pre%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A precificação direta do Seedance é em crédito. O plano Standard sai por US$ 24,90/mês no anual ou US$ 49,90/mês no mensal, e te dá 2500 créditos por mês. Cada 10 segundos de vídeo gerado consome 6 créditos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/pricing.png" alt="Tabela de preços do Seedance 2.0, com planos Standard até o tier mais alto e custos em crédito por segundo de geração"  loading="lazy" /></p>
<p>A conta é cruel. Com 2500 créditos no Standard, dá pra rodar cerca de 416 gerações de 10 segundos por mês. Parece muito, mas lembra do meu próprio teste: foram pelo menos duas tentativas pra chegar perto do que eu queria, e mesmo assim não foi cópia exata do que pedi. Na prática, pra cada cena que você quer entregar bem, é razoável assumir 3 a 5 regerações. Isso derruba o número útil pra algo na faixa de 80 a 140 cenas curtas por mês. Pode até dar pra um criador solo fazendo conteúdo curto. Não dá pra produção que precisa de minutos contínuos de vídeo polido.</p>
<p>Os planos superiores escalam linearmente o número de créditos, mas o problema continua: no preço de hoje, o Seedance 2.0 ainda é ferramenta pra clipe curto e meme bem-feito, ou pra prototipagem de cena antes de mandar pra estúdio de verdade. Não substitui produção profissional contínua. Pra esse perfil, na prática a Runway com Unlimited sai mais em conta. <a href="https://seedance2.ai/pricing"target="_blank" rel="noopener">Confira a tabela de preços completa aqui</a>.</p>
<h2>O que ninguém quer falar: restrição e deepfake<span class="hx:absolute hx:-mt-20" id="o-que-ninguém-quer-falar-restrição-e-deepfake"></span>
    <a href="#o-que-ningu%c3%a9m-quer-falar-restri%c3%a7%c3%a3o-e-deepfake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem dois pontos que eu não quero deixar de fora. O primeiro: o Seedance 2.0 demorou a chegar pro grande público em parte porque a ByteDance está sendo bem mais conservadora que a concorrência na hora de moderar. O modelo bloqueia geração com referência de pessoas reais (celebridades, políticos, figuras públicas) e filtra personagens com IP forte de Disney, Marvel/DC, Nintendo e marcas registradas em geral. Quem quiser detalhe sobre o que passa e o que não passa, e por que a empresa optou por essa postura, tem <a href="https://www.mindstudio.ai/blog/seedance-2-0-content-restrictions-workarounds"target="_blank" rel="noopener">esse artigo da MindStudio explicando os limites</a>. Faz sentido: a ByteDance opera em dezenas de jurisdições, está sob escrutínio regulatório global, e ainda tem o tema espinhoso de Hollywood pressionando o setor inteiro com ações de copyright sobre uso de material protegido em treinamento e geração. O lançamento atrasou e veio com filtro pesado justamente por isso.</p>
<p>Segundo ponto, e mais importante: pra você, leitor comum, a mensagem é que <strong>deepfake deixou de ser hipótese</strong>. Foto na internet já não vale como prova há um bom tempo, porque modelos de imagem como Nano Banana, Hunyuan e congêneres entregam realismo que engana qualquer um. Agora vídeo está na mesma categoria. O que a gente acabou de ver acima, onde com algumas referências e dois prompts dá pra gerar uma cena animada coerente em minutos, é só o começo. A pessoa comum, sem treino, não vai mais conseguir distinguir vídeo gerado de vídeo real só batendo o olho. E os filtros do Seedance só barram o uso aberto e legal. Quem quer fazer mau uso vai sempre encontrar caminho com modelo open source, com workaround, com plataforma menos rígida. Esse é o mundo real agora, e quanto antes a gente assumir isso socialmente, melhor.</p>
<h2>Não, isso não substitui artista de verdade<span class="hx:absolute hx:-mt-20" id="não-isso-não-substitui-artista-de-verdade"></span>
    <a href="#n%c3%a3o-isso-n%c3%a3o-substitui-artista-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Já vou cortar o discurso de sempre antes que ele apareça nos comentários. Não, o Seedance não substitui artista de VFX, diretor, editor, nada disso. Isso aqui é martelo. A ferramenta é a mesma pra todo mundo. O que sai do outro lado depende inteiramente de quem está segurando ela.</p>
<p>Quem estudou enquadramento, ritmo, continuidade, direção de arte, edição, cor, composição, vai usar o Seedance pra acelerar 10 vezes o trabalho que já sabia entregar. Quem não estudou nada disso vai produzir 10 vezes mais rápido o mesmo meme de Instagram que já produzia. <strong>A IA reflete quem você é.</strong> Se você é bom artista, a ferramenta multiplica. Se você só acha que é bom, a ferramenta multiplica a mesmice. Não tem milagre.</p>
<p>E é por isso que essa onda de &ldquo;agora todo mundo vira diretor de cinema com IA&rdquo; é a mesma ladainha que ouvimos com câmera digital, com Photoshop, com After Effects, com Premiere e com qualquer ferramenta poderosa que apareceu nos últimos 30 anos. A barreira técnica cai, a barreira de gosto, repertório e estudo continua exatamente onde estava. Quem é bom fica mais produtivo. Quem não é continua precisando de gente de verdade pra fazer trabalho de verdade.</p>
<h2>Onde isso vai parar<span class="hx:absolute hx:-mt-20" id="onde-isso-vai-parar"></span>
    <a href="#onde-isso-vai-parar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seedance 2.0 é claramente um passo na direção certa, mas ainda não é a ferramenta que substitui produção profissional. Texto sozinho continua impreciso, vídeo de referência ajuda mas ainda escapa, e o preço por minuto útil de saída polida é alto pro que entrega. A boa notícia é que a história se repete: Google já tem o Veo, Runway está com modelo próprio e ainda revendendo Seedance, Kling e Hailuo da China continuam puxando, e os modelos open source vão ganhando tração. A OpenAI tinha o Sora, mas descontinuou. Conforme a competição aperta, qualidade sobe e preço cai. Em um ou dois anos a gente deve estar olhando pra esse post e achando primitivo o que acabei de mostrar. Como sempre.</p>
]]></content:encoded><category>ai</category><category>video</category><category>seedance</category><category>bytedance</category><category>vfx</category><category>blender</category><category>deepfake</category><category>themakitachronicles</category></item><item><title>Distrobox de Emulação com Claude Code</title><link>https://www.akitaonrails.com/2026/04/11/distrobox-de-emulacao-com-claude-code/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/11/distrobox-de-emulacao-com-claude-code/</guid><pubDate>Sat, 11 Apr 2026 18:00:00 GMT</pubDate><description>&lt;p&gt;Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em &lt;code&gt;/mnt/terachad/Emulators&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em <code>/mnt/terachad/Emulators</code>.</p>
<p>No meu canal eu inclusive usei jogos antigos pra ensinar fundamentos de computação. No <a href="https://www.youtube.com/watch?v=hYJ3dvHjeOE&amp;pp=ygUUYWtpdGFuZG8gc3VwZXIgbWFyaW8%3D"target="_blank" rel="noopener">Akitando sobre Super Mario e computadores antigos</a> eu explico 6502, mapa de memória, PPU, limitações de hardware e por que aqueles jogos eram programados daquele jeito. Se você nunca viu, recomendo assistir:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/hYJ3dvHjeOE"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>O problema é o de sempre: a cada poucos anos eu resolvo montar uma máquina nova de emulação, baixo de novo os emuladores principais da vez, e lá vou eu repetir o mesmo ritual masoquista. <code>PCSX2</code>, <code>RPCS3</code>, <code>Eden</code>, <code>Azahar</code>, <code>Dolphin</code>, <code>RetroArch</code>, <code>Flycast</code>, <code>shadPS4</code> e por aí vai. Cada um com suas manias. Cada um com sua forma particular de te fazer perder horas.</p>
<p>Em teoria, montar tudo isso deveria ser divertido. Na prática, leva dias. E não é exagero. <code>Dolphin</code> ainda consegue ser um dos piores porque controle de GameCube e Wii não tem nada de padrão. <code>RPCS3</code> exige tuning por jogo. <code>Eden</code> precisa de DLC, update e cheats no lugar certo. <code>shadPS4</code> é um festival de tentativa e erro. Quando eu finalmente terminava de configurar tudo, eu já não queria mais jogar porcaria nenhuma. Eu só queria fechar os menus e ir fazer outra coisa.</p>
<p>Eu já cheguei a dizer que talvez a graça estivesse justamente no tuning. Não acho mais isso. Depois de repetir esse processo vezes demais, eu cansei. Eu quero jogar os jogos, não preencher formulário escondido em GUI de emulador.</p>
<h2>O problema não era Linux<span class="hx:absolute hx:-mt-20" id="o-problema-não-era-linux"></span>
    <a href="#o-problema-n%c3%a3o-era-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já resolvi esse tipo de coisa de várias formas no passado. Anos atrás eu rodava Linux no host e um Windows virtualizado com GPU passthrough pra jogar dentro da VM. Na época era o que fazia sentido. Eu fiz um vídeo inteiro sobre isso:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/IDnabc3DjYY"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Mas isso foi antes do trabalho monumental da Valve, do Proton, do Wine moderno e de toda a comunidade open source que empurrou o desktop Linux pra um lugar muito melhor. Hoje dá pra evitar Windows na maior parte dos casos. No outro vídeo abaixo eu explico a evolução de CPU, GPU, OpenGL, Vulkan e por que chegamos nesse ponto:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/JEp7ozWqIps"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Meu mini-PC dedicado a games com RTX 4090 continua sendo minha principal máquina de Steam, especialmente agora que montei um <a href="/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/">cockpit decente de sim racing</a>. Nele eu uso <a href="https://www.emudeck.com/"target="_blank" rel="noopener">EmuDeck</a>, que nasceu justamente pra automatizar instalação e configuração de emuladores no Steam Deck, depois passou a suportar Windows também, e ajuda bastante a reduzir a complicação de montar esse tipo de ambiente. Mas meu desktop principal também é forte demais pra ficar subutilizado. Ele tem uma RTX 5090, e hoje eu uso essa GPU principalmente pra testar LLMs e benchmarking, como contei em <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">Testando LLMs Open Source e Comerciais</a>. Seria um desperdício não usar isso pra gaming também.</p>
<p>Só que no Linux eu queria outra coisa. Eu não queria uma caixa-preta fazendo tudo por mim sem eu saber exatamente o que estava sendo alterado, principalmente no meu PC principal, que é minha máquina de trabalho. Eu queria um setup meu, feito &ldquo;na mão&rdquo; no sentido certo da expressão: não clicar GUI por GUI, mas entender o que está sendo configurado, manter os arquivos sob meu controle, e conseguir reconstruir tudo do jeito que eu gosto. Eu sei que o pessoal do NixOS vai pular aqui pra dizer &ldquo;é só usar Nix&rdquo; — explico mais abaixo por que descartei essa rota. Também não queria transformar a minha máquina de trabalho num carnaval de pacote de emulador, tema GTK, wrapper, launcher, runtime esquisito e config enterrada em <code>~/.config</code>. Nem queria dual boot ou VM. Em 2026, a melhor resposta pra isso, na minha opinião, é <a href="https://distrobox.it/"target="_blank" rel="noopener">Distrobox</a>.</p>
<p>Pelo que o próprio projeto explica, Distrobox é um wrapper em cima de <code>podman</code>, <code>docker</code> ou <code>lilipod</code> pra criar containers fortemente integrados ao host, com acesso a <code>HOME</code>, Wayland/X11, áudio, dispositivos USB, storage externo e GPU. Ou seja: exatamente o tipo de isolamento pragmático que eu queria. Não é uma fronteira de segurança, e o próprio site deixa isso claro. Não use pensando em sandbox de alta segurança. Use pensando em separar ambientes sem ficar pagando o preço de uma VM.</p>
<p>E antes que alguém repita a confusão de sempre: container não é máquina virtual. Eu explico isso com calma neste outro episódio:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/85k8se4Zo70"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>O setup<span class="hx:absolute hx:-mt-20" id="o-setup"></span>
    <a href="#o-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A ideia é simples: um Arch Linux vanilla dentro de um Distrobox chamado <code>gaming</code>, com os caminhos do NAS montados em read-only, a biblioteca Steam acessível onde faz sentido, e todos os emuladores instalados lá dentro. Como o container tem acesso a GPU NVIDIA, áudio, USB e demais periféricos, eu separo trabalho de jogo na mesma máquina sem penalidade prática de performance.</p>
<p>O bootstrap inicial é mais ou menos isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox create --name gaming --image archlinux:latest --nvidia <span class="se">\
</span></span></span><span class="line"><span class="cl">  --home /mnt/data/distrobox/gaming <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/data/steam:/mnt/data/steam <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/terachad/Emulators:/mnt/terachad/Emulators:ro</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Só esse começo já tem pegadinha. O <code>archlinux:latest</code> dentro de Distrobox vem sem <code>[multilib]</code> no <code>pacman.conf</code>, com um <code>sudoers</code> mal configurado, e com o velho problema do <code>--nvidia</code>: as libs do driver do host entram bind-mounted read-only, então qualquer instalação que dependa de <code>nvidia-utils</code> quebra com conflito de arquivo. Só isso já é o tipo de dor de cabeça que, alguns anos atrás, me faria abrir meia dúzia de abas, mais uma penca de terminais, e gastar uma noite inteira em tentativa e erro.</p>
<p>Desta vez eu fiz diferente.</p>
<h2>Claude Code como assistente de infraestrutura<span class="hx:absolute hx:-mt-20" id="claude-code-como-assistente-de-infraestrutura"></span>
    <a href="#claude-code-como-assistente-de-infraestrutura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu venho ficando cada vez mais confortável em usar Claude Code como meu assistente de infraestrutura nas minhas máquinas pessoais. Eu não faria isso cegamente em servidor de cliente. Mas em máquina minha, em ambiente que eu posso destruir e reconstruir quantas vezes eu quiser, faz todo sentido. Escrevi sobre isso no artigo <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Migrando meu Home Server com Claude Code</a>, quando usei Claude pra instalar e configurar openSUSE MicroOS, Docker, NFS, firewall, hardening e o resto todo sem eu precisar ficar lembrando comando chato de shell.</p>
<p>Então pensei: por que não fazer a mesma coisa aqui?</p>
<p>Foi exatamente o que eu fiz. Comecei com prompts bem objetivos, sempre pedindo duas coisas ao mesmo tempo: fazer o trabalho e documentar o suficiente pra eu poder reconstruir tudo depois. O histórico mais importante ficou em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-prompts.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-prompts.md</code></a>.</p>
<p>Vale um esclarecimento: aqueles prompts no GitHub não são as minhas mensagens cruas, do jeito que saíram na hora. Depois que tudo funcionou, eu pedi pro próprio Claude reescrever os prompts de forma organizada e detalhada, só pra fins de documentação. Os prompts originais eram bem mais simples e bem menos específicos. Eu descrevia o objetivo e deixava o Claude descobrir sozinho paths, arquivos de configuração, formatos e comandos necessários.</p>
<p>O primeiro prompt criou a box com <code>--nvidia</code>, <code>--home</code> separado e os mounts corretos. O segundo resolveu os três problemas clássicos do Arch dentro de Distrobox: <code>sudo</code>, <code>multilib</code> e o dummy package de <code>nvidia-utils</code>. O terceiro instalou a base de gaming, inclusive um detalhe que eu teria esquecido fácil se estivesse fazendo na mão: <code>pipewire-pulse</code>, necessário pra vários emuladores não ficarem mudos nem perderem sincronia.</p>
<p>O ponto não é o texto em si. O ponto é o método. Eu não preciso lembrar a ordem exata, nem as opções precisas, nem onde está a documentação de cada detalhe. Eu descrevo o objetivo e deixo o agente carregar o piano. Fico no papel de tech lead: observando, revisando, corrigindo direção quando precisa, e mandando continuar.</p>
<h2>O projeto no GitHub<span class="hx:absolute hx:-mt-20" id="o-projeto-no-github"></span>
    <a href="#o-projeto-no-github" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup vive em <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">akitaonrails/distrobox-gaming</a>, estruturado como um projeto Ansible. 17 roles cobrem desde criação da box, bootstrap de pacotes, seed de configs, instalação de DLC, configuração de controle, até verificação pós-setup. Os playbooks principais são:</p>
<ul>
<li><code>site.yml</code>: setup completo do zero</li>
<li><code>reset-configs.yml</code>: reset só das configs (útil quando você quer zerar tuning e redeployar)</li>
<li><code>backup.yml</code> / <code>restore.yml</code>: snapshot e restauração do container inteiro via <code>podman commit</code></li>
<li><code>refresh-shadps4.yml</code>: atualização standalone do shadPS4</li>
<li><code>install-xenia.yml</code>: instalação opcional do Xenia Manager (Wine prefix)</li>
</ul>
<p>O passo a passo completo está em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a>. As decisões de pacote (por que AUR em vez de Flatpak, por exemplo) estão em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-packages.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-packages.md</code></a>.</p>
<h3>Por que não Nix?<span class="hx:absolute hx:-mt-20" id="por-que-não-nix"></span>
    <a href="#por-que-n%c3%a3o-nix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alguém vai perguntar. &ldquo;Você tá recriando ambiente reproduzível. Por que não usar NixOS, ou pelo menos <code>nix-shell</code>/flakes?&rdquo;</p>
<p>Eu considerei e descartei. O ecossistema de emuladores que eu preciso vive no AUR. Pacotes <code>-bin</code> do AUR são wrappers de AppImage que empacotam o binário oficial do upstream sem compilar nada. <code>rpcs3-bin</code>, <code>pcsx2-latest-bin</code>, <code>eden-bin</code>, <code>duckstation-qt-bin</code>, <code>shadps4-bin</code> — esses binários acompanham o upstream praticamente no dia do lançamento. No Nixpkgs, muitos desses emuladores ficam semanas ou meses atrás do release, e alguns nem existem. Criar um overlay Nix pra cada emulador que eu preciso, manter, e lidar com os patches de compatibilidade quando upstream muda, é trabalho que eu não quero ter.</p>
<p>Tem também o modelo mental. Eu não quero aprender a linguagem Nix, o sistema de derivações e o modelo de avaliação pra montar um ambiente de games pessoal. Ansible eu já conheço, as roles são pastas com tasks em YAML, e o debug é <code>ansible-playbook -vvv</code>. Quando dá errado, eu leio a mensagem de erro, abro o task que falhou, e sei exatamente onde mexer. Se eu precisasse do mesmo nível de reprodutibilidade em escala, num cluster, com rollback atômico, aí sim Nix teria argumento. Pra um distrobox com emuladores na minha máquina pessoal, Ansible resolve com muito menos atrito.</p>
<p>Flatpak também foi testado e descartado. O <code>bwrap</code> (bubblewrap) do Flatpak não aninha direito dentro das mount namespaces do Docker, então apps Flatpak simplesmente não rodam dentro de distrobox. A alternativa seria instalar Flatpak no host, mas aí você volta ao problema de poluir a máquina de trabalho com pacotes de gaming. Além disso, as versões Flatpak de emuladores ficam bem atrás do AUR — o <code>pcsx2-latest-bin</code> do AUR tracka o AppImage 2.7.x, enquanto o Flathub ainda distribui a v2.6.3.</p>
<h2>Plataformas cobertas<span class="hx:absolute hx:-mt-20" id="plataformas-cobertas"></span>
    <a href="#plataformas-cobertas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup cobre 12 plataformas hoje:</p>
<table>
  <thead>
      <tr>
          <th>Plataforma</th>
          <th>Emulador</th>
          <th>Renderer</th>
          <th>Destaque</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PS1</td>
          <td>DuckStation</td>
          <td>Vulkan, 8x upscale</td>
          <td>JINC2 filter, PGXP, widescreen hack</td>
      </tr>
      <tr>
          <td>PS2</td>
          <td>PCSX2 2.7.x</td>
          <td>Vulkan, 4x SSAA</td>
          <td>FXAA + PCRTC antiblur, Xbox bindings</td>
      </tr>
      <tr>
          <td>PS3</td>
          <td>RPCS3</td>
          <td>Vulkan</td>
          <td>Per-game configs curados via API</td>
      </tr>
      <tr>
          <td>PS4</td>
          <td>shadPS4</td>
          <td>Vulkan</td>
          <td>Driveclub em foco (experimental)</td>
      </tr>
      <tr>
          <td>PS Vita</td>
          <td>Vita3K</td>
          <td>—</td>
          <td>Seeding padrão</td>
      </tr>
      <tr>
          <td>GameCube</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Perfis 8BitDo Ultimate 2</td>
      </tr>
      <tr>
          <td>Wii</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Classic Controller + Nunchuk</td>
      </tr>
      <tr>
          <td>Wii U</td>
          <td>Cemu</td>
          <td>Vulkan</td>
          <td>Baseline seed only-if-missing</td>
      </tr>
      <tr>
          <td>Switch</td>
          <td>Eden</td>
          <td>Vulkan</td>
          <td>DLC + cheats Atmosphere</td>
      </tr>
      <tr>
          <td>Dreamcast</td>
          <td>Flycast</td>
          <td>Vulkan, 2880p</td>
          <td>Wrapper hires, widescreen</td>
      </tr>
      <tr>
          <td>OG Xbox</td>
          <td>xemu</td>
          <td>—</td>
          <td>BIOS/HDD via symlink</td>
      </tr>
      <tr>
          <td>Xbox 360</td>
          <td>Xenia (Wine)</td>
          <td>Vulkan</td>
          <td>Project Forza Plus, PGR3/4</td>
      </tr>
  </tbody>
</table>
<p>RetroArch entra por cima pra cobrir o resto (NES, SNES, Genesis, N64, arcade, etc.), com 21 cores do buildbot e 8 asset packs (databases, shaders, cheats, overlays, autoconfig).</p>
<p>Frontend é <a href="https://es-de.org/"target="_blank" rel="noopener">ES-DE</a>. As definições dos sistemas customizados ficam em <code>ansible/group_vars/all/esde.yml</code> e cada emulador entra com um wrapper que já sobe com as variáveis certas de GPU (voltarei nisso).</p>
<h2>GPU hardforce pra NVIDIA<span class="hx:absolute hx:-mt-20" id="gpu-hardforce-pra-nvidia"></span>
    <a href="#gpu-hardforce-pra-nvidia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Num sistema híbrido com NVIDIA dGPU + AMD iGPU (meu caso no desktop), Vulkan às vezes escolhe o iGPU sozinho e o resultado é emulador rodando a 15 fps enquanto a 5090 fica parada. A solução é forçar o ICD Vulkan da NVIDIA em todos os launchers.</p>
<p>Cada entrada de desktop rendereizada pelo Ansible exporta <code>VK_ICD_FILENAMES</code> apontando pro ICD da NVIDIA, e o wrapper do ES-DE faz a mesma coisa antes de lançar qualquer core. Em combinação com a flag <code>--nvidia</code> na criação da box, que bind-monta os drivers do host em read-only, o resultado é previsível: Vulkan sempre na dGPU NVIDIA, independente do estado do sistema na hora.</p>
<p>Isso vale principalmente pra quem tem setup híbrido. Num desktop com só uma GPU, o ICD filtering é placebo, mas também não atrapalha.</p>
<h2>PS2: PCSX2 com tuning por jogo<span class="hx:absolute hx:-mt-20" id="ps2-pcsx2-com-tuning-por-jogo"></span>
    <a href="#ps2-pcsx2-com-tuning-por-jogo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A linha Gran Turismo é meu vício declarado. Cada versão precisa de tweak específico. O setup do PCSX2 hoje tem configs globais razoáveis (Vulkan, 4x SSAA, widescreen 16:9, Xbox-style bindings, FXAA + PCRTC antiblur, 16x anisotropic, deinterlace Automatic) e per-game INIs pros títulos que eu realmente jogo:</p>
<ul>
<li><strong>Gran Turismo 3 A-spec</strong> (SCUS-97102 + bundle PBPX-95503): widescreen pnach, pack de retextura opcional (desabilitado por default porque NFS causa flicker em cutscenes de showcase de carro)</li>
<li><strong>Gran Turismo 4 USA</strong> (SCUS-97328): HD HUD do Silentwarior112, pack completo de retextura, patches do Silent pra gatilho e câmera, widescreen pnach</li>
<li><strong>Gran Turismo 4 Spec II</strong> (SCUS-97436, CRC <code>4CE521F2</code>): caso especial — os HD packs de GT4 vanilla são linkados via symlink porque o Spec II tem mesma estrutura mas CRC diferente. Widescreen pnach renomeado pra bater com o CRC. Deinterlace mode 8 (Adaptive TFF), ShadeBoost tuning (+10 saturação, +3 brilho, +2 contraste) pra corrigir ghosting de pause-interlace e saturação anêmica do upscale</li>
<li><strong>Enthusia Professional Racing</strong> (SLUS-20967): HD textures + widescreen</li>
<li><strong>Ridge Racer V</strong> (SLUS-20002): widescreen + no-interlace pnach</li>
</ul>
<p>Os pnach files do Silent e da comunidade são baixados automaticamente pelo role <code>pcsx2_textures</code>, que também faz o deploy do pack de cheats do NAS e instala os HD packs do Spec II via symlink. Widescreen é ligado via <code>gamesettings</code> por CRC (PCSX2 identifica jogo por CRC antes de aplicar o pnach).</p>
<p>Sobre versão do PCSX2: o <code>pcsx2-latest-bin</code> do AUR tracka a linha 2.7.x, que é onde todas as features modernas estão (texture replacement, FXAA, PCRTC antiblur). O <code>pcsx2</code> padrão dos repos oficiais do Arch ainda está na 2.6.3. A diferença é mais de 250 commits de correção e feature novo.</p>
<h2>PS3: RPCS3 com per-game configs curados<span class="hx:absolute hx:-mt-20" id="ps3-rpcs3-com-per-game-configs-curados"></span>
    <a href="#ps3-rpcs3-com-per-game-configs-curados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RPCS3 tem um detalhe chato: configs por jogo ficam em <code>~/.config/rpcs3/custom_configs/config_&lt;SERIAL&gt;.yml</code>. O formato do arquivo tem versão, e se você gerar um YAML manualmente com versão errada, o RPCS3 rejeita silenciosamente e volta pro default. Isso custou tempo pra descobrir.</p>
<p>A role <code>rpcs3_per_game_configs</code> resolve isso assim:</p>
<ol>
<li>Query o <a href="https://rpcs3.net/compatibility"target="_blank" rel="noopener">compatibility DB da comunidade RPCS3</a> pra pegar os presets recomendados por título</li>
<li>Aplica overrides curados por cima pra títulos que eu sei que precisam de ajuste específico (tipicamente GT5 e GT6)</li>
<li>Salva o YAML com a filename convention exata (<code>config_&lt;SERIAL&gt;.yml</code>, prefixo <code>config_</code> obrigatório)</li>
</ol>
<p>Na linha Gran Turismo:</p>
<ul>
<li><strong>GT5</strong> (BCUS98114, BCES00569): Resolution Scale 300%, Shader Precision Ultra, Force High Precision Z, SPU XFloat Accurate, Multithreaded RSX. Combinação que mata o dithering típico do RSX sem quebrar a GT series safety config (WCB/RCB off).</li>
<li><strong>GT6</strong> (BCES01893 + variantes regionais): caso especial, <strong>versão travada em v1.05</strong>. Patches 1.06+ regridem com superfícies pretas em carros no menu garagem. O script <code>extract_ps3_dlc.py</code> tem per-title PATCH ceiling justamente pra pinar o GT6 no 1.05 mesmo quando o PSN cuspe patches mais novos. Além disso, Force CPU Blit ligado é mandatório (sem ele, flicker de tela inteira). O trade-off é que o retrovisor fica permanentemente preto — ligar WCB restauraria o retrovisor mas downgradaria pra 720p nativo. Prefiro perder o retrovisor.</li>
</ul>
<p>Mais detalhes desses dois em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt5-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt5-rpcs3.md</code></a> e <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt6-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt6-rpcs3.md</code></a>.</p>
<h3>Update checker de PS3<span class="hx:absolute hx:-mt-20" id="update-checker-de-ps3"></span>
    <a href="#update-checker-de-ps3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra não ter que abrir a GUI do RPCS3 e ficar clicando em <code>Download Game Updates</code> jogo por jogo, criei um script em Python: <code>check_ps3_updates.py</code>. Ele percorre os jogos instalados em <code>dev_hdd0/game</code>, parseia o <code>PARAM.SFO</code> pra pegar a versão atual de cada título, consulta o servidor de updates da PSN (<code>a0.ww.np.dl.playstation.net</code>) e compara com o cache local de patches. <code>--list</code> mostra o diff; <code>--download</code> baixa o que falta. <code>--max-version</code> respeita os teto por título (ex: 1.05 pro GT6).</p>
<p>O primeiro scan na minha biblioteca achou 51 de 72 jogos com update disponível, 69 patches faltando localmente, uns 24 GB no total. Só o GT6 sozinho tinha 16 patches faltando, da 1.07 até 1.22, que eu descartei pelos motivos acima.</p>
<p>Os patches chegam como pacotes PSN tipo 0x0001 sem criptografia, então o próprio <code>extract_ps3_dlc.py</code> consegue instalar depois. A chamada padrão:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/extract_ps3_dlc.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;/mnt/terachad/Emulators/EmuDeck/roms_heavy/ps3-DLC&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest <span class="s2">&#34;/mnt/data/distrobox/gaming/.config/rpcs3/dev_hdd0/game&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Acompanhar versão de DLC de PS3 manualmente, jogo por jogo no RPCS3, é inviável. Vira lista de tarefas eterna e você só descobre que tinha patch 1.22 quando o jogo crasha.</p>
<h2>PS1: DuckStation com per-game<span class="hx:absolute hx:-mt-20" id="ps1-duckstation-com-per-game"></span>
    <a href="#ps1-duckstation-com-per-game" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A trilogia Gran Turismo começa no PS1, e a qualidade ali só sobe se você ligar as coisas certas:</p>
<ul>
<li><strong>Gran Turismo</strong> (SCUS-94949): built-in WidescreenHack do DuckStation (não existe cheat pra esse jogo), PGXP ligado</li>
<li><strong>Gran Turismo 2 Simulation</strong> (SCUS-94455): widescreen cheat + 8 MB RAM cheat (fix de áudio em track crowded), filter JINC2</li>
<li><strong>Gran Turismo 2 Arcade</strong> (SCUS-94488): mesmo tratamento do Simulation</li>
</ul>
<p>Global defaults: Vulkan, 8x upscale, JINC2 como filter default.</p>
<p>Os cheats pro GT2 saem de um repositório de widescreen fixes e são ligados automaticamente na pasta de cheats do DuckStation.</p>
<h2>Switch: Eden com DLC, update e cheats<span class="hx:absolute hx:-mt-20" id="switch-eden-com-dlc-update-e-cheats"></span>
    <a href="#switch-eden-com-dlc-update-e-cheats" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Switch, <code>eden-bin</code> é o emulador. A integração automatiza três coisas que antes eu fazia na mão:</p>
<ol>
<li>
<p><strong>Install de DLC e update</strong>: o role <code>install_dlcs</code> aponta pro diretório de dumps no NAS, identifica os arquivos <code>.nsp</code>/<code>.nsz</code>/<code>.xci</code>/<code>.xcz</code>, e instala tudo em <code>~/.local/share/eden/nand/user/Contents/registered/</code>. Se o dump está bagunçado (patches e DLC misturados em flat folder), o script <code>reorganize_switch_nsps.py</code> sorteia tudo por Title ID antes.</p>
</li>
<li>
<p><strong>Cheats Atmosphere</strong>: os cheats em formato Atmosphere vão symlinkados pro load path do Eden (<code>~/.local/share/eden/load/&lt;TITLE_ID&gt;/cheats/</code>). O role <code>switch_cheats</code> faz isso por todos os títulos cobertos pelo pack do NAS.</p>
</li>
<li>
<p><strong>Update checker</strong>: <code>check_switch_updates.py</code> lista títulos com update disponível vs o que tá instalado localmente. Útil pra não perder patch importante de jogo que sai atualização ainda.</p>
</li>
</ol>
<p>Atenção ao detalhe que me queimou: <code>QT_STYLE_OVERRIDE</code> precisa ser desarmado antes de lançar o <code>eden-bin</code>, senão ele conflita com Kvantum e quebra a UI. O wrapper já faz o <code>unset</code> antes do <code>exec</code>.</p>
<h2>Xbox 360: Xenia Manager via Wine<span class="hx:absolute hx:-mt-20" id="xbox-360-xenia-manager-via-wine"></span>
    <a href="#xbox-360-xenia-manager-via-wine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Xbox 360 ainda é território áspero no Linux. Não tem emulador nativo decente. A melhor rota hoje é <a href="https://xenia.jp/"target="_blank" rel="noopener">Xenia</a> rodando via Wine, gerenciado pelo <a href="https://github.com/xenia-manager/xenia-manager"target="_blank" rel="noopener">Xenia Manager</a>.</p>
<p>Isso é opt-in no meu setup. O playbook <code>install-xenia.yml</code> cria um Wine prefix dedicado, baixa o Xenia Manager, e registra os launchers. Não faz parte do <code>site.yml</code> principal porque nem todo mundo quer Wine no meio do caminho.</p>
<h3>Project Forza Plus (Forza Motorsport 2/3/4)<span class="hx:absolute hx:-mt-20" id="project-forza-plus-forza-motorsport-234"></span>
    <a href="#project-forza-plus-forza-motorsport-234" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Forza antigo ainda é o rei do simcade de geração passada. Rodar FM2/3/4 no Xenia requer mods da comunidade Project Forza Plus: patches de compatibilidade, mods de performance, instalação de title updates específicos. Documentei o processo em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/project-forza.md"target="_blank" rel="noopener"><code>docs/project-forza.md</code></a>.</p>
<h3>Project Gotham Racing 3 e 4<span class="hx:absolute hx:-mt-20" id="project-gotham-racing-3-e-4"></span>
    <a href="#project-gotham-racing-3-e-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>PGR3 roda razoavelmente bem no Xenia Canary. PGR4 precisa de ajuste específico: <code>render_target_path_vulkan = &quot;fsi&quot;</code> no config, senão algumas races quebram com artefato visual. Documentei o setup em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-pgr.md"target="_blank" rel="noopener"><code>docs/xbox360-pgr.md</code></a>. Áudio do PGR4 no NVIDIA ainda tem problema de XMA decoding que gera garbage intermitente. Tem issue aberta no xenia-canary, sem resolução up até agora.</p>
<h3>Title Updates em batch<span class="hx:absolute hx:-mt-20" id="title-updates-em-batch"></span>
    <a href="#title-updates-em-batch" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Title Update pra jogo de Xbox 360 era distribuído pela Xbox Live, que já foi desligada. A alternativa é o archive.org, que tem o catálogo completo preservado. Escrevi <code>scripts/download-xbox360-tus.py</code> pra automatizar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/download-xbox360-tus.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  --src /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360-updates <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dry-run</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O script precisa do CLI <code>internetarchive</code> autenticado (<code>ia configure</code> uma vez, guarda o token). Ele scaneia a pasta de jogos, faz match contra o manifest do archive.org (cacheado localmente), prioriza por região (USA &gt; World &gt; Europe &gt; Japan), e baixa via <code>ia download --checksum</code> com retry automático. Os .zip resultantes vão pro Xenia Manager via <code>Manage → Install Content</code>, que extrai pro diretório correto (<code>000B0000/</code>).</p>
<p>O setup completo tá em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-title-updates.md"target="_blank" rel="noopener"><code>docs/xbox360-title-updates.md</code></a>.</p>
<h2>Steam no distrobox<span class="hx:absolute hx:-mt-20" id="steam-no-distrobox"></span>
    <a href="#steam-no-distrobox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde que o Proton virou cidadão de primeira no Linux, faz sentido ter Steam também no gaming box. Adicionei o pacote <code>steam</code> no Ansible (que puxa multilib e dependências 32-bit), montei <code>/mnt/data/steam</code> em read-write pra a biblioteca persistir fora do container, e criei launcher no host.</p>
<p>Na prática, isso me permite rodar jogos Steam via Proton lado a lado com emulação, sem precisar alternar ambiente. O mini-PC com RTX 4090 continua sendo a máquina principal de Steam no cockpit de sim racing. O desktop é fallback e ambiente de teste.</p>
<h2>Shell e controle por controle<span class="hx:absolute hx:-mt-20" id="shell-e-controle-por-controle"></span>
    <a href="#shell-e-controle-por-controle" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Shell mínimo dentro da box<span class="hx:absolute hx:-mt-20" id="shell-mínimo-dentro-da-box"></span>
    <a href="#shell-m%c3%adnimo-dentro-da-box" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando eu entro no gaming box via <code>distrobox enter gaming</code>, eu quero prompt decente mesmo pra troubleshooting rápido. Instalei <code>zsh</code> + <code>starship</code> com config mínima, zero plugins exóticos. Não é meu setup de dev full, é só &ldquo;prompt que mostra path e git branch sem me dar vergonha&rdquo;.</p>
<h3>Dolphin e o inferno do controle moderno<span class="hx:absolute hx:-mt-20" id="dolphin-e-o-inferno-do-controle-moderno"></span>
    <a href="#dolphin-e-o-inferno-do-controle-moderno" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>Dolphin</code> sempre foi o rei do atrito manual. Se você usa um controle mais moderno, como meu 8BitDo Ultimate 2, precisa lembrar como eu gosto do mapeamento de GameCube, como adapto o Wii Remote, quando usar perfil de Nunchuk, quando trocar pro Classic Controller. Eu não tenho a menor paciência pra reconstruir isso na mão toda vez. Os perfis ficam prontos em <code>config/emulator-overrides/dolphin/Profiles/</code> e são copiados pelo role <code>seed_configs</code>. Detalhes em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/controller-hotkeys.md"target="_blank" rel="noopener"><code>docs/controller-hotkeys.md</code></a>.</p>
<h2>Alguns tropeços que sobraram<span class="hx:absolute hx:-mt-20" id="alguns-tropeços-que-sobraram"></span>
    <a href="#alguns-trope%c3%a7os-que-sobraram" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem tudo virou magia. Emulação sempre tem casca de banana.</p>
<p><code>Flycast</code>, por exemplo, tem uma armadilha irritante. O arquivo <code>emu.cfg</code> usa chaves <code>rend.*</code> dentro da seção <code>[config]</code>. Se você cria uma seção <code>[rend]</code> separada, parece certo, mas o emulador simplesmente ignora e depois reescreve tudo com default medíocre. A correção virou wrapper dedicado em <code>$DG_BOX_HOME/bin/flycast-hires</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">$DG_BOX_HOME</span>/bin/flycast-hires <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:pvr.rend<span class="o">=</span><span class="m">4</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.Resolution<span class="o">=</span><span class="m">2880</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.EmulateFramebuffer<span class="o">=</span>no <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.WideScreen<span class="o">=</span>yes</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhes em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/flycast-resolution.md"target="_blank" rel="noopener"><code>docs/flycast-resolution.md</code></a>.</p>
<p><code>shadPS4</code> ainda está longe de ser um caso encerrado. O setup atual é focado em <code>Driveclub</code>, com config espelhada, patch XML e links dos <code>sys_modules</code> do firmware 11.00. E isso não foi por acaso: eu gosto muito de <code>Driveclub</code>. Aliás, é praticamente o único jogo ainda preso no PS4 que eu realmente queria conseguir rodar direito. Eu já vi vários vídeos no YouTube mostrando o jogo funcionando, mas até agora eu mesmo não consegui fazer isso rodar de forma satisfatória no Linux. Documentado em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a>. A parte boa é que agora eu não dependo mais de memória muscular pra lembrar como lançar, com qual patch, em que pasta, com quais módulos. A parte ruim é que emulação de PS4 ainda é emulação de PS4. Não existe script que faça upstream amadurecer mais rápido. Se alguém souber como fechar essa configuração no Linux, olhe o projeto no GitHub e mande um PR.</p>
<p>Ridge Racer V tem um flickering de textura de carro documentado nas issues do PCSX2 (#3639, #13729). O renderer Hardware é aceitável pra jogo de corrida onde você não fica admirando parado o modelo. Software renderer resolve mas mata performance. Escolhi aceitar o flicker.</p>
<h2>O ganho real não é só automação<span class="hx:absolute hx:-mt-20" id="o-ganho-real-não-é-só-automação"></span>
    <a href="#o-ganho-real-n%c3%a3o-%c3%a9-s%c3%b3-automa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seria fácil resumir isso como &ldquo;olha que legal, usei IA pra automatizar shell script&rdquo;. Não é isso.</p>
<p>O ganho real é mais chato e mais importante: eu parei de gastar energia mental com trabalho braçal. Não precisei mais lembrar comando raro. Não precisei manter dezenas de abas com wiki, issue, fórum e README. Não precisei deixar meia dúzia de terminais espalhados dando <code>tail</code> em log enquanto tento lembrar que opção escondida de GUI o emulador insiste em regravar no primeiro boot. Eu passei a trabalhar em conversa.</p>
<p>Eu digo pro agente o objetivo. Ele procura os arquivos, lê os logs, encontra o formato certo do config, compara defaults, propõe correções, escreve script, valida paths, verifica UID/GID, confere symlink quebrado, gera wrapper, exporta launcher. Eu continuo responsável pelas decisões, claro. Mas parei de ser digitador de comando raro.</p>
<p>Esse é o ponto que mais me interessa no uso de coding agents em Linux. Eles reduzem dramaticamente a fricção de entrada. Muita gente desiste do desktop Linux não porque o sistema seja incapaz, mas porque a curva de tuning historicamente foi irritante demais. Ter um assistente capaz de ler docs, cruzar config, propor automação e executar com supervisão muda esse jogo.</p>
<h2>Se você quiser reproduzir<span class="hx:absolute hx:-mt-20" id="se-você-quiser-reproduzir"></span>
    <a href="#se-voc%c3%aa-quiser-reproduzir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O repo foi publicado pra isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/distrobox-gaming.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> distrobox-gaming/ansible
</span></span><span class="line"><span class="cl">ansible-galaxy collection install -r collections/requirements.yml
</span></span><span class="line"><span class="cl">cp host_vars/localhost.yml.example host_vars/localhost.yml
</span></span><span class="line"><span class="cl"><span class="nv">$EDITOR</span> host_vars/localhost.yml  <span class="c1"># ajuste paths da sua máquina</span>
</span></span><span class="line"><span class="cl">ansible-playbook site.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Xenia é opt-in:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ansible-playbook install-xenia.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Leia o <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/README.md"target="_blank" rel="noopener"><code>README.md</code></a> e o <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a> antes. Eu não distribuo ROM, BIOS, firmware nem chaves. O repo só detecta, linka e configura o que você já tem na sua própria máquina.</p>
<h2>Se você preferir a rota com Claude Code<span class="hx:absolute hx:-mt-20" id="se-você-preferir-a-rota-com-claude-code"></span>
    <a href="#se-voc%c3%aa-preferir-a-rota-com-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Minha recomendação é simples:</p>
<ol>
<li>Comece pelo objetivo, não pelo comando. &ldquo;Quero um distrobox Arch com GPU NVIDIA, home separado, ROMs read-only e Steam rw&rdquo; é melhor do que despejar meia linha de shell sem contexto.</li>
<li>Peça sempre pra documentar o que está fazendo. Se der certo, promova o resultado a script ou role. Se não documentar, você acabou de fabricar uma gambiarra descartável.</li>
<li>Trabalhe em fases. Criação da box. Bootstrap. Config de emuladores. Export de launchers. Verificação. Foi exatamente assim que eu quebrei o problema.</li>
<li>Peça verificações objetivas. <code>command -v</code>, existência de arquivos, symlinks quebrados, UID/GID, path de ROM, áudio, GPU. A melhor automação é a que falha cedo.</li>
<li>Não use agente como papagaio de comando. Use como assistente técnico. Você continua revisando as decisões e mandando ajustar o rumo.</li>
</ol>
<p>Pra mim, esse foi o ganho. Eu saí do zero pra uma máquina de emulação muito mais completa sem precisar customizar tudo na unha, em GUI, uma janela por vez. E, dessa vez, terminei com energia pra fazer o que eu queria desde o começo.</p>
<p>Jogar.</p>
]]></content:encoded><category>gaming</category><category>emulation</category><category>linux</category><category>distrobox</category><category>ansible</category><category>claude-code</category><category>AI</category></item><item><title>VS Code é o novo Cartão Perfurado</title><link>https://www.akitaonrails.com/2026/04/11/vs-code-e-o-novo-cartao-perfurado/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/11/vs-code-e-o-novo-cartao-perfurado/</guid><pubDate>Sat, 11 Apr 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;Toda vez que alguém pergunta se os júniors vão deixar de aprender a programar porque LLMs escrevem código, eu tenho a mesma reação: vocês estão fazendo a pergunta errada.&lt;/p&gt;
&lt;p&gt;Vocês estão confundindo &lt;strong&gt;input de código&lt;/strong&gt; com &lt;strong&gt;engenharia de software&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Não é a mesma coisa. Nunca foi.&lt;/p&gt;
&lt;p&gt;Teve uma época em que programar significava saber converter números pra binário de cor e enfiar instrução direto em endereço de memória, bit por bit, na mão. Teve uma época em que programar significava saber organizar baralho de cartão perfurado, saber que ordem do deck estava certa, saber onde um furo errado destruiu a execução, e saber debugar visualmente sem a fantasia moderna de backspace infinito. Teve uma época em que programar de verdade significava saber 6502, Z80 e Assembly porque os computadores tinham tão pouco recurso que cada byte importava mesmo, não como figura de linguagem.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Toda vez que alguém pergunta se os júniors vão deixar de aprender a programar porque LLMs escrevem código, eu tenho a mesma reação: vocês estão fazendo a pergunta errada.</p>
<p>Vocês estão confundindo <strong>input de código</strong> com <strong>engenharia de software</strong>.</p>
<p>Não é a mesma coisa. Nunca foi.</p>
<p>Teve uma época em que programar significava saber converter números pra binário de cor e enfiar instrução direto em endereço de memória, bit por bit, na mão. Teve uma época em que programar significava saber organizar baralho de cartão perfurado, saber que ordem do deck estava certa, saber onde um furo errado destruiu a execução, e saber debugar visualmente sem a fantasia moderna de backspace infinito. Teve uma época em que programar de verdade significava saber 6502, Z80 e Assembly porque os computadores tinham tão pouco recurso que cada byte importava mesmo, não como figura de linguagem.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/altair-8800-computer.jpg" alt="Altair 8800, símbolo da era em que programar ainda passava por painéis físicos e entrada manual de instruções"  loading="lazy" /></p>
<p><em>Painel frontal e switches: antes de editor, antes de IDE, antes de terminal confortável.</em></p>
<p>E olha, tem fase da computação que foi ainda pior que cartão perfurado. Esse vídeo abaixo mostra alguém programando num LGP-21, um dos computadores pessoais mais antigos do mundo (o segundo mais velho, depois do Bendix G15 dos anos 50). Começa a partir do minuto 5:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/TJjRCCetyo4?start=300"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Imagina o que isso era: você digitava o programa em binário direto numa máquina de escrever, olhando pro acumulador, instrução por instrução, depois virava uma alavanca fisicamente pra executar, e o resultado era datilografado de volta em papel. A métrica não era framerate. Era caractere por minuto. Uma operação que hoje você faz num piscar de olho dentro de qualquer aplicativo, ali demorava horas de trabalho humano, digitando bit a bit, conferindo, testando, conferindo de novo.</p>
<p>É a mesma coisa que tá acontecendo com digitar código em editor de texto hoje versus agente de IA rodando no seu lugar. O jeito manual continua funcionando, do mesmo jeito que o painel do Altair continuou funcionando por anos depois que compilador ficou comum. Mas a ferramenta de escolha virou outra, e a diferença de velocidade e esforço é exatamente a mesma ordem de magnitude que separava o LGP-21 do editor de texto moderno. A nossa geração tá vendo essa transição acontecer em câmera lenta, e muita gente não quer ver.</p>
<p>Depois vieram compiladores melhores. Veio C. Vieram máquinas mais gordas, consoles 32/64 bits, PCs mais decentes, e Assembly deixou de ser o centro de tudo pra virar ferramenta de baixo nível, otimização localizada, rotina crítica, inicialização, driver, essas coisas. Ninguém sério olhou pra essa transição e falou: &ldquo;pronto, agora acabou a programação porque o compilador escreve as instruções de máquina pra você.&rdquo;</p>
<p>No século 21 veio a Web e empurrou uma geração inteira pra HTML, CSS e um monte de burocracia de markup que agrega pouco valor intelectual e exige muito trabalho braçal pra ficar minimamente certo. Eu continuo achando que a indústria estendeu demais a vida útil desse modelo. Por anos demais, programador virou operador de formulário glorificado, montador de CRUD, alinhador de <code>div</code>, sacerdote de framework de front-end que faz a mesma coisa com sintaxe diferente.</p>
<p>E aí a bolha dos anos 2010 piorou tudo.</p>
<p>Eu já escrevi sobre isso em <a href="/2026/02/08/rant-ia-acabou-com-programadores/">RANT: IA acabou com os programadores?</a> e também no <a href="/2026/03/05/37-dias-de-imers%C3%A3o-em-vibe-coding-conclus%C3%A3o-quanto-a-modelos-de-neg%C3%B3cio/">37 dias de Imersão em Vibe Coding</a>. A bolha das startups, o dinheiro barato e a fome de contratação produziram uma legião de programadores muito ruins, saídos de bootcamps de dois meses e cursinhos prometendo salário de Google sem base, sem formação e sem profundidade. O mercado passou uma década fingindo que isso era normal. Não era. Era a mesma história de sempre: muito volume, pouco valor agregado, muita gente confundindo empregabilidade inflada com competência real.</p>
<p>E quando os layoffs começaram em 2022, isso não caiu do céu. Eu passei anos avisando que a bolha ia estourar. Tá tudo registrado na playlist <a href="https://www.youtube.com/watch?v=wpPv1dJWjDs&amp;list=PLdsnXVqbHDUehzKjiRruy--gncHz9Injy&amp;pp=sAgC"target="_blank" rel="noopener">EU AVISEI</a>. A mensagem sempre foi a mesma: quando o dinheiro barato acabasse, a régua subiria de novo, e só teria chance quem tivesse feito o esforço de aprender Ciência da Computação de verdade. O novo ciclo econômico seria menos sobre volume de contratação e mais sobre eficiência. Foi exatamente o que aconteceu.</p>
<h2>O que mudou de verdade<span class="hx:absolute hx:-mt-20" id="o-que-mudou-de-verdade"></span>
    <a href="#o-que-mudou-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>LLMs ficaram populares no fim de 2022. Isso é fato. Mas popular não é a mesma coisa que útil pra projeto sério.</p>
<p>Entre 2023 e 2024, eu já usava IA pra escrever código. Funcionava? Funcionava. Mas ainda era cheio de chatice: alucinação demais, loop agente demais, contexto se perdendo fácil demais, ferramenta quebrando demais, custo alto demais pra pouca confiabilidade. Era útil pro programador experiente que sabia segurar o bicho na coleira. Ainda não era ferramenta madura pra trabalho pesado do dia a dia.</p>
<p>Pra mim, a virada veio no fim de 2025. Foi quando a combinação de melhores modelos, prompt caching, tool calling decente, otimizações de inferência, janelas de contexto mais úteis na prática, e principalmente interfaces de agentes de verdade fizeram a coisa parar de parecer demo de conferência e começar a parecer ferramenta de trabalho.</p>
<p>Foi aí que Claude Code, Codex, OpenCode e similares deixaram de ser &ldquo;autocomplete turbinado&rdquo; e viraram outra categoria de interface.</p>
<p>Pra mim, Claude Code já virou o novo terminal. O editor ficou em segundo plano.</p>
<p>Eu falei disso também em <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Migrando meu Home Server com Claude Code</a>. Eu simplesmente não tenho mais paciência pra gastar atenção com trabalho braçal de shell Linux quando o problema é mundano: instalar servidor, subir e organizar serviços Docker, endurecer firewall, revisar regra de segurança, ajustar parâmetro de kernel, auditar <code>dmesg</code>, caçar log de systemd, esse tipo de coisa. Eu mando o Claude fazer o grosso, eu reviso direção e risco. E, ironicamente, meus Linux nunca pareceram tão estáveis, rápidos e robustos.</p>
<p>Hoje, pra quem trabalha o dia inteiro construindo software, voltar pro combo cru de editor de texto mais terminal e fazer tudo manualmente começa a parecer regressão. Não porque digitar ficou impossível. Claro que não. Eu digitei código por décadas. O problema é outro: virou desperdício de atenção.</p>
<p>Se eu posso descrever uma intenção, pedir pra um agente vasculhar o código, editar vinte arquivos, rodar teste, compilar, corrigir, e me devolver uma proposta de mudança em minutos, por que exatamente eu vou sentir nostalgia de ficar digitando boilerplate na unha dentro do VS Code?</p>
<p>Não vou.</p>
<p>E aqui entra uma distinção que muita gente ainda não entendeu. Não é pra usar agente de código como se fosse extensão burra de editor, no estilo &ldquo;gera esse arquivinho aqui&rdquo; e você fica microgerenciando cada linha no canto da tela. Isso é usar Ferrari pra ir comprar pão na esquina. O ganho grande não vem de tratar Claude Code, Codex ou similares como autocomplete glorificado dentro do VS Code. O ganho vem quando você larga a mentalidade de operador de editor e passa a tratar o agente como pair programmer de verdade.</p>
<p>Em vez de agir como digitador profissional, você sobe um nível. Age mais como tech lead, product owner, QA, gerente do fluxo. Define a intenção, explica contexto, cobra critério, pede plano, manda rodar teste, pede refatoração, pede comparação de alternativas, pede revisão da própria mudança. Deixa o trabalho braçal do código com o agente e usa sua cabeça pra julgar direção, prioridade, risco e qualidade.</p>
<p>Mas tem um equilíbrio aí que eu já comentei em outros posts de Agile Vibe Coding. Não é pra largar o volante e deixar a LLM dar <code>git push</code> cega em tudo. E também não é pra cair no extremo oposto e virar fiscal de vírgula, nitpickando cada detalhe pequeno até o agente virar mais uma burocracia e matar o ganho de velocidade. Os dois extremos são ruins. Num extremo você terceiriza responsabilidade. No outro você estrangula produtividade.</p>
<p>O ponto certo do pêndulo é outro: usar práticas de XP e engenharia de verdade pra sustentar a velocidade. Refactoring contínuo. Testes. CI. Revisão. Feedback rápido. Código pequeno. Mudança incremental. Foi exatamente isso que eu vim documentando em posts como <a href="/2026/02/20/do-zero-a-pos-producao-em-1-semana-como-usar-ia-em-projetos-de-verdade-bastidores-do-the-m-akita-chronicles/">Do Zero à Pós-Produção em 1 Semana - Como usar IA em Projetos de Verdade</a> e <a href="/2026/03/01/software-nunca-esta-pronto-4-projetos-a-vida-pos-deploy-e-por-que-one-shot-prompt-e-mito/">Software Nunca Está &lsquo;Pronto&rsquo;</a>. O multiplicador de 10x não vem da mágica do modelo. Vem do modelo somado a processo decente.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cursor-homepage-crop.png" alt="Interface moderna de agente de código, representando a transição do editor tradicional para uma interface orientada a intenção e execução assistida"  loading="lazy" /></p>
<p><em>A interface mudou. O que continua igual é a necessidade de julgamento.</em></p>
<h2>VS Code é o novo cartão perfurado<span class="hx:absolute hx:-mt-20" id="vs-code-é-o-novo-cartão-perfurado"></span>
    <a href="#vs-code-%c3%a9-o-novo-cart%c3%a3o-perfurado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>É isso que o título quer dizer.</p>
<p>VS Code não &ldquo;ficou ruim&rdquo;. Não é isso. Cartão perfurado também não era &ldquo;ruim&rdquo; no contexto histórico dele. Foi uma evolução brutal em relação a digitar bit na mão ou religar fio. O ponto é que ele era o mecanismo da era dele pra informar instruções à máquina.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/punched-card-program-deck.jpg" alt="Deck de cartões perfurados, quando a profissão exigia mais disciplina na preparação do input do que conforto na interface"  loading="lazy" /></p>
<p><em>VS Code não é o inimigo. Ele só está virando o mecanismo de input da era anterior.</em></p>
<p>Hoje, editor de texto está virando isso de novo: um mecanismo de input que ainda funciona, ainda vai existir por muito tempo, mas que já não é mais o centro da atividade.</p>
<p>Se você nunca viu essa história das eras mais antigas da computação, eu já expliquei isso em <a href="/2020/10/23/akitando-86-o-computador-de-turing-e-von-neumann-por-que-calculadoras-nao-sao-computadores/">Akitando #86 - O Computador de Turing e Von Neumann</a>:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/G4MvFT8TGII"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>E se quiser relembrar por que 6502, Z80 e as máquinas antigas forçavam outro tipo de disciplina, revisita o <a href="/2020/06/04/akitando-80-o-guia-hardcore-de-introducao-a-computacao/">Guia +Hardcore de Introdução à Computação</a> e o episódio <a href="/2020/06/18/akitando-81-aprendendo-sobre-computadores-com-super-mario-do-jeito-hardcore/">Aprendendo sobre Computadores com Super Mario (do jeito Hardcore++)</a>. Aquilo não era nostalgia de velho. Era pra mostrar que, em cada era, a interface muda, mas a máquina continua exigindo precisão.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/akitando-80-6502.jpg" alt="Thumbnail do Akitando #80, parte da série feita para ensinar fundamentos de computação usando a era 6502 e microcomputadores como ponto de partida"  loading="lazy" /></p>
<p><em>Foi pra isso que boa parte do Akitando existiu: ensinar o que continua valendo quando a ferramenta da moda muda.</em></p>
<p>Aliás, vale lembrar uma coisa que muita gente esquece: os 146 vídeos do <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando</a>, mais de 96 horas de conteúdo, foram feitos justamente pra ensinar esse tipo de fundamento pra estudante de Ciência da Computação, júnior e pra quem queria deixar de ser apertador de framework. Eu gravei aquilo porque já via a indústria empurrando gente demais pra tarefa braçal e entendimento de menos. Ironicamente, agora que os agentes chegaram, esse acervo ficou mais relevante do que nunca.</p>
<p>Hoje a interface mudou de novo.</p>
<p>Antes você precisava saber como digitar a instrução.
Depois você precisava saber como ordenar o deck.
Depois você precisava saber como convencer o compilador.
Depois você precisava saber como costurar framework, HTML, CSS, YAML, CI, container, cloud, ORM, fila, observabilidade e mais cinquenta camadas de parafernália.</p>
<p>Agora você precisa saber como <strong>orquestrar um agente</strong>.</p>
<p>E isso, de novo, não elimina fundamento. Só muda o ponto onde o trabalho braçal termina e o trabalho intelectual começa.</p>
<h2>&ldquo;Então não precisa mais aprender Ciência da Computação?&rdquo;<span class="hx:absolute hx:-mt-20" id="então-não-precisa-mais-aprender-ciência-da-computação"></span>
    <a href="#ent%c3%a3o-n%c3%a3o-precisa-mais-aprender-ci%c3%aancia-da-computa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pelo contrário.</p>
<p>Agora precisa mais.</p>
<p>O sujeito sem base olha pro agente fazendo um <code>SELECT * FROM table</code>, vê o negócio funcionando localmente com 300 linhas na base fake, e acha que tá tudo certo. Em produção a query puxa um milhão de linhas, explode memória, degrada latência, derruba fila, congestiona conexão, e o cidadão não faz a menor ideia de por que &ldquo;na minha máquina funciona&rdquo;.</p>
<p>Esse é o problema real.</p>
<p>O agente não sabe o contexto do seu sistema do jeito que um engenheiro experiente sabe. Ele não sabe quais tabelas vão crescer dez vezes no próximo trimestre. Ele não sabe qual endpoint precisa responder em 80 ms e qual pode levar 2 segundos. Ele não sabe qual fluxo precisa de transação, qual precisa de idempotência, qual precisa de lock pessimista, qual precisa de compensação assíncrona, qual precisa de auditoria, qual não pode jamais vazar dado sensível.</p>
<p>Ele pode até acertar a sintaxe.</p>
<p>Só que sintaxe nunca foi a parte mais difícil.</p>
<p>Eu já falei isso no <a href="/2026/02/24/rant-o-akita-abriu-as-pernas-pra-ia/">RANT: o Akita abriu as pernas pra IA??</a>: o que IA faz muito bem é remover as tarefas mundanas. E graças a deus. Eu não entrei em computação pra virar operador de IDE. Eu não sinto nenhum apego romântico por ficar formatando HTML, brigando com CSS, montando CRUD de sempre, colando framework novo em stack velha, ou escrevendo pela centésima vez o mesmo monte de código de infraestrutura que qualquer máquina decente já deveria conseguir produzir.</p>
<p>Mas o que sobra depois que essa camada mundana some?</p>
<p>Sobra justamente a parte que separa amador de programador de verdade:</p>
<ul>
<li>modelagem de domínio</li>
<li>arquitetura</li>
<li>trade-off</li>
<li>performance</li>
<li>escalabilidade</li>
<li>segurança</li>
<li>observabilidade</li>
<li>manutenção</li>
<li>legibilidade</li>
<li>custo operacional</li>
<li>decisão de produto</li>
</ul>
<p>Tudo isso continua existindo. Tudo isso continua sendo difícil. Tudo isso continua dependendo de julgamento.</p>
<h2>O erro da turma que acha que programar era digitar<span class="hx:absolute hx:-mt-20" id="o-erro-da-turma-que-acha-que-programar-era-digitar"></span>
    <a href="#o-erro-da-turma-que-acha-que-programar-era-digitar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem gente realmente achando que, se a máquina escreve o código, então acabou a necessidade de saber software.</p>
<p>Isso é a mesma burrice de achar que compilador matou a necessidade de entender computador.</p>
<p>Não matou.</p>
<p>Só matou a necessidade de ficar escrevendo Assembly pra tudo.</p>
<p>E ainda bem.</p>
<p>Da mesma forma, agente de código não mata a necessidade de entender software. Mata a necessidade de você ser datilógrafo de sintaxe.</p>
<p>E ainda bem.</p>
<p>Aliás, tem uma ironia bonita aqui: durante anos a indústria vendeu a fantasia de que programar era &ldquo;aprender framework&rdquo;. Depois vendeu a fantasia de que programar era &ldquo;aprender React&rdquo;. Depois vendeu a fantasia de que programar era &ldquo;aprender a stack do momento&rdquo;. Agora vai vender a fantasia de que programar é &ldquo;aprender prompt&rdquo;.</p>
<p>Também não é.</p>
<p>Prompt é interface.
Framework é interface.
IDE é interface.
Cartão perfurado era interface.</p>
<p>Programação continua sendo o ato de instruir uma máquina a computar algo útil dentro de restrições reais.</p>
<p>Quem entende isso sobrevive a qualquer mudança de ferramenta.
Quem não entende vira operador da ferramenta da moda e, quando a moda muda, dança junto.</p>
<h2>O que eu acho que vai acontecer com os júniors<span class="hx:absolute hx:-mt-20" id="o-que-eu-acho-que-vai-acontecer-com-os-júniors"></span>
    <a href="#o-que-eu-acho-que-vai-acontecer-com-os-j%c3%baniors" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Então vamos responder a pergunta original direito.</p>
<p>Os júniors não vão deixar de aprender.</p>
<p>Mas vão ter que aprender <strong>outra coisa</strong>.</p>
<p>Se o júnior de 2015 conseguia passar anos escondendo ignorância atrás de tarefa braçal de baixo valor, mexendo em view, ajustando CSS, montando endpoint bobo, copiando snippet de Stack Overflow e fazendo parecer que estava &ldquo;produzindo&rdquo;, esse esconderijo está acabando.</p>
<p>O júnior da era dos agentes vai subir de nível mais rápido ou vai ser exposto mais rápido. Não tem muito meio-termo.</p>
<p>Se ele usar agente e realmente estudar fundamento, ele vai conseguir testar hipótese mais rápido, ler mais código, comparar mais soluções, iterar mais, errar mais cedo e corrigir mais cedo. Vai aprender mais em menos tempo.</p>
<p>Mas se ele usar agente sem fundamento, ele vai só terceirizar a própria ignorância. Vai virar revisor incapaz de revisar. Vai aceitar patch que não entende. Vai aprovar decisão que não sabe medir. Vai confundir &ldquo;passou no teste local&rdquo; com &ldquo;está pronto pra produção&rdquo;.</p>
<p>Esse profissional é perigoso.</p>
<p>Muito mais perigoso do que o júnior antigo que ao menos era limitado pela própria lentidão.</p>
<h2>O pós-bolha<span class="hx:absolute hx:-mt-20" id="o-pós-bolha"></span>
    <a href="#o-p%c3%b3s-bolha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A boa notícia é que isso vem logo depois do colapso da fase mais idiota da bolha de contratação.</p>
<p>Já passou da hora de o mercado parar de premiar trabalho intensivo e burro como se fosse competência. Já passou da hora de parar de tratar burocracia de stack como profundidade técnica. Já passou da hora de parar de confundir volume de commit com valor de engenharia.</p>
<p>Se a nova era elimina uma parte grande desse teatro, ótimo.</p>
<p>Num cenário pós-bolha, pós-bootcamp milagroso, pós-CSS como carreira, pós-CRUD como profissão, fundamento volta a ser o que sempre deveria ter sido: o ativo principal.</p>
<p>Quem entende sistema operacional, banco de dados, rede, estrutura de dados, compiladores, arquitetura de computador, profiling, debugging, concorrência, consistência, segurança e custo, vai usar agentes como exoesqueleto.</p>
<p>Quem não entende nada disso vai usar agente como muleta.</p>
<p>Exoesqueleto amplia força.
Muleta só tenta esconder fraqueza.</p>
<h2>&ldquo;Mas isso não é sustentável&rdquo;<span class="hx:absolute hx:-mt-20" id="mas-isso-não-é-sustentável"></span>
    <a href="#mas-isso-n%c3%a3o-%c3%a9-sustent%c3%a1vel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sempre aparece alguém com a mesma desculpa: &ldquo;ah, mas eu não acho que isso seja sustentável, os data centers não vão aguentar, os preços estão subsidiados demais, não tem como isso continuar assim.&rdquo;</p>
<p>E olha, essa pessoa não está completamente errada.</p>
<p>Só que isso não muda em nada o que eu tenho pra fazer amanhã de manhã.</p>
<p>Esse tipo de preocupação pode até render papo de bar ou thread no X, mas não me ajuda a decidir nada útil. Eu, você, nenhum de nós vai sentar com a diretoria da Anthropic ou da OpenAI pra redesenhar capex de data center, renegociar contrato de energia, decidir margem de subsídio ou planejar a próxima geração de GPU. Não tem nenhuma ação concreta que saia disso pra nós além de ficar repetindo que &ldquo;um dia vai dar problema&rdquo;.</p>
<p>É a mesma mentalidade de quem olhava pra internet nos anos 90 e falava: &ldquo;vamos não usar muito isso, é lento demais, o limite é ridículo, o preço por kilobyte é absurdo, melhor esperar arrumarem.&rdquo; Ou de quem, no começo dos anos 2000, olhava pra dados móveis e dizia: &ldquo;2G é lento demais, é limitado demais, melhor não depender disso.&rdquo; Por que exatamente você ia querer ser essa pessoa?</p>
<p>Ainda bem que OpenAI, Anthropic e o resto estão se estapeando e subsidiando pesado essa corrida. Eu estou aproveitando sem o menor pudor. Já torrei meu Claude Max 20x inteiro, já bati no limite de extra usage, já torrei meu plano do Codex, e subi pra Pro pra continuar usando neste fim de semana. Quem paga mensalidade e usa pouco está, na prática, me subsidiando pra eu usar tudo o que consigo. Eu não tenho a menor intenção de desacelerar. Por que você teria?</p>
<p>Se amanhã os preços mudarem, a infraestrutura apertar ou o jogo virar, eu reavalio amanhã. É assim que tecnologia sempre funcionou. Enquanto a janela está aberta, o racional não é frear por antecipação. O racional é aprender o máximo, extrair o máximo, ganhar vantagem enquanto o resto está ocupado explicando por que ainda não começou.</p>
<h2>A decisão continua sendo humana<span class="hx:absolute hx:-mt-20" id="a-decisão-continua-sendo-humana"></span>
    <a href="#a-decis%c3%a3o-continua-sendo-humana" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No fim do dia, nada do que importa mudou.</p>
<p>Alguém continua precisando olhar pro resultado e decidir:</p>
<ul>
<li>isso pode ir pra produção?</li>
<li>isso aguenta carga?</li>
<li>isso está legível?</li>
<li>isso está seguro?</li>
<li>isso está fácil de manter?</li>
<li>isso conversa com o resto do sistema?</li>
<li>isso resolve o problema certo?</li>
</ul>
<p>Se a resposta for não, alguém continua precisando saber <strong>por que</strong> é não.</p>
<p>E mais importante: alguém continua precisando saber <strong>como corrigir</strong>.</p>
<p>É por isso que, na era dos agentes, conhecimento de base não ficou menos importante. Ficou mais caro errar sem ele.</p>
<p>VS Code é o novo cartão perfurado.</p>
<p>Não porque ficou inútil.</p>
<p>Mas porque finalmente estamos entrando numa era em que o ato de digitar código manualmente deixa de ser o centro da profissão.</p>
<p>E, honestamente? Já foi tarde.</p>
<blockquote>
  <p><strong>IA reflete quem você é.</strong></p>
<p>Se você é bom, ela acelera código bom.</p>
<p>Se você é ruim, ela acelera dívida técnica numa velocidade industrial.</p>
<p>IA não vai pegar programador ruim e transformar em programador bom. Nunca transformou, não transforma e não vai transformar.</p>

</blockquote>
<p>Por isso fundamento importa mais agora do que antes.</p>
<p>O agente pode escrever. Quem continua precisando saber se aquilo presta é você.</p>
]]></content:encoded><category>ai</category><category>llm</category><category>opinion</category><category>programming</category></item><item><title>Como a ElevenLabs Não Foi Morta pelo Qwen3 TTS</title><link>https://www.akitaonrails.com/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/</guid><pubDate>Thu, 09 Apr 2026 08:30:00 GMT</pubDate><description>&lt;p&gt;&lt;strong&gt;TL;DR — Escuta isso e continua lendo:&lt;/strong&gt;&lt;/p&gt;
&lt;audio controls preload="metadata" style="width: 100%; max-width: 640px;"&gt;
&lt;source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg"&gt;
Seu navegador não suporta o elemento de áudio. &lt;a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3"&gt;Baixar o mp3 aqui.&lt;/a&gt;
&lt;/audio&gt;
&lt;p&gt;Esse é o episódio do dia 6 de abril do podcast do &lt;a href="https://www.akitaonrails.com/tags/themakitachronicles/"&gt;The M.Akita Chronicles&lt;/a&gt;, já gerado com a nova pipeline da ElevenLabs v3. Assina o canal no &lt;a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener"&gt;Spotify&lt;/a&gt; pra não perder episódio novo desses.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Quando o Qwen3 TTS foi lançado, pelos idos de janeiro desse ano, todo mundo no Twitter/X e nas newsletters de IA gritou &amp;ldquo;ElevenLabs killer&amp;rdquo;. Tem &lt;a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener"&gt;artigo no Medium&lt;/a&gt; dizendo que é a primeira ameaça open source real à ElevenLabs. Tem &lt;a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener"&gt;post da byteiota&lt;/a&gt; dizendo que a clonagem de voz em 3 segundos bate a ElevenLabs. Tem &lt;a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener"&gt;análise no Analytics Vidhya&lt;/a&gt; falando que é o TTS open source mais realista já lançado. O consenso da internet entusiasta era: finalmente temos open source que faz frente à ElevenLabs, o jogo virou, é só questão de tempo.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>TL;DR — Escuta isso e continua lendo:</strong></p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Seu navegador não suporta o elemento de áudio. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Baixar o mp3 aqui.</a>
</audio>
<p>Esse é o episódio do dia 6 de abril do podcast do <a href="/tags/themakitachronicles/">The M.Akita Chronicles</a>, já gerado com a nova pipeline da ElevenLabs v3. Assina o canal no <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> pra não perder episódio novo desses.</p>
<hr>
<p>Quando o Qwen3 TTS foi lançado, pelos idos de janeiro desse ano, todo mundo no Twitter/X e nas newsletters de IA gritou &ldquo;ElevenLabs killer&rdquo;. Tem <a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener">artigo no Medium</a> dizendo que é a primeira ameaça open source real à ElevenLabs. Tem <a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener">post da byteiota</a> dizendo que a clonagem de voz em 3 segundos bate a ElevenLabs. Tem <a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener">análise no Analytics Vidhya</a> falando que é o TTS open source mais realista já lançado. O consenso da internet entusiasta era: finalmente temos open source que faz frente à ElevenLabs, o jogo virou, é só questão de tempo.</p>
<p>Eu resolvi testar no meu próprio fluxo de produção, como de costume. Montei um pipeline inteiro em cima do Qwen3 TTS 1.7B pra gerar o podcast semanal do <a href="/tags/themakitachronicles/">The M.Akita Chronicles</a>, e documentei os bastidores no <a href="/2026/02/18/servindo-ia-na-nuvem-meu-tts-pessoal-bastidores-do-the-m-akita-chronicles/">post sobre servir IA na nuvem</a>. Quem quiser ver o detalhe de tempo de partida a frio, clonagem de voz, parâmetros de sampling que mudam de um modo pro outro, dá uma olhada nesse link que eu não vou repetir tudo aqui.</p>
<p>A pergunta desse post é diferente. Depois de quase dois meses rodando essa configuração em produção, com episódio indo ao ar toda segunda-feira, ontem à noite eu finalmente desliguei o Qwen3 e passei tudo pra ElevenLabs v3. Vou contar por quê.</p>
<h2>O que não funcionou no Qwen3<span class="hx:absolute hx:-mt-20" id="o-que-não-funcionou-no-qwen3"></span>
    <a href="#o-que-n%c3%a3o-funcionou-no-qwen3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Entre 15 de fevereiro e 30 de março eu fiz dezenas de commits ajustando o fluxo do podcast: prompts, parâmetros de sampling, ordem de geração, silêncios de referência na amostra de clone de voz, normalização de volume, pronúncia de siglas técnicas. Corrigir a voz do Marvin que cortava a primeira sílaba porque o áudio de referência começava sem silêncio inicial. Afinar a voz do Akita pra ela soar mais confiante e assertiva. Adicionar crossfade entre jingles de seção. Resolver a pronúncia errada de &ldquo;podcast&rdquo; pra ela não sair como &ldquo;pódcast&rdquo;. Mandar o gerador de roteiro preferir português a anglicismo gratuito pro TTS não engasgar. Cada um desses ajustes foi uma sessão de horas escutando áudio, gerando de novo, ajustando parâmetro.</p>
<p>O resultado ficou aceitável. &ldquo;Aceitável&rdquo; no sentido de que eu consegui publicar todo episódio sem ter que regravar nada à mão. Mas escutando com atenção, a voz do Qwen3 tem aquele jeito inconfundível de IA gerando áudio. Intonação meio morta, ritmo uniforme. Em trechos longos, você sente que é máquina falando. Serve pra ir pro ar, mas tá a quilômetros do que você escuta num podcast profissional feito por gente.</p>
<p>O problema pior foi a pronúncia em inglês. Meu podcast cobre notícias de tecnologia, então termos como &ldquo;MCP&rdquo;, &ldquo;RAG&rdquo;, &ldquo;Claude Opus&rdquo;, &ldquo;GPT-5&rdquo;, &ldquo;open source&rdquo; aparecem em toda conversa. O Qwen3 pegava esses termos e pronunciava eles com sotaque brasileiro, tipo &ldquo;ó-péne-ssourssê&rdquo;, coisa assim. Ficava ilegível pro ouvinte. A solução que eu tive que implementar foi mapear manualmente no prompt do LLM que gera o roteiro quais palavras em inglês trocar por equivalente em português. O prompt hoje tem uma seção inteira dividida entre &ldquo;manter em inglês&rdquo; (nomes próprios, marcas, termos já incorporados ao brasileiro) e &ldquo;traduzir pro português&rdquo; (anglicismo gratuito), mais ou menos assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">**REPLACE with Portuguese** (common English words that have natural
</span></span><span class="line"><span class="cl">Brazilian Portuguese equivalents):
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;update&#34; → &#34;atualização&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;release&#34; → &#34;lançamento&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;feature&#34; → &#34;recurso/funcionalidade&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;deploy&#34; → &#34;implantação&#34; or just &#34;colocar em produção&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;trade-off&#34; → &#34;dilema&#34; or &#34;escolha&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;performance&#34; → &#34;desempenho&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;default&#34; → &#34;padrão&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;insight&#34; → &#34;percepção/sacada&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;skills&#34; → &#34;habilidades&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;approach&#34; → &#34;abordagem&#34;
</span></span><span class="line"><span class="cl">- &#34;highlights&#34; → &#34;destaques&#34;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Chamam isso aqui em casa de &ldquo;limpar anglicismo pra o TTS não engasgar&rdquo;. Engraçado porque eu não queria esse nível de restrição no meu roteiro. Queria que a voz simplesmente pronunciasse &ldquo;update&rdquo; quando o contexto natural fosse &ldquo;update&rdquo;. Como o modelo não consegue, tive que mutilar o vocabulário do podcast pra o resultado final ficar escutável. É uma solução paliativa, daquelas que você adiciona torcendo pra poder remover depois quando a tecnologia amadurecer.</p>
<h2>A experiência com a ElevenLabs v3<span class="hx:absolute hx:-mt-20" id="a-experiência-com-a-elevenlabs-v3"></span>
    <a href="#a-experi%c3%aancia-com-a-elevenlabs-v3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ontem à tarde abri uma conta na ElevenLabs, comprei o plano Pro (US$99/mês), e comecei a experimentar com o modelo <code>eleven_v3</code>, que saiu em fevereiro desse ano. Trinta minutos depois eu tinha uma prova de conceito rodando, e umas duas horas depois o sistema inteiro do podcast migrou. A diferença de esforço é abissal.</p>
<p>Os detalhes técnicos da migração ficaram documentados num doc interno do projeto, então vou resumir aqui o quadro comparativo que importa:</p>
<table>
  <thead>
      <tr>
          <th>Dimensão</th>
          <th>Qwen3 TTS 1.7B (antigo)</th>
          <th>ElevenLabs <code>eleven_v3</code> (atual)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qualidade (Akita)</td>
          <td>Boa, clone de áudio real</td>
          <td>Melhor, mesmo clone mas prosódia mais natural</td>
      </tr>
      <tr>
          <td>Emoção inline no roteiro</td>
          <td>Não suporta</td>
          <td><code>[sighs]</code>, <code>[sarcastically]</code>, <code>[excited]</code>, <code>[laughs]</code>, funciona em pt-BR</td>
      </tr>
      <tr>
          <td>Tempo de partida a frio</td>
          <td>5 a 15 min subindo GPU na RunPod antes de cada rodagem</td>
          <td>Zero, chamada HTTPS com resposta imediata</td>
      </tr>
      <tr>
          <td>Vazão</td>
          <td>~1× do tempo real (serializado)</td>
          <td>~6× do tempo real com concorrência 4</td>
      </tr>
      <tr>
          <td>Tempo total pra um episódio de 28 min</td>
          <td>~25 a 30 min</td>
          <td><strong>~4 min</strong></td>
      </tr>
      <tr>
          <td>Superfície operacional</td>
          <td>RunPod, Docker, FastAPI, pesos do Qwen, conta de GPU</td>
          <td>Uma variável de ambiente (<code>ELEVENLABS_API_KEY</code>)</td>
      </tr>
      <tr>
          <td>Custo por episódio</td>
          <td>~$0.08 de GPU</td>
          <td>~$2.70 em créditos ElevenLabs</td>
      </tr>
  </tbody>
</table>
<p>Repara no último ponto. O Qwen3 custa menos de dez centavos de dólar por episódio. A ElevenLabs custa quase trinta vezes mais. E mesmo assim vale a pena. Os outros pontos do quadro resolvem problemas que sugavam horas da minha semana. Eu não preciso mais escrever código pra escalar GPU na nuvem, não preciso esperar a máquina ligar toda vez, não preciso ficar de babá do modelo nem reconstruir a imagem Docker quando o peso muda. A operação virou uma linha de configuração.</p>
<p>E o mais interessante: as tags de emoção no meio do texto. O modelo v3 aceita marcadores tipo <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[dryly]</code>, <code>[excited]</code>, e muda a entonação de acordo. Isso funciona em mais de 70 idiomas, incluindo português do Brasil. Isso transformou a geração do roteiro porque agora eu posso pedir ao LLM que monta o roteiro pra colocar tags emocionais nos momentos certos, o que dá uma vivacidade que o Qwen3 não conseguia entregar nem de longe. Um exemplo concreto do que sai no roteiro depois:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AKITA: Isso é simples. [dismissive] Quem ainda acredita que Bitcoin
vai morrer não tá prestando atenção.
MARVIN: [sighs] Mais uma semana, mais uma leva de devs confiando
cegamente em pacotes npm. Previsível.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Eu tenho até duas paletas separadas de tags, uma pro Akita (expressivo mas controlado, usa <code>[excited]</code>, <code>[dismissive]</code>, <code>[emphatic]</code>) e outra pro Marvin (estóico, só <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[tired]</code>, <code>[dryly]</code>). Isso tá tudo codificado nos prompts de geração do roteiro pro LLM saber que personagem pode usar o quê.</p>
<h2>Sobre a voz do Marvin<span class="hx:absolute hx:-mt-20" id="sobre-a-voz-do-marvin"></span>
    <a href="#sobre-a-voz-do-marvin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pros ouvintes que já se acostumaram com a voz do Marvin, fica tranquilo: eu fiz o clone dele na ElevenLabs usando a mesma amostra de áudio que eu já tinha usado pra treinar no Qwen3. É a mesma voz. Só que agora ela soa ainda melhor, porque o modelo da ElevenLabs captura nuance e prosódia que o Qwen3 não conseguia entregar.</p>
<h2>Escuta aí e me conta<span class="hx:absolute hx:-mt-20" id="escuta-aí-e-me-conta"></span>
    <a href="#escuta-a%c3%ad-e-me-conta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra provar que o papo é real, aqui tá o episódio de segunda-feira, dia 6 de abril, já gerado com a nova pipeline:</p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Seu navegador não suporta o elemento de áudio. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Baixar o mp3 aqui.</a>
</audio>
<p>Se você já é ouvinte do podcast no <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> e escutou os episódios anteriores feitos com Qwen3, compara e me diz nos comentários se você nota a diferença ou se pra você tanto faz. Eu tô curioso de saber quanto é percepção treinada minha escutando horas de áudio de TTS e quanto é diferença óbvia pra ouvinte casual.</p>
<p>A partir da próxima semana, todos os episódios do podcast serão gerados pela ElevenLabs v3. A newsletter já tá ligada na nova pipeline, os jobs agendados de pré-aquecer a GPU na RunPod foram desativados, e o código legado do Qwen3 fica no repositório como plano B caso um dia o v3 dê problema crônico. Em duas edições de arquivo eu volto pra ele. Provavelmente nunca vou ter que voltar.</p>
<h2>A parte de dublar os vídeos do YouTube<span class="hx:absolute hx:-mt-20" id="a-parte-de-dublar-os-vídeos-do-youtube"></span>
    <a href="#a-parte-de-dublar-os-v%c3%addeos-do-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora o gancho pra segunda metade desse post. No <a href="/2026/04/09/20-anos-de-blog-o-ano-em-que-a-ia-finalmente-me-deixou-traduzir-tudo/">artigo de aniversário que eu publiquei mais cedo hoje</a>, contei que o Claude Code traduziu quase metade do meu blog pra inglês num fim de semana. No mesmo espírito, fui atrás de dublar os vídeos do canal <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando</a>.</p>
<p>O canal tem 146 episódios, algo como 96 horas de conteúdo técnico em português, e mais de 500 mil inscritos. Eu já tinha as legendas traduzidas (um <code>.srt</code> curado por episódio), e o YouTube até oferece dublagem automática em inglês. Mas o resultado é tipo Google Translate de 2015: entende, passa a ideia, mas ninguém quer ouvir por muito tempo.</p>
<p>Testei as três abordagens de voz da ElevenLabs e só uma servia. Speech-to-Speech converte voz mas não traduz. A API de Dubbing traduz mas cria a voz sozinha, sem deixar forçar um clone específico. Só a Text-to-Speech resolvia: pegar meu <code>.srt</code> em inglês, mandar cada bloco pro endpoint TTS com a minha voz clonada, e montar o áudio alinhado com o vídeo original.</p>
<p>Só que ter o <code>.srt</code> traduzido não basta. Tradução crua de legenda não sobrevive ao TTS. Trechos com código-fonte, URLs, hashes hexadecimais, listas de comandos shell — o modelo entra em modo soletrar e o áudio sai com o dobro da duração esperada. Traduções longas demais estouram a janela de tempo e precisam ser condensadas pra a voz não ficar atropelada. SRTs truncadas precisam ser completadas. E a cada correção, rodar a pipeline de novo, escutar, achar o próximo problema, corrigir, repetir. O processo inteiro foi um ciclo de interrupções, correções manuais e re-runs — longe do &ldquo;aperta o botão e sai dublado&rdquo; que a gente imagina antes de meter a mão.</p>
<p>O grande desafio era o sotaque. Minha voz clonada foi treinada em português brasileiro, então quando tenta falar inglês o sotaque puxado aparece. A ElevenLabs tem a tag <code>[American accent]</code> que funciona em v3, mas em cima de uma voz treinada em outra língua ela é fraca — o sotaque brasileiro ainda saía por baixo. A saída foi treinar uma segunda voz minha, só em inglês. Gravei uns minutos no meu melhor sotaque americano, subi como Instant Voice Clone separado na minha conta, batizei de &ldquo;Akita English&rdquo;, e configurei o pipeline pra usar essa voz por padrão. O resultado sai mais natural, sem precisar de tag nenhuma, e a identidade de voz continua sendo a minha.</p>
<h3>O teto honesto de qualidade da minha voz em inglês<span class="hx:absolute hx:-mt-20" id="o-teto-honesto-de-qualidade-da-minha-voz-em-inglês"></span>
    <a href="#o-teto-honesto-de-qualidade-da-minha-voz-em-ingl%c3%aas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Vamos ser francos. Eu sou brasileiro falando inglês, não sou nativo, e não tenho tempo nem paciência pra treinar pronúncia por horas. O clone só consegue ser tão bom quanto a amostra original — se a amostra é um não-nativo lendo um roteiro, o clone herda as imperfeições. Nenhum clone vai me fazer soar como &ldquo;Fabio falando inglês perfeito&rdquo;, porque esse som não existe pra o modelo imitar.</p>
<p>Pra chegar na versão que tá rodando, gravei cinco amostras de treino diferentes ao longo do dia, cada uma com ritmo e roteiro diferentes, e rodei um A/B ao vivo comparando com o original em português. Nenhuma soou como nativo, e nunca foi esse o objetivo. A meta realista era mais modesta: soar reconhecivelmente como eu, ler conteúdo técnico sem tropeçar, e aguentar um episódio inteiro sem cansar o ouvinte. O vencedor acabou sendo a primeira iteração, a &ldquo;Akita English&rdquo; original. Longe de perfeito, mas suficiente pra colocar no ar. Trocar no futuro é literalmente uma linha no arquivo de configuração.</p>
<p>O que mais me surpreendeu no caminho foi perceber que o ritmo da amostra importa mais que o timbre. O clone aprende a cadência de quem gravou: a iteração 5 ficou 25% mais lenta que a iteração 2 lendo exatamente o mesmo texto, só porque eu gravei aquela amostra mais devagar. Pra dublagem de vídeo tech, o alvo certo é ritmo de explicação de YouTube, vivo, uns 150 palavras por minuto. Nada de ritmo de audiobook.</p>
<h3>Como eu alinho o áudio com o vídeo original<span class="hx:absolute hx:-mt-20" id="como-eu-alinho-o-áudio-com-o-vídeo-original"></span>
    <a href="#como-eu-alinho-o-%c3%a1udio-com-o-v%c3%addeo-original" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Um ponto não óbvio: texto em inglês tende a ser mais longo ao falar do que o português equivalente. Se você simplesmente gera o áudio de cada legenda e cola no tempo original, o dub vai se acumulando. Com 30 minutos de vídeo, você já tá vários segundos fora de sincronismo.</p>
<p>Minha abordagem foi atacar o problema em camadas. A primeira versão dividia o SRT em blocos menores, chamados de &ldquo;chunks&rdquo;. Cada chunk tinha no máximo 700 caracteres (pra v3 não alucinar) e só podia cortar em fim de frase, nunca no meio. Um algoritmo simples acumulava legendas num buffer até chegar perto do teto, depois voltava procurando a última legenda do buffer que terminava com ponto, exclamação ou interrogação, e fazia flush só até ali. O resto das legendas já acumuladas ficava pra compor o próximo chunk. Cada chunk guardava o timestamp de início e fim das legendas que cobria, pra a montagem saber exatamente onde posicionar aquele pedaço de áudio depois.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Pra cada nova legenda que chega, checa se adicionar ela</span>
</span></span><span class="line"><span class="cl"><span class="c1"># estouraria o limite do chunk atual.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cue</span> <span class="ow">in</span> <span class="n">cues</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">projected</span> <span class="o">=</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">cue</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">projected</span> <span class="o">&gt;</span> <span class="n">max_chars</span> <span class="ow">and</span> <span class="n">buf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Procura a última legenda do buffer que termina</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># com pontuação final (&#39;.&#39;, &#39;!&#39;, &#39;?&#39;, &#39;…&#39;).</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentence_end</span> <span class="o">=</span> <span class="n">last_sentence_end_index</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">sentence_end</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">sentence_end</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Run-on maior que max_chars sem fim de frase.</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Raro, mas acontece. Avisa e corta mesmo assim.</span>
</span></span><span class="line"><span class="cl">            <span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;run-on sentence &gt; </span><span class="si">%d</span><span class="s2"> chars — &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;splitting mid-sentence as a last resort&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">max_chars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cue</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Soft-break: se o buffer já tá pelo menos 60% cheio E</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># a legenda atual terminou uma frase, esvazia agora.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Mantém os chunks balanceados em torno de 60 a 100% do max.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cue</span><span class="o">.</span><span class="n">ends_sentence</span> <span class="ow">and</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">max_chars</span> <span class="o">*</span> <span class="mf">0.6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tecnicamente, esse foi o ponto de virada: em vez de deixar a estrutura da dublagem seguir a estrutura visual da legenda, o chunker passou a seguir a estrutura real dos parágrafos do roteiro. Não vou repetir aqui toda a novela de como cheguei nisso e por que isso acabou forçando rerender em massa, porque volto nessa história mais abaixo, na parte dos problemas que só apareceram depois do primeiro upload. O ponto técnico aqui é simples: quando a janela passou a representar um parágrafo falado de verdade, o áudio começou a soar natural e o alvo de stretch pôde subir de 92% pra 95%.</p>
<p>Feito isso, ainda sobra o problema do inglês ser mais longo ao falar que o português equivalente. Aqui a sacada é prever se um chunk vai estourar a janela alvo <strong>antes</strong> de gerar o áudio. Se a razão <code>caracteres / 16 caracteres-por-segundo</code> já passa da duração alvo com uma margem, a pipeline manda o parâmetro <code>speed</code> direto pra API da ElevenLabs, pedindo pro modelo gerar o áudio nativamente mais rápido. Isso preserva a prosódia muito melhor do que comprimir depois. O teto é 1.15× (acima disso a voz começa a soar atropelada).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># antes de chamar a API, prevê se vai estourar</span>
</span></span><span class="line"><span class="cl"><span class="n">expected_sec</span> <span class="o">=</span> <span class="n">char_count</span> <span class="o">/</span> <span class="n">EXPECTED_CHARS_PER_SEC</span>  <span class="c1"># 16 chars/s</span>
</span></span><span class="line"><span class="cl"><span class="n">predicted_ratio</span> <span class="o">=</span> <span class="n">expected_sec</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">voice_speed</span> <span class="o">=</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">predicted_ratio</span> <span class="o">&gt;</span> <span class="n">PREEMPTIVE_SPEED_THRESHOLD</span><span class="p">:</span>   <span class="c1"># 1.05</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_speed</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">predicted_ratio</span> <span class="o">*</span> <span class="mf">0.98</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="n">PREEMPTIVE_SPEED_MAX</span><span class="p">)</span>         <span class="c1"># cap em 1.15</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_settings</span><span class="p">[</span><span class="s2">&#34;speed&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">voice_speed</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mesmo com o <code>speed</code> preventivo ligado, às vezes o áudio gerado ainda fica um pouquinho mais longo do que a janela alvo. Pra isso existe uma segunda rede de proteção: medir o áudio real com <code>ffprobe</code> depois de gerado, comparar com a janela alvo, e aplicar o filtro <code>atempo</code> do <code>ffmpeg</code> pra comprimir no pós-processo se passou de 2% de tolerância, com teto de 1.20×. A combinação do <code>speed</code> nativo (1.15×) com o <code>atempo</code> (1.20×) dá uma compressão efetiva de até 1.38×, suficiente pra caber naturalmente mesmo nos casos mais extremos sem quebrar qualidade.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># depois de gerar o chunk</span>
</span></span><span class="line"><span class="cl"><span class="n">actual</span> <span class="o">=</span> <span class="n">ffprobe_duration</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ratio</span> <span class="o">=</span> <span class="n">actual</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">ratio</span> <span class="o">&gt;</span> <span class="n">FIT_TOLERANCE</span><span class="p">:</span>       <span class="c1"># 1.02</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># comprime o áudio sem mexer no pitch</span>
</span></span><span class="line"><span class="cl">    <span class="n">ffmpeg_atempo</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">ratio</span><span class="p">,</span> <span class="n">FIT_MAX_ATEMPO</span><span class="p">)</span>   <span class="c1"># 1.20</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply_atempo</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">,</span> <span class="n">ffmpeg_atempo</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando um chunk gerado fica mais curto que a janela alvo, o pipeline estende o áudio levemente (sem afetar pitch) pra reduzir o silêncio feio depois que ele termina. O objetivo não é preencher a janela inteira, uma pausa natural entre frases é boa, mas suavizar os casos mais gritantes de silêncio que dariam a impressão de &ldquo;áudio cortado&rdquo;.</p>
<p>Num episódio grande, 95 minutos, 194 chunks, o desvio cumulativo total ficou em -0,7%. Uns 43 segundos de drift ao longo do episódio inteiro, imperceptível enquanto você assiste.</p>
<p>O que salvou o orçamento nessa arquitetura é que cada chunk fica salvo individualmente no disco como um <code>.mp3</code> separado. A pipeline mantém um manifesto com o texto normalizado de cada chunk, e antes de chamar a API da ElevenLabs, compara o texto atual com o cache. Se o texto não mudou, reutiliza o áudio já gerado sem gastar crédito nenhum. Se eu reescrevo uma cue problemática, só os chunks afetados por aquela cue são regenerados — o resto do episódio fica intacto.</p>
<p>Isso é o que viabilizou as iterações. Eu rodava o batch, escutava trechos, identificava um problema (tradução longa demais, snippet de código que o TTS não conseguia pronunciar, chunking que cortou num ponto ruim), corrigia o SRT ou ajustava os parâmetros do chunker, e rodava de novo. Cada re-run consumia uma fração dos créditos e do tempo do run original, porque só os chunks alterados eram regenerados. Sem esse cache, cada iteração teria custado quase o mesmo que a primeira rodada, e o custo total do batch teria sido duas ou três vezes maior.</p>
<p>Essa mudança de chunking também teve impacto pesado no cache e no custo, mas deixo essa parte pro trecho mais abaixo onde explico a sequência de retrabalho. Aqui basta dizer que, do ponto de vista técnico, era a mudança certa.</p>
<h3>Quando o <code>atempo</code> não salva: reescrevendo cue antes do TTS<span class="hx:absolute hx:-mt-20" id="quando-o-atempo-não-salva-reescrevendo-cue-antes-do-tts"></span>
    <a href="#quando-o-atempo-n%c3%a3o-salva-reescrevendo-cue-antes-do-tts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nem tudo é questão de alinhamento de janela. Tem um tipo de cue que quebra o pipeline inteiro, onde nem <code>speed</code> preventivo nem <code>atempo</code> no pós-processo resolvem, e eu só esbarrei nisso quando comecei a rodar o batch nos episódios mais técnicos do canal.</p>
<p>O problema é o seguinte. O v3 lê a uns 16 caracteres por segundo quando você entrega texto normal de conversa. Mas quando você entrega uma cue recheada de URL literal, hash hexadecimal, string binária, uma lista de números ordenados ou um bloco de código shell, o modelo entra em modo &ldquo;soletrar letra por letra&rdquo; e despenca pra uns 9 caracteres por segundo. Uma cue de 500 caracteres que devia virar 30 segundos de áudio vira 55. A checagem de sanidade derruba (porque passou de 1.8×), o retry automático tenta de novo com os mesmos 500 caracteres, os cinco retries falham seguidos, e o chunk fica preso.</p>
<p>E isso não aconteceu por acaso. Eu escrevia desse jeito porque sabia que esses mesmos roteiros depois virariam post no blog, então fazia sentido deixar a URL crua, o comando completo, o hash inteiro, o detalhe técnico todo mastigado pro leitor. Em português isso funcionava bem porque era exatamente o meu fluxo original: gravar o vídeo e depois publicar o texto correspondente no blog. O que eu nunca tinha preparado era a etapa seguinte, de transformar esse mesmo material em dublagem automatizada pra inglês. Eu só fui perceber esse conflito quando a pipeline começou a falhar de verdade.</p>
<p>Peguei isso primeiro no ep052, o guia de Ubuntu pra devs iniciantes, onde duas cues traziam URLs do <code>github.com</code>, URLs <code>hkp://keyserver.ubuntu.com</code> e um hash GPG de 40 caracteres. Tentar resolver no pós-processo seria perda de tempo. O teto de 1.20× do <code>atempo</code> nunca vai comprimir 55 segundos em 30, e mesmo se conseguisse ia sair num chipmunk ilegível. A saída é atacar antes, no texto.</p>
<p>A solução foi reescrever a cue dizendo o que o comando faz, em vez de mostrar o comando literal:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- Rode: apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
</span></span></span><span class="line"><span class="cl"><span class="gd">-   --recv-keys 0A6A3E7F79F93EF8AAB9E92BAEBB74C8B5A1E44D
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Rode o comando completo que tá na descrição do vídeo pra importar
</span></span></span><span class="line"><span class="cl"><span class="gi">+ a chave de assinatura do repositório do keyserver do Ubuntu.
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A legenda em inglês no vídeo continua mostrando o comando inteiro na tela, então quem lê a legenda vê o que precisa digitar. O áudio dublado descreve a intenção em linguagem falada, que é o que o TTS entrega sem engasgar. A instrução pro ouvinte fica intacta, e o caractere-por-segundo volta pros 16 esperados, então o áudio gerado cabe na janela original sem precisar esticar nem comprimir nada no pós.</p>
<p>Saí caçando cue assim em todos os episódios do batch. Varri do ep052 ao ep146 procurando cues com densidade alta de caractere &ldquo;duro&rdquo; (dígitos, colchetes, operadores, URLs longas, binário, hex) e acabei reescrevendo umas 30 cues em 13 episódios:</p>
<ul>
<li>ep052 Ubuntu pra devs: URLs do GitHub e hash GPG de 40 caracteres</li>
<li>ep091 Hello World em C: exemplos binários</li>
<li>ep095 Memória 640kB: wrap de endereço, segment-offset</li>
<li>ep106 CodeMiner: endereço de e-mail soletrado</li>
<li>ep113 Compressão: strings de binário puro, 75% de caractere duro</li>
<li>ep115 SQL Server: lista longa de números ordenados</li>
<li>ep120 Internet: endereços IP com pontos</li>
<li>ep121 Sockets: dois blocos de código JavaScript</li>
<li>ep122 Proxies: headers User-Agent do Chrome</li>
<li>ep123 Rede segura: one-liners shell, flags do Docker</li>
<li>ep126 Gentoo: demo de <code>chroot</code> em C</li>
<li>ep136 Containers: URL de release do GitHub</li>
<li>ep144 Criptografia: endereço de signing key</li>
</ul>
<p>O padrão foi sempre o mesmo: mantém a legenda exibindo o código, a URL ou o hash na íntegra, e reescreve o texto pro narrador descrever o que aquilo faz. Eu podia ter automatizado a reescrita, deixando um LLM ler a cue e propor uma substituição no voo, só que achei mais seguro revisar tudo manualmente. É exatamente o tipo de coisa onde o modelo resolve &ldquo;melhorar&rdquo; um comando pra deixá-lo mais limpo e sai com shell errado. A varredura manual levou umas duas horas. A automática teria custado o mesmo tempo em revisão, com o bônus da ansiedade de ter deixado passar algum comando mutilado.</p>
<p>Teve também um caso estrutural esquisito no ep146, sobre Docker Compose. Duas cues ficaram grudadas por causa de uma linha em branco faltando no SRT, e o <code>pysrt</code> tratava as duas como uma única cue gigante. O TTS nem chegava a processar, o chunker engasgava antes. Corrigi à mão adicionando a linha em branco que faltava. Fix de um caractere, meia hora pra rastrear.</p>
<h3>Reconstruindo SRTs truncadas<span class="hx:absolute hx:-mt-20" id="reconstruindo-srts-truncadas"></span>
    <a href="#reconstruindo-srts-truncadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Outra armadilha apareceu quando fui rodar o batch completo: três episódios tinham legendas em inglês curtas demais em relação à duração real do vídeo. O ep056, sobre Rails, era o caso mais absurdo. Dezenove minutos de legenda pra um vídeo de 80 minutos. Sessenta e dois minutos de conteúdo sem legenda nenhuma. O ep057 (WSL 2) tinha 14 minutos faltando, e o ep068 (Git Direito) tinha 2.</p>
<p>Isso é rastro do meu fluxo de tradução antigo. Em algum momento eu comecei a revisar a legenda manualmente, parei no meio, e o arquivo <code>.en.srt</code> ficou salvo truncado no ponto onde eu parei. Não dá pra dublar um vídeo de 80 minutos com legenda de 19. O chunker gera áudio até onde consegue ler e simplesmente não sabe o que fazer com o resto do vídeo.</p>
<p>A solução virou um script novo. Ele faz diff entre o <code>.en.srt</code> truncado e o <code>.pt-orig.srt</code> (o auto-caption bruto do YouTube, que sempre cobre o vídeo inteiro), pega as cues que existem só em português, manda pro Claude Sonnet 4.6 com um schema JSON rígido pedindo tradução cue a cue, e cola o resultado de volta no fim do arquivo em inglês. O schema é o detalhe que importa. Quando eu tentei pedir a resposta em texto no formato <code>N|texto</code>, o Claude deixava cair umas 20% das cues no caminho. Com um schema JSON estrito, a taxa de drop caiu pra zero nos três reparos.</p>
<p>O resultado em números:</p>
<ul>
<li>ep068 Git Direito: +50 cues / 2 minutos reconstruídos</li>
<li>ep057 WSL 2: +368 cues / 14 minutos</li>
<li>ep056 Rails: +1445 cues / 62 minutos (e ainda drop de 5 cues falsas que o tradutor antigo tinha inventado no fim pra tampar a saída)</li>
</ul>
<p>Depois dos reparos, os três episódios entraram no batch normalmente e foram dublados como qualquer outro. Tipo de ferramenta que você espera nunca precisar, só que quando precisa, compensa escrever uma vez e resolver o problema inteiro de uma vez.</p>
<h3>Tags de emoção automáticas<span class="hx:absolute hx:-mt-20" id="tags-de-emoção-automáticas"></span>
    <a href="#tags-de-emo%c3%a7%c3%a3o-autom%c3%a1ticas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Enquanto eu tava testando, resolvi adicionar mais um passo no pipeline: um &ldquo;emotion tagger&rdquo; que lê o SRT em inglês antes de mandar pro TTS e insere tags tipo <code>[sarcastic]</code>, <code>[thoughtful]</code>, <code>[emphatic]</code>, <code>[deadpan]</code> em pontos onde um narrador humano naturalmente mudaria o tom. A ideia é imitar o que um ator profissional faria, colocar ênfase nos momentos certos, sem transformar o vídeo num teatro de emoções exageradas.</p>
<p>Essa parte é delicada por dois motivos. O primeiro: deixar um LLM solto editando o SRT tem risco real de ele &ldquo;melhorar&rdquo; o texto (trocar uma palavra aqui, reescrever uma frase ali) e você acabar com uma dublagem que não bate com a legenda. O segundo: LLM adora exagerar. Se você pedir pra marcar emoção, ele vai colocar tag em toda segunda frase. Pra evitar os dois, eu fixei uma lista pequena de tags permitidas e rodo uma validação de round-trip depois que a resposta do Claude chega:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ALLOWED_TAGS</span><span class="p">:</span> <span class="nb">frozenset</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="nb">frozenset</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sarcastic]&#34;</span><span class="p">,</span> <span class="s2">&#34;[thoughtful]&#34;</span><span class="p">,</span> <span class="s2">&#34;[emphatic]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[deadpan]&#34;</span><span class="p">,</span> <span class="s2">&#34;[serious]&#34;</span><span class="p">,</span> <span class="s2">&#34;[amused]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sighs]&#34;</span><span class="p">,</span> <span class="s2">&#34;[exasperated]&#34;</span><span class="p">,</span> <span class="s2">&#34;[confident]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[matter-of-fact]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_tagged</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Garante que o SRT com tags preserva o original sem drift.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">original</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="s2">&#34;cue count mismatch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">o</span><span class="p">,</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># O index e o timestamp têm que bater byte a byte.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">o</span><span class="o">.</span><span class="n">index</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">index</span> <span class="ow">or</span> <span class="n">o</span><span class="o">.</span><span class="n">timestamp</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">timestamp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: header drift&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Qualquer tag fora da whitelist invalida tudo.</span>
</span></span><span class="line"><span class="cl">        <span class="n">bad</span> <span class="o">=</span> <span class="n">find_disallowed_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">bad</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">bad</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># O texto sem as tags tem que ser idêntico ao original.</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Se o LLM trocou uma palavra, reescreveu algo, a</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># validação quebra e a resposta é rejeitada.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> <span class="o">!=</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: text drift&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando a validação quebra, a pipeline ignora a resposta e reemite o request (ou, se falhar muito, sobe o SRT sem tags mesmo). Sobre a quantidade de tags, o prompt que vai pro Claude diz explicitamente: mira em <code>N/10</code> tags pra um SRT de N legendas, chão de 1 tag a cada 12, teto de 1 tag a cada 7, distribuição equilibrada entre os quatro quartos do episódio, nunca duas tags adjacentes a menos de 4 legendas uma da outra. Esse conjunto de regras foi a sexta iteração — as cinco anteriores ou enchiam tudo de tag ou concentravam no começo do vídeo.</p>
<h3>Não deixe a v3 alucinar<span class="hx:absolute hx:-mt-20" id="não-deixe-a-v3-alucinar"></span>
    <a href="#n%c3%a3o-deixe-a-v3-alucinar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Um detalhe crítico que só aparece em produção: a ElevenLabs v3 tende a alucinar quando você manda blocos de texto muito longos. Já peguei blocos onde o texto tinha uns 1.500 caracteres e o modelo gerou um áudio com <strong>nove minutos</strong> quando o esperado era um minuto e meio. O modelo simplesmente resolve continuar falando sozinho, inventando conteúdo.</p>
<p>A documentação oficial da ElevenLabs recomenda manter cada chamada abaixo de 800 caracteres pra evitar isso. Eu fui mais conservador e cortei em 700, sempre em fim de frase (nunca no meio de uma sentença, porque o corte no meio soa horrível quando concatena). Depois disso, subi o parâmetro <code>stability</code> pra 0.9 (modo Robust, mais estável porém menos responsivo a tags de emoção), liguei o <code>apply_text_normalization</code> pra o modelo pronunciar números e siglas direito, e adicionei uma verificação de sanidade que rejeita qualquer áudio gerado que passe de 1.8× da duração esperada.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DEFAULT_VOICE_SETTINGS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stability&#34;</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>          <span class="c1"># modo Robust</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;similarity_boost&#34;</span><span class="p">:</span> <span class="mf">0.95</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;style&#34;</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;use_speaker_boost&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">EXPECTED_CHARS_PER_SEC</span> <span class="o">=</span> <span class="mf">16.0</span>
</span></span><span class="line"><span class="cl"><span class="n">CHUNK_SANITY_MAX_FACTOR</span> <span class="o">=</span> <span class="mf">1.8</span>   <span class="c1"># rejeita se actual &gt; 1.8× esperado</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois dessas mitigações, um episódio de 95 minutos (194 blocos) rodou do começo ao fim sem nenhuma alucinação. Exatamente um bloco falhou por erro 502 transitório da API, que o retry automático pegou na tentativa seguinte.</p>
<h3>Masterização pro YouTube<span class="hx:absolute hx:-mt-20" id="masterização-pro-youtube"></span>
    <a href="#masteriza%c3%a7%c3%a3o-pro-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O YouTube não publica número oficial, mas o consenso da indústria é que a normalização de volume dele na hora da reprodução mira em -14 LUFS. Se você entregar áudio mais alto que isso, o YouTube baixa; se entregar mais baixo, ele deixa como tá. Pra bater o alvo exato, o pipeline roda o filtro <code>loudnorm</code> do <code>ffmpeg</code> em duas passadas. A primeira mede o áudio inteiro e imprime as estatísticas (<code>input_i</code>, <code>input_tp</code>, <code>input_lra</code>, <code>input_thresh</code>) em formato JSON no stderr. A segunda passada lê essas estatísticas de volta e aplica um ganho estático linear pra pousar exato em -14 LUFS, -1.5 dBTP:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Passada 1: medir</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9:print_format=json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f null -
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Passada 2: aplicar ganho linear com os valores medidos</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_I=-18.3:measured_TP=-3.1:measured_LRA=5.4\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_thresh=-28.7:offset=1.2:linear=true&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -ar <span class="m">48000</span> -ac <span class="m">2</span> -c:a pcm_s16le final_en_mastered.wav</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Duas passadas em vez de uma porque o modo de passada única roda em compressão dinâmica e acaba &ldquo;bombeando&rdquo; o ganho em trechos com muito silêncio. Com <code>linear=true</code> na passada 2, o ganho fica estático em cima das medições da passada 1, então não tem bombeamento. O resultado pousa dentro de ±0.1 LU do alvo, que é praticamente inaudível. O filtro <code>highpass=f=80</code> na frente derruba rumble de ar-condicionado e hum de rede elétrica abaixo de 80 Hz, que o ouvido humano não escuta mas mexe nas medições de pico.</p>
<h3>O ponto em que eu achei que estava pronto. E não estava.<span class="hx:absolute hx:-mt-20" id="o-ponto-em-que-eu-achei-que-estava-pronto-e-não-estava"></span>
    <a href="#o-ponto-em-que-eu-achei-que-estava-pronto-e-n%c3%a3o-estava" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui entra a parte mais chata, e mais educativa, do projeto.</p>
<p>Eu realmente achei que tinha acertado a mão na primeira grande rodada. A pipeline fechou os 146 episódios, eu subi tudo, e parecia resolvido. A duração batia, os arquivos estavam todos lá, o processo inteiro tinha finalmente atravessado o lote completo de quase 96 horas de vídeo. Só que foi justamente depois do upload que comecei a perceber problemas que eu simplesmente não tinha antecipado.</p>
<p>O primeiro susto foi a situação dos jingles. Eu já sabia que muitos episódios do Akitando começam com aquele jingle instrumental curto antes de eu começar a falar, mas subestimei o problema. O <code>.srt</code> do YouTube praticamente não ajuda nisso, porque jingle não é fala. Às vezes ele marca como <code>[Música]</code>, às vezes não marca nada útil, às vezes a janela fica torta. Na primeira montagem, teve episódio onde o jingle sumiu, episódio onde ele entrou duplicado, e episódio onde o source-fill do áudio original já trazia o jingle certo e o meu splice vinha por cima, empurrando todo o resto pra frente. Foi o tipo de bug que só aparece quando você para pra escutar a master final de verdade, não quando olha só pra duração total.</p>
<p>A solução foi abandonar a esperança de que a legenda me diria onde estava o jingle e ir direto pra fonte. Montei um detector usando os próprios áudios originais dos vídeos e jingles de referência, com auditoria e repair pass em cima dos masters já gerados. Isso acabou virando um mini-sistema próprio: detector, auditoria, reassemble por cache, correção por janela exata. No fim dessa novela, o batch de episódios alterados caiu num conjunto de reupload bem definido e o problema dos jingles ficou finalmente sob controle. Foi um daqueles casos clássicos de produção: a primeira solução parecia suficiente até bater no acervo inteiro.</p>
<p>Só que aí veio a segunda pancada, e essa foi mais cara: eu tinha esquecido de um detalhe fundamental sobre legendas de YouTube. SRT é feita pra ser lida na tela, não pra virar roteiro de fala. Então uma ideia que no meu texto original era um parágrafo inteiro aparecia quebrada em várias cues pequenas, cada uma com sua microjanela de tempo. Na primeira versão da pipeline eu estava usando essas cues como unidade de geração na ElevenLabs. Resultado: o TTS gerava áudio em blocos coerentes localmente, mas a montagem introduzia silêncios artificiais no meio da frase. Tecnicamente alinhado, humanamente esquisito.</p>
<p>O conserto foi doloroso porque exigiu voltar várias casas. Em vez de respeitar a estrutura das cues do SRT, precisei voltar pros meus textos originais e reconstruir o chunking com base nos parágrafos que eu realmente li quando gravei os vídeos. Quase todo episódio do Akitando tem o post correspondente com a seção <code>## Script</code>, e isso salvou o projeto. Como eu fui meticuloso na época em que produzi os vídeos — escrevendo os roteiros antes, lendo exatamente aquilo na gravação e arquivando tanto os vídeos quanto os scripts — eu tinha a fonte de verdade. Então deu pra mapear os parágrafos do script aos timings do <code>.pt-BR.srt</code>, reagrupar o <code>.en.srt</code> em cima disso e regenerar os chunks no formato certo.</p>
<p>O problema é que essa mudança destrói o cache anterior. Mesmo quando o texto quase não muda, se a fronteira do chunk muda, a chamada ao TTS muda junto: muda contexto, muda prosódia, muda começo, muda fim. Então não era um retoquezinho. Era uma regeneração completa de tudo de novo. Foi aí que o custo começou a sair do meu controle inicial.</p>
<p>E quando eu achei que agora sim tinha terminado, veio a terceira paulada: os clipes externos. Muitos vídeos do canal têm trechos inseridos de outras fontes. No episódio de Ruby on Rails, por exemplo, tem 37signals, Apple, comerciais, pedaços externos no meio da narrativa. O <code>.srt</code> do YouTube não sabia lidar direito com isso. Em alguns pontos ele simplesmente alucinava texto, em outros os timings ficavam completamente tortos, em outros a fala original não tinha nada a ver com a legenda que eu estava usando de base. De novo: olhando só pra automação parecia tudo bem; escutando a master final, não estava.</p>
<p>A correção, mais uma vez, foi sair da legenda e voltar pro material original. Montei um detector de janelas de clipe externo a partir de gaps de alinhamento entre transcript, <code>.pt-BR.srt</code>, cues sem fala útil e chunks que atravessavam essas janelas. Depois, em vez de tentar dublar o que não fazia sentido dublar, o processo passou a recuperar o áudio original do vídeo exatamente nesses trechos e preencher a master com o source correto. Foi isso que consertou os casos de 37signals, Apple Education, comercial de Mac vs PC e outros inserts parecidos que eu tinha esquecido que existiam.</p>
<p>Esse foi o aprendizado mais importante dessa segunda metade do projeto: automação te leva muito longe, mas o acervo completo sempre tem mais cantos escuros do que você imagina no primeiro passe.</p>
<h2>Os números do batch de dublagem (e por que doeu mais do que eu imaginei)<span class="hx:absolute hx:-mt-20" id="os-números-do-batch-de-dublagem-e-por-que-doeu-mais-do-que-eu-imaginei"></span>
    <a href="#os-n%c3%bameros-do-batch-de-dublagem-e-por-que-doeu-mais-do-que-eu-imaginei" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu preciso me corrigir de novo. Não errei só a primeira conta. Errei também o grau de confiança que eu tinha depois da primeira rodada.</p>
<p>Quando subi pro plano Business da ElevenLabs, eu achei que finalmente estaria com folga. Na prática, não fiquei. O screenshot mais recente do dashboard mostra <strong>10.046.279 créditos consumidos de um limite de 11.000.000</strong>, sobrando <strong>953.721</strong>:</p>
<p><img src="elevenlabs-credits-dashboard.png" alt="Dashboard da ElevenLabs mostrando 10.046.279 créditos consumidos de 11.000.000"  loading="lazy" /></p>
<p>Em português claro: eu torrei mais de 91% do limite do workspace Business. E isso ajuda a entender o que &ldquo;10 milhões de créditos&rdquo; realmente significa nesse contexto. Não é só &ldquo;renderizei 146 episódios uma vez&rdquo;. É renderizar 146 episódios, descobrir um problema estrutural, voltar, corrigir, regenerar chunks, descobrir outro problema estrutural, voltar, reconstruir masters, auditar jingles, reparar janelas de clipes externos, reupar de novo. Cada rodada a mais custa. E quando a mudança invalida o cache, custa muito.</p>
<p>O custo também não para na ElevenLabs. Todo o trabalho de curadoria das legendas, reescrita de cues problemáticas, reconstrução de SRTs truncadas, auditorias de jingle, detector de clipes externos e o resto rodou via Claude Code no plano Claude Max 20×. Somando isso com a tradução em massa do blog pra inglês que eu descrevi no <a href="/2026/04/09/20-anos-de-blog-o-ano-em-que-a-ia-finalmente-me-deixou-traduzir-tudo/">artigo de aniversário</a>, o limite semanal do Claude Max 20× bateu 100%, com gasto extra acima de R$ 300. Pra quem acha que IA generativa vira grátis depois que você assinou o plano, os boletos discordam.</p>
<p>Mesmo assim, continuo achando que vale a pena. Estamos falando de 146 episódios, quase 96 horas de conteúdo técnico, mais de 5 anos de acervo do canal. Fazer isso manualmente em estúdio, com ator, direção, pickup e revisão humana em tudo, custaria uma ordem de grandeza completamente diferente.</p>
<p>O que mudou foi minha visão do projeto. No começo eu tava olhando pra isso como &ldquo;um batch grande&rdquo;. No fim, era outra coisa. Era mexer num acervo inteiro, achar exceção escondida, consertar ferrugem antiga, refazer master, rerender, reouvir.</p>
<p>O primeiro batch completo levou pouco menos de dois dias e me fez pensar que eu tinha resolvido tudo. Não tinha. A segunda rodada, com chunking por parágrafo, já foi cara. A terceira, com os reparos de jingle e os consertos de masters, mais cara ainda. E a quarta pancada veio dos clipes externos que eu não tinha modelado direito na primeira automação.</p>
<p>Mas agora, finalmente, eu acho que acabou. Ou melhor: eu espero que tenha acabado. Tudo foi enviado de novo, pela última vez. Não está perfeito no sentido absoluto da palavra. Está tão bom quanto eu consigo deixar com script, auditoria e automação, sem entrar num inferno de intervenção manual vídeo por vídeo.</p>
<p>E se isso tudo foi possível, é porque anos atrás eu tive a disciplina de produzir os vídeos do jeito certo: escrever primeiro, ler o roteiro quando gravava, e arquivar os materiais depois. Sem os vídeos originais e sem os scripts originais, eu não teria como descobrir onde o <code>.srt</code> mentia, onde o timing estava torto, onde o clipe externo entrava, onde o parágrafo original começava e terminava. Essa organização antiga foi o que permitiu consertar o projeto agora.</p>
<p>Então sim: a dublagem dos 146 episódios está pronta. Não perfeita. Mas fechada. Pelo menos, eu sinceramente espero que agora sim esteja.</p>
<h2>O primeiro teste, assista<span class="hx:absolute hx:-mt-20" id="o-primeiro-teste-assista"></span>
    <a href="#o-primeiro-teste-assista" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fiz um vídeo teste pra provar que funciona. Dois avisos antes: primeiro, o embed abaixo já carrega com a legenda em inglês ligada por padrão, então esse lado dá pra resolver via URL. Segundo, o áudio é chato: o YouTube removeu o seletor de faixa de áudio dos players embedados lá por março desse ano. Então dentro do embed você não vai encontrar essa opção no menu de engrenagem mesmo, só roda em português. Pra ouvir a dublagem em inglês de verdade, clica em <strong><a href="https://www.youtube.com/watch?v=QNLd8TZ_JQc"target="_blank" rel="noopener">assistir direto no YouTube</a></strong>, que a página principal do YouTube ainda tem o seletor de faixa de áudio no menu de engrenagem.</p>
<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/QNLd8TZ_JQc?cc_load_policy=1&amp;cc_lang_pref=en&amp;hl=en"
    title="Akitando dublado em inglês (teste)"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
<p>Se você tá acostumado com a dublagem automática do YouTube ou com as dublagens de IA de TikTok, compara. A diferença é gritante. A voz é a minha, com sotaque americano trabalhado, e a sincronização com o vídeo original fica dentro de 1 a 2% de desvio cumulativo, praticamente imperceptível em 95 minutos de vídeo.</p>
<h2>E o que faltava: traduzir as thumbnails<span class="hx:absolute hx:-mt-20" id="e-o-que-faltava-traduzir-as-thumbnails"></span>
    <a href="#e-o-que-faltava-traduzir-as-thumbnails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando eu estava fechando esse post, me caiu a ficha de que tinha deixado uma ponta solta. As 146 thumbnails do meu canal estão em português, muitas com título em caixa alta grande tipo &ldquo;9 DICAS PARA PALESTRANTES&rdquo; ou &ldquo;7 RECOMENDAÇÕES DE SHOWS PARA PESSOAS DE TECH&rdquo;. De nada adianta áudio em inglês perfeito se a imagem que aparece na busca é um bloco de texto ilegível pra quem não lê português. O YouTube permite subir uma thumbnail alternativa por idioma, então eu precisava gerar versões em inglês, mantendo o resto da arte idêntico, trocando só o texto.</p>
<p>A ferramenta usa duas peças. <strong><code>yt-dlp</code></strong> é o fork moderno do antigo <code>youtube-dl</code>, uma CLI em Python que baixa qualquer coisa do YouTube (vídeos, áudios, legendas, thumbnails) sem precisar de API key. <strong>Nano Banana Pro</strong> (<code>nano-banana-pro-preview</code> na API) é o modelo de edição de imagem mais recente do Google Gemini — aceita uma imagem de entrada com um prompt e devolve a imagem editada, preservando o resto da composição quando você pede pra mexer só numa parte.</p>
<p>O pipeline tem dois passos. Passo 1: <code>yt-dlp</code> pega a thumbnail em <code>.jpg</code>, sem baixar o vídeo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yt-dlp --skip-download <span class="se">\
</span></span></span><span class="line"><span class="cl">       --write-thumbnail <span class="se">\
</span></span></span><span class="line"><span class="cl">       --convert-thumbnails jpg <span class="se">\
</span></span></span><span class="line"><span class="cl">       -o <span class="s2">&#34;thumbnails/originals/&lt;slug&gt;/&lt;video_id&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">       <span class="s2">&#34;https://www.youtube.com/watch?v=&lt;video_id&gt;&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Passo 2: mandar cada imagem pro Gemini Nano Banana Pro com um prompt que é um contrato rígido de requisitos duros, não só um &ldquo;traduz isso&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">TASK:
</span></span><span class="line"><span class="cl">Detect every piece of Portuguese text visible on this image and
</span></span><span class="line"><span class="cl">translate it to clear, natural American English. Replace the
</span></span><span class="line"><span class="cl">Portuguese text with the English translation in the same visual
</span></span><span class="line"><span class="cl">position.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">STRICT REQUIREMENTS — follow every one:
</span></span><span class="line"><span class="cl">- Preserve EVERY non-text element identically: face, pose,
</span></span><span class="line"><span class="cl">  expression, background, color palette, lighting, icons, logos,
</span></span><span class="line"><span class="cl">  decorative shapes, borders, layout. Only the text changes.
</span></span><span class="line"><span class="cl">- The English translation must be IDIOMATIC and CONFIDENT — not
</span></span><span class="line"><span class="cl">  a literal word-for-word rewrite. It&#39;s a YouTube thumbnail for a
</span></span><span class="line"><span class="cl">  tech audience, so use punchy phrasing a native English-speaking
</span></span><span class="line"><span class="cl">  tech YouTuber would write.
</span></span><span class="line"><span class="cl">- Match the original text&#39;s font family, weight, size, color,
</span></span><span class="line"><span class="cl">  stroke outline, drop shadow, and any decorative treatment.
</span></span><span class="line"><span class="cl">- If there is no Portuguese text at all on the image, return the
</span></span><span class="line"><span class="cl">  original image unchanged.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A cláusula &ldquo;return the original image unchanged&rdquo; é o que salva os primeiros episódios do canal, que não têm texto na thumbnail, só minha cara. Sem ela, o modelo invariavelmente ia &ldquo;melhorar&rdquo; a composição, trocar a iluminação, etc.</p>
<p>Olha o resultado em dois exemplos (episódios 10 e 11):</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_pt.jpg" alt="Thumbnail original em português: 7 Recomendações de Shows para pessoas de Tech" style="width: 100%;"></div>
<div><strong>Traduzida (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_en.jpg" alt="Thumbnail traduzida em inglês: 7 TV Shows You Must Watch If You're In Tech" style="width: 100%;"></div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_pt.jpg" alt="Thumbnail original em português: 9 Dicas para Palestrantes: venda sua caneta" style="width: 100%;"></div>
<div><strong>Traduzida (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_en.jpg" alt="Thumbnail traduzida em inglês: 9 Tips for Speakers: sell your pen" style="width: 100%;"></div>
</div>
<p>Repara no detalhe do episódio 10. O modelo não traduziu &ldquo;7 Recomendações de Shows para Pessoas de Tech&rdquo; literal pra &ldquo;7 Show Recommendations for Tech People&rdquo;, que seria a versão tradutor automático. Ele reformulou pra &ldquo;7 TV Shows You Must Watch If You&rsquo;re In Tech&rdquo;, como um tech YouTuber americano de verdade escreveria. O resto da imagem fica idêntico pixel a pixel. No episódio 11, &ldquo;9 DICAS PARA PALESTRANTES: venda sua caneta&rdquo; vira &ldquo;9 TIPS FOR SPEAKERS: sell your pen&rdquo;, com fonte, caixa alta e posição preservadas. Se você não soubesse que o original era em português, não dava pra adivinhar que é uma edição automática feita por IA.</p>
<p>Custo do lado Gemini: alguns centavos por imagem, poucos dólares totais pro batch inteiro. Trivial perto dos $1.500+ do dub. Com a thumbnail resolvida, a conversão do canal pra inglês fica fechada — áudio clonado pela ElevenLabs v3 em cima das legendas curadas em <code>.srt</code>, e a imagem editada pelo Nano Banana Pro pra o título bater com o resto.</p>
<h2>A conclusão, que é o título desse post<span class="hx:absolute hx:-mt-20" id="a-conclusão-que-é-o-título-desse-post"></span>
    <a href="#a-conclus%c3%a3o-que-%c3%a9-o-t%c3%adtulo-desse-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando o Qwen3 TTS saiu, o hype dizia que era &ldquo;ElevenLabs killer&rdquo;. Passei semanas tentando viver com essa premissa na prática, com conteúdo real no ar toda semana. E o que eu descobri é que open source de TTS ainda tá muito longe da ElevenLabs. A diferença é grande, e ela aparece justamente quando você sai do tweet de demo de 5 segundos e coloca o modelo pra rodar um podcast semanal de 30 minutos de verdade.</p>
<p>Na prática, o Qwen3 nem bate o modelo v2 da ElevenLabs. O v3, que é o atual, fica um degrau acima do v2 ainda. A prosódia sai melhor, as tags de emoção no meio do texto funcionam em português e em inglês sem esforço, e a API fica de pé sem você precisar manter servidor nenhum. O custo por caractere é um pouco mais alto, mas pro meu volume atual ele fica confortavelmente dentro do orçamento e me devolve horas que eu gastava cuidando de GPU.</p>
<p>A lição aqui é a mesma que a galera de LLMs tá aprendendo devagar. Demo de tweet de 30 segundos é uma coisa, produção é outra coisa completamente diferente. Open source tem seu nicho, principalmente quando você tem dado sensível que não pode sair de casa, ou quando você tem muita GPU ociosa e pouco orçamento recorrente. Só que pra uso sério de TTS em produto comercial, aqui em abril de 2026, a ElevenLabs segue imbatível. O Qwen3 não matou ninguém.</p>
<p>E não esquece: assina o <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">The M.Akita Chronicles no Spotify</a> pra não perder episódio novo desses, feito com a nova pipeline.</p>
]]></content:encoded><category>ai</category><category>llm</category><category>tts</category><category>elevenlabs</category><category>qwen</category><category>themakitachronicles</category></item><item><title>20 Anos de Blog: Traduzindo Tudo pra Inglês</title><link>https://www.akitaonrails.com/2026/04/09/20-anos-de-blog-o-ano-em-que-a-ia-finalmente-me-deixou-traduzir-tudo/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/09/20-anos-de-blog-o-ano-em-que-a-ia-finalmente-me-deixou-traduzir-tudo/</guid><pubDate>Thu, 09 Apr 2026 08:00:00 GMT</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_20years_blog_celebration.png" alt="20 ANOS em letras grandes brilhantes em estilo neon-tech, com taças de champanhe, sparklers, confete e silhuetas de monitor CRT e código no fundo, paleta roxa com acentos ciano e magenta" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Quatro dias atrás, 5 de abril de 2026, meu blog completou 20 anos. Eu sei, eu atrasei o post comemorativo. Mas tem um motivo, e esse motivo é o assunto desse texto.&lt;/p&gt;
&lt;p&gt;Eu comecei em 2006 no Blogspot do Google, como a maioria fazia na época. Depois migrei pra um CMS em Rails 2.0 que já existia, passei pelo Typo3, e lá em 2012 &lt;a href="https://www.akitaonrails.com/2025/09/10/meu-novo-blog-como-eu-fiz/"&gt;fiz minha própria engine do zero em ActiveAdmin&lt;/a&gt;, que fui arrastando de Rails 3 até Rails 7. Só recentemente, em setembro de 2025, finalmente larguei essa engine própria e fui pro &lt;a href="https://www.akitaonrails.com/2025/09/10/meu-novo-blog-como-eu-fiz/"&gt;Hugo com o tema Hextra&lt;/a&gt;, que é onde esse post está rodando agora. Vinte anos carregando os mesmos posts de Textile, migrando de Less pra Sass, trocando Liquid por Markdown, e removendo lixo obsoleto ao longo do caminho.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_20years_blog_celebration.png" alt="20 ANOS em letras grandes brilhantes em estilo neon-tech, com taças de champanhe, sparklers, confete e silhuetas de monitor CRT e código no fundo, paleta roxa com acentos ciano e magenta"  loading="lazy" /></p>
<p>Quatro dias atrás, 5 de abril de 2026, meu blog completou 20 anos. Eu sei, eu atrasei o post comemorativo. Mas tem um motivo, e esse motivo é o assunto desse texto.</p>
<p>Eu comecei em 2006 no Blogspot do Google, como a maioria fazia na época. Depois migrei pra um CMS em Rails 2.0 que já existia, passei pelo Typo3, e lá em 2012 <a href="/2025/09/10/meu-novo-blog-como-eu-fiz/">fiz minha própria engine do zero em ActiveAdmin</a>, que fui arrastando de Rails 3 até Rails 7. Só recentemente, em setembro de 2025, finalmente larguei essa engine própria e fui pro <a href="/2025/09/10/meu-novo-blog-como-eu-fiz/">Hugo com o tema Hextra</a>, que é onde esse post está rodando agora. Vinte anos carregando os mesmos posts de Textile, migrando de Less pra Sass, trocando Liquid por Markdown, e removendo lixo obsoleto ao longo do caminho.</p>
<h2>Duas décadas, cinco eras<span class="hx:absolute hx:-mt-20" id="duas-décadas-cinco-eras"></span>
    <a href="#duas-d%c3%a9cadas-cinco-eras" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que poucos programadores param pra refletir é o quanto o mundo de tecnologia muda em 20 anos. Eu já falei sobre isso no <a href="/2019/01/30/akitando-37-a-dimensao-do-tempo-para-iniciantes-em-programacao-serie-comecando-aos-40/">Akitando #37 - A Dimensão do Tempo</a>. Quando eu abri esse blog em abril de 2006, eu já tinha 10 anos de experiência. Já tinha visto a bolha da internet subir e explodir em 2000. E desde então ainda vi mais umas quantas: a ascensão das redes sociais (Orkut, Facebook, Twitter), a revolução do smartphone e do app móvel em 2008, a crise econômica de 2008, o nascimento do Bitcoin em 2009, a era da nuvem e do SaaS, agora a era da IA generativa.</p>
<p>Eu vi essas ondas estourarem na prática. Cada uma delas mudou completamente como a gente trabalha e quem sobrevive profissionalmente. Esse tipo de coisa a gente só enxerga olhando de longe, depois de acumular umas quantas viradas.</p>
<p>Minha carreira em si passou por viradas drásticas. Contei em detalhe nos <a href="/2019/09/12/akitando-61-meus-primeiros-5-anos-1990-1995/">Primeiros 5 Anos (1990-1995)</a>, mas resumindo: comecei em agências de multimídia, migrei pra <a href="/2018/12/26/akitando-34-voce-nao-sabe-nada-de-enterprise-conhecendo-a-sap/">consultoria enterprise trabalhando com SAP</a>, abandonei tudo em 2006 pra entrar em Ruby on Rails e open source, passei uma década organizando evento, principalmente a Rubyconf Brasil de 2007 até 2018, depois fechei a porta do evento e comecei o canal <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando no YouTube</a>, que cresceu pra mais de 500 mil inscritos. No meio disso teve a pandemia que reorganizou a vida de todo mundo. E agora, desde o começo de 2025, virei pesquisador e usuário em tempo integral de IA, rodando modelos localmente e quebrando coisas com Claude Code até elas funcionarem.</p>
<p>Olhando pra tag <a href="/tags/ai/">/tags/ai</a>, eu escrevi 51 posts relacionados a IA só entre 2025 e 2026 — 19 em 2025 e mais 32 em 2026, que mal começou. Antes de 2025, entre 2018 e 2024, o blog basicamente virou o arquivo dos transcripts dos vídeos do Akitando. Eu filmava o vídeo, fazia o transcript, jogava no blog, e as pessoas liam lá. Mas desde que eu voltei a escrever ativamente, notei que tinha uma audiência leal que nunca foi embora, mesmo nos anos quietos. E foi esse feedback que me fez começar <a href="/tags/themakitachronicles/">The M.Akita Chronicles</a>, uma série mais pessoal contando os bastidores de projetos recentes.</p>
<h2>O problema que eu nunca resolvi<span class="hx:absolute hx:-mt-20" id="o-problema-que-eu-nunca-resolvi"></span>
    <a href="#o-problema-que-eu-nunca-resolvi" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Durante esses 20 anos, uma coisa me incomodava. Como o conteúdo é todo em português, fica inacessível pra todo mundo que não lê a língua. E faz uns bons anos que eu venho recebendo mensagens de leitores brasileiros morando fora (Portugal, Estados Unidos, Japão, Alemanha, Canadá) pedindo alguma versão em inglês pra compartilhar com colegas gringos. Coisa tipo &ldquo;olha, eu mostraria esse texto pro meu time, mas só tá em português.&rdquo;</p>
<p>Eu sempre disse &ldquo;um dia eu traduzo.&rdquo; E nunca traduzi. Por quê? Porque são centenas de posts. Passei os últimos dois dias olhando os números: 727 arquivos <code>index.md</code> em português no repositório. Traduzir tudo à mão, um por um, ia tomar semanas se não meses de trabalho dedicado. Eu nunca ia ter fôlego pra isso. E a cada ano que passava, mais posts se acumulavam pra traduzir.</p>
<p>A ironia é que alguns posts do blog nasceram em inglês. Durante 2017 e 2018 eu tinha decidido escrever primeiro em inglês pra tentar alcançar audiência internacional. Uma série inteira de entrevistas, as &ldquo;<a href="/2008/01/09/conversando-com-hal-fulton/">chatting-with</a>&rdquo; com gente tipo Hal Fulton, Scott Hanselman, Chris Wanstrath (do GitHub), Blaine Cook (ex-Twitter), Adam Jacob (Chef), nasceram em inglês e ficaram lá. Faltava o inverso: pegar os posts em português e traduzir pro outro lado.</p>
<h2>O fim de semana que consertou 20 anos de dívida técnica<span class="hx:absolute hx:-mt-20" id="o-fim-de-semana-que-consertou-20-anos-de-dívida-técnica"></span>
    <a href="#o-fim-de-semana-que-consertou-20-anos-de-d%c3%advida-t%c3%a9cnica" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Na segunda passada eu abri o Claude Code dentro do diretório do blog. E pedi pra ele traduzir tudo pra inglês. Foi literalmente isso.</p>
<p>Começou na segunda-feira, dia 6 de abril, por volta das 18:30h. Eu deixei rodar. Fui dormir. Acordei terça, continuei rodando. Dormi de novo. Quarta de manhã cedo ele terminou. Contando o tempo total do primeiro commit de tradução até o último, foi algo como 39 horas. Mas não foi contínuo, teve noites, almoços, uma roda de café. Na prática, um fim de semana estendido de trabalho.</p>
<p>O resultado tá no repositório git, em commits que qualquer um pode inspecionar no <a href="https://github.com/akitaonrails/akitaonrails.github.io"target="_blank" rel="noopener">repositório público do blog</a>. Olhando rapidamente o log, dá pra ver a cadência: lote de posts de 2008 QCon. Lote de 2009 RailsConf. Lote de 2011 Objective-C. 2012 completo. 2015 série Elixir. 2016, 2017, 2018 completos. Lote depois de lote, organizados por ano e por série. Mais de 80 commits marcados como <code>i18n:</code> ou <code>EN translation:</code> entre o começo da noite do dia 6 e a manhã do dia 8. No final desse post, abri 354 arquivos <code>index.en.md</code> contra os 727 <code>index.md</code> originais. Quase metade do blog inteiro traduzido de um só golpe.</p>
<p>E olha, noventa por cento foi tranquilo. A tradução ficou boa, natural, fiel à minha voz no original — eu não imaginava que ia funcionar tão bem. Claude Code respeita o tom do texto se você der instruções boas de voz e estilo, e mostra respeito por ideias técnicas sem &ldquo;corrigir&rdquo; nada do que você escreveu. No pior caso, revisa alguns parágrafos. No melhor caso, a versão em inglês soa como se você tivesse escrito direto na outra língua.</p>
<p>Um parênteses sobre o custo do lado Claude. Eu sou assinante do plano Max 20x da Anthropic, que é o tier de uso pesado feito pra quem abusa do Claude Code o dia inteiro. Nunca tinha batido no teto dele antes, nem nas minhas sessões mais intensas de vibe coding. Esse fim de semana de tradução foi a primeira vez que eu realmente estourei o limite do Max 20x e continuei usando já no modo de &ldquo;uso extra&rdquo; (a Anthropic cobra por consumo acima do teto do plano mensal).</p>
<p>Pra ter uma ideia do estrago, esse é o painel da minha conta agora de manhã:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_claude_usage_max20x.png" alt="Plano Max 20x com 91% da quota semanal ‘all models’ consumida e R$280,88 já gastos em uso extra"  loading="lazy" /></p>
<p><strong>91% da quota semanal &ldquo;all models&rdquo; consumida</strong>, já pra dentro do modo de uso extra, R$280,88 (uns $50 USD) gastos em cima da mensalidade fixa, e ainda falta um bom pedaço do ciclo. Faz sentido: ler o post inteiro, gerar a tradução, aplicar o humanizer em cima, repetir centenas de vezes seguidas. A quantidade de tokens envolvida é massiva. E hoje, enquanto eu escrevo esse texto, o Claude Code tá me dando com frequência crescente o erro <code>API Error: Request rejected (429) · Rate limited</code>, o que também faz sentido: deve ser um combo de quota do meu plano e do Anthropic aplicando backpressure geral, porque eu não devo ser o único fazendo loucura esses dias. Ok, normal. É o preço de ter usado a ferramenta pesado quando fazia diferença.</p>
<p>Os outros dez por cento foram uma história separada.</p>
<h2>Quando os LLMs comerciais dizem &ldquo;não&rdquo;<span class="hx:absolute hx:-mt-20" id="quando-os-llms-comerciais-dizem-não"></span>
    <a href="#quando-os-llms-comerciais-dizem-n%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Meia dúzia de posts antigos se recusaram a ser traduzidos. Tanto Claude quanto GPT, pela API, caíam num erro 400 com mensagem de política de conteúdo. Tentei várias vezes, em sessões frescas, com contexto limpo. Nada.</p>
<p>A hipótese é simples: esses posts tocavam em tópicos sensíveis pra content moderation automatizado. Um post de 2009 sobre o discurso do Steve Jobs em Stanford de 2005 (que menciona câncer, já que o Jobs estava falando do diagnóstico terminal que ele tinha recebido). Dois capítulos antigos da Ayn Rand, traduzidos por mim anos atrás, sobre direitos do homem e argumentação. Um post anti-nazista que ironicamente foi bloqueado provavelmente só porque a palavra &ldquo;nazi&rdquo; aparecia lá, mesmo com o contexto sendo crítico. Um post sobre o discurso do dinheiro do Atlas Shrugged. E um ensaio sobre democracia e ética que travava sistematicamente todas as tentativas.</p>
<p>A ironia dessa parte é que o único jeito de conseguir traduzir esses posts foi usando um modelo open source. Carreguei o <a href="https://qwen.ai"target="_blank" rel="noopener">Qwen 3.5 35B</a> no llama-swap, rodando localmente, sem filtros de política corporativa. O modelo leu, entendeu o contexto, e traduziu tudo sem drama. É o mesmo modelo que eu tinha <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">testado extensivamente no meu último benchmark de LLMs</a>, e que eu avalio como um dos melhores open source disponíveis hoje.</p>
<p>Ou seja: um modelo chinês conseguiu traduzir posts que modelos ocidentais recusaram. Eu não consigo deixar de achar isso levemente hilário. Ah, claro, e não posso falar mal da China (sarcasmo). LLMs comerciais vão sempre ter o problema de política corporativa e censura preventiva aplicada com régua meio grossa. É um trade-off real pra quem quer usar eles em produção: você ganha fluência e capacidade de raciocínio, perde controle quando o tópico resvala no que a empresa decidiu que é sensível.</p>
<h2>O caso inverso: 2017-2018 traduzido pro português<span class="hx:absolute hx:-mt-20" id="o-caso-inverso-2017-2018-traduzido-pro-português"></span>
    <a href="#o-caso-inverso-2017-2018-traduzido-pro-portugu%c3%aas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Enquanto eu estava ajustando o Claude Code pra gerar o conteúdo em inglês, aproveitei pra resolver o problema simétrico. Os posts que eu tinha escrito originalmente em inglês durante 2017 e 2018, mais a série inteira de entrevistas &ldquo;chatting-with&rdquo; de 2008, estavam parados lá sem versão em português. Claude Code rodou o reverso: leu o inglês, gerou o português. Então se você não leu os originais em inglês (porque a maioria do meu público é brasileiro mesmo), agora você tem acesso também. Dá uma olhada na <a href="/off-topic/">seção Off-Topic</a> pra ver o que apareceu de novidade.</p>
<p>Junto disso, o Claude Code também atualizou o script <code>generate_index.rb</code> pra entender a estrutura bilíngue do blog e gerar dois índices separados, um em português e outro em inglês. O seletor PT/EN no rodapé de cada post aparece automaticamente quando existe o arquivo <code>index.en.md</code> irmão. Tudo bem integrado ao Hugo, seguindo o padrão nativo de multilíngue dele.</p>
<h2>O ponto mais amplo<span class="hx:absolute hx:-mt-20" id="o-ponto-mais-amplo"></span>
    <a href="#o-ponto-mais-amplo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui tá o recado que eu queria deixar nesse post de aniversário. Tradução em escala, centenas de artigos, conteúdo seu, na sua voz, respeitando o tom do original, era um problema caro e chato. Agora é um problema de fim de semana. Isso já tá no ar, você tá lendo o resultado. A barreira que existia pra alcançar audiência fora do Brasil virou algo barato o suficiente pra eu não ter mais desculpa.</p>
<p>Então, vamos lá, recapitulando o aniversário. 20 anos. 727 posts em português, construídos ao longo de cinco eras diferentes de tecnologia. 354 novos posts em inglês gerados num fim de semana. Metade do blog agora é bilíngue. E o resto vai indo. Os que faltam são principalmente posts antigos de ActiveAdmin, Rails 2 e dicas obsoletas que já faz sentido deletar ou não traduzir de propósito.</p>
<p>Se você tem amigo gringo que já cansou de ver você postando link em português e precisa de uma desculpa pra mandar alguma coisa minha, manda. Os posts principais sobre IA, os <a href="/tags/themakitachronicles/">Chronicles</a>, benchmarks, rants e boa parte do arquivo histórico já tão disponíveis em inglês no mesmo domínio, é só trocar o toggle no rodapé. Obrigado a todo mundo que acompanhou esse blog por esses vinte anos. Tem leitores aqui que me conhecem desde o Blogspot. Vocês sabem quem são.</p>
<p>Vamos pra mais um ano.</p>
]]></content:encoded><category>blog</category><category>ai</category><category>llm</category><category>themakitachronicles</category><category>off-topic</category></item><item><title>RAG Está Morto? Contexto Longo, Grep e o Fim do Vector DB Obrigatório</title><link>https://www.akitaonrails.com/2026/04/06/rag-esta-morto-contexto-longo/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/06/rag-esta-morto-contexto-longo/</guid><pubDate>Mon, 06 Apr 2026 11:00:00 GMT</pubDate><description>&lt;p&gt;Já tem um tempo que essa coceira não me larga. No começo da onda de LLMs, lá em 2022/2023, a gente tinha 4k de contexto no GPT 3.5, 8k se esticasse, 32k era luxo. Pra fazer qualquer coisa com documento real, você não tinha escolha: tinha que recortar o texto em pedaços, gerar embeddings, jogar num vector database, fazer similarity search, pegar os top-5 chunks e rezar pra que os pedaços certos aparecessem.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Já tem um tempo que essa coceira não me larga. No começo da onda de LLMs, lá em 2022/2023, a gente tinha 4k de contexto no GPT 3.5, 8k se esticasse, 32k era luxo. Pra fazer qualquer coisa com documento real, você não tinha escolha: tinha que recortar o texto em pedaços, gerar embeddings, jogar num vector database, fazer similarity search, pegar os top-5 chunks e rezar pra que os pedaços certos aparecessem.</p>
<p>Daí virou indústria. Pinecone, Weaviate, Qdrant, Chroma, Milvus, pgvector, LangChain, LlamaIndex, Haystack. Tutorial em todo canto, &ldquo;construa seu chatbot com seus PDFs&rdquo;, consultorias inteiras vivendo disso. Virou meio que o &ldquo;hello world&rdquo; de LLM aplicado: documento → chunk → embed → vector DB → query.</p>
<p>Hoje, em abril de 2026, o Claude Opus 4.6 tem 1 milhão de tokens de contexto. O Sonnet 4.6, idem. O Gemini 3.1 Pro também. O GPT 5.4 fica numa janela menor mas ainda confortável, na casa das centenas de milhares. E pra alguns modelos já tem modos experimentais de 2M tokens. A pergunta que não me larga é: pra que cargas d&rsquo;água eu preciso montar uma stack vector pra resolver problema que cabe na janela do modelo?</p>
<p>E mais: vector database tem problemas reais que ninguém gosta muito de falar. Falsos vizinhos. Chunking arbitrário que parte definição do uso. Embeddings que envelhecem mal. Sem dizer que quando o resultado vem errado, você não tem a menor ideia do porquê.</p>
<p>A tese que eu venho amadurecendo é simples: na maioria dos casos, um <code>grep</code> bem feito mais uma janela de contexto generosa do modelo bate uma stack RAG completa. É mais barato, é mais fácil de manter, e quando dá pau você consegue debugar. Bora destrinchar.</p>
<h2>O que o vazamento do Claude Code mostrou<span class="hx:absolute hx:-mt-20" id="o-que-o-vazamento-do-claude-code-mostrou"></span>
    <a href="#o-que-o-vazamento-do-claude-code-mostrou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de entrar na parte teórica, vale falar de algo que aconteceu há poucos dias e que reforça muito essa discussão. Em 31 de março de 2026, a Anthropic, sem querer, publicou no npm a versão 2.1.88 do pacote <code>@anthropic-ai/claude-code</code> com um source map de quase 60 MB anexado, e cerca de 512 mil linhas de TypeScript da ferramenta interna deles vazaram pro mundo. Eu já tinha <a href="/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/">escrito sobre o incidente na semana passada</a>, com mais detalhe sobre o que apareceu no código.</p>
<p>O que interessa pra essa discussão é o sistema de memória do Claude Code. Em vez de jogar tudo num vector DB, a arquitetura tem três camadas. Um <code>MEMORY.md</code> que fica permanentemente carregado no contexto, mas não guarda dado nenhum: é só um índice de ponteiros, umas 150 caracteres por linha, mantido abaixo de 200 linhas e uns 25 KB. Os fatos de verdade ficam em &ldquo;topic files&rdquo; buscados sob demanda quando o agente precisa. E os transcripts brutos das sessões anteriores nunca são relidos inteiros, só pesquisados com grep atrás de identificador específico. Sem embedding. Sem Pinecone. Disciplina de escrita (topic file primeiro, índice depois) e busca lexical, só isso.</p>
<p>O loop principal do Claude Code também tem um sistema escalonado pra lidar com contexto enchendo. Como <a href="/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/">eu detalhei no post anterior</a>, são cinco estratégias diferentes de compactação de contexto, com nomes tipo <code>microcompact</code> (limpa resultados de tool antigos por tempo), <code>context collapse</code> (resume trechos longos da conversa), e <code>autocompact</code> (que dispara quando o contexto chega perto do limite). O CLAUDE.md, que muita gente pensava que era só uma convenção, é primeira classe na arquitetura: o sistema relê o arquivo a cada iteração da query.</p>
<p>O que isso me diz: a melhor ferramenta de coding agent que existe hoje, feita pela empresa que vende o modelo mais caro do mercado, <strong>não usa vector DB</strong>. Usa arquivos no disco, índice em markdown, busca lexical, e estratégias inteligentes de compactação quando o contexto estoura. Eles poderiam ter botado embedding, eles têm dinheiro pra rodar o que quisessem, e escolheram não. O motivo, na minha leitura, é exatamente o que esse post defende: pra recuperar texto de arquivos que você controla, com contexto generoso disponível, vector DB é peso morto. Melhor investir em compactar bem o que você já tem na janela do que indexar tudo num banco externo.</p>
<p>Tem um detalhe curioso de segurança que veio junto: a galera percebeu que o pipeline de compactação tem uma vulnerabilidade chamada de &ldquo;context poisoning&rdquo;. Conteúdo que parece instrução, vindo de um arquivo que o modelo lê (tipo um CLAUDE.md de um repo clonado), pode acabar sendo preservado pelo modelo de compactação como se fosse &ldquo;feedback do usuário&rdquo;, e o modelo seguinte segue isso como instrução genuína. É um vetor de ataque novo. Mas isso é assunto pra outro post.</p>
<h3>O sistema &ldquo;Dream&rdquo; e a consolidação de memória<span class="hx:absolute hx:-mt-20" id="o-sistema-dream-e-a-consolidação-de-memória"></span>
    <a href="#o-sistema-dream-e-a-consolida%c3%a7%c3%a3o-de-mem%c3%b3ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mas o que mais me chamou atenção pro debate de RAG, e que eu já <a href="/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/">destrinchei na semana passada</a>, é o sistema chamado <code>autoDream</code>. É um subagente forkado, com bash read-only no projeto, que roda em background enquanto você não está usando a ferramenta. O job dele é literalmente sonhar: consolidar memória. O nome não é à toa, e a analogia óbvia (que eu não consegui evitar) é a do cérebro humano consolidando memória durante o sono, transformando experiência de curto prazo em conhecimento mais estável.</p>
<p>Pra um sonho rodar, três portas têm que abrir ao mesmo tempo: 24 horas desde o último sonho, no mínimo 5 sessões desde o último, e um lock de consolidação que impede sonhos concorrentes. Quando dispara, segue quatro fases. Orient (faz <code>ls</code> no diretório de memória, lê o índice). Gather (busca sinais novos em logs, memórias desatualizadas e transcripts). Consolidate (escreve ou atualiza os topic files, converte data relativa em absoluta, deleta fato que foi contraditado). E Prune, que é a faxina final que mantém o índice abaixo das 200 linhas.</p>
<p>A decisão de fazer o <code>autoDream</code> como subagente forkado é o detalhe que importa aqui. Ele não roda no mesmo loop do agente principal. Por quê? Porque consolidação de memória é processo barulhento. O modelo tem que reler transcript antigo, comparar com o que está no <code>MEMORY.md</code>, decidir o que fica e o que sai, fazer hipótese sobre coisa que viu em sessão anterior. Se isso rodasse no contexto principal, poluía o &ldquo;train of thought&rdquo; do agente que está tentando ajudar você na tarefa do momento. Forkando, separa as duas coisas. O agente principal continua focado no que você pediu, o <code>autoDream</code> faz a faxina em paralelo, sem permissão de escrita no projeto.</p>
<p>E o jeito como ele acha o que tem que consolidar é busca lexical, pura e dura. Os transcripts ficam em arquivos JSONL no disco, e o <code>autoDream</code> usa grep pra procurar sinais novos. Grep mesmo, em log de texto. Pensa nisso por um segundo. A consolidação de memória do agente mais avançado do mundo, feita por uma das empresas mais ricas de IA, é um subagente forkado fazendo grep em log de texto. Se vector DB fosse a resposta certa pra esse tipo de problema, a Anthropic tinha botado vector DB. Não botaram.</p>
<p>E tem um detalhe que pra mim é o ouro escondido do leak inteiro, e que cabe perfeitamente nesse argumento. No <code>autoDream</code>, memória é tratada como pista. O sistema parte do princípio de que o que está armazenado pode estar velho, errado, contraditado por algo que aconteceu depois, e o modelo tem que verificar antes de confiar. O pitch do vector DB é o oposto disso: indexa tudo, busca por similaridade, devolve top-k, confia no resultado. O Claude Code escolheu o caminho conservador. Indexa pouco, busca por palavra, devolve pista, e desconfia até bater o olho no fato real.</p>
<p>A estratégia inteira tem duas camadas. Dentro da sessão, contexto generoso mais grep mais compactação inteligente (<code>microcompact</code>, <code>context collapse</code>, <code>autocompact</code>). Entre sessões, um subagente que consolida memória de forma assíncrona, usando grep nos transcripts e tratando o resultado como dica, não como verdade. Embedding e vector DB não aparecem em nenhum dos dois lugares. A escolha consciente foi leitor inteligente comendo texto bruto, não leitor burro consumindo top-k de embedding.</p>
<p>A lição prática pro nosso debate é simples. Os agentes mais avançados do mercado tão indo na direção de contexto generoso, busca lexical e compactação inteligente, não na direção de pipeline RAG clássico. Se a Anthropic, com toda a infraestrutura e talento que tem, escolheu esse caminho pra Claude Code, a gente que tá construindo aplicação interna com uma fração do orçamento deveria pelo menos considerar a mesma direção.</p>
<h2>Onde a história começou a virar<span class="hx:absolute hx:-mt-20" id="onde-a-história-começou-a-virar"></span>
    <a href="#onde-a-hist%c3%b3ria-come%c3%a7ou-a-virar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando o teto era 32k de contexto, retrieval era o gargalo do problema inteiro. Você tinha que pré-filtrar agressivo, porque qualquer coisa que entrasse na janela era espaço sagrado. Vector DB foi a única forma decente de fazer essa pré-filtragem semântica. A lógica era: &ldquo;o leitor (LLM) é caro e burro, então o retriever tem que ser inteligente e seletivo&rdquo;.</p>
<p>Hoje a equação virou. O leitor agora é o cara mais inteligente da mesa, e a janela cresceu pra caber documento inteiro. Aí o retriever pode (e talvez deva) voltar pra ser burro. Quanto mais burro, melhor. Você quer alta cobertura e baixa precisão, e deixa o modelo fazer a parte fina. Grep faz exatamente isso. BM25 também. E ripgrep voa em cima de milhões de linhas sem pestanejar.</p>
<p>E não é só achismo meu. Os benchmarks BEIR já mostraram faz tempo que BM25 bate ou empata com vários retrievers densos quando o domínio sai de onde os embeddings foram treinados. A própria Anthropic publicou um post sobre <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener">Contextual Retrieval</a> basicamente dizendo a mesma coisa: sinal lexical mais julgamento de LLM bate embedding puro na maior parte das tarefas de conhecimento. E olha o Claude Code, a ferramenta que eu uso todo dia há 500 horas: ele navega o repositório com <code>Glob</code> e <code>Grep</code>. Sem vector DB, sem embedding, sem LangChain. Funciona ridiculamente bem.</p>
<h2>Os problemas reais do vector database que ninguém anuncia<span class="hx:absolute hx:-mt-20" id="os-problemas-reais-do-vector-database-que-ninguém-anuncia"></span>
    <a href="#os-problemas-reais-do-vector-database-que-ningu%c3%a9m-anuncia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A propaganda de vector DB vende o sonho da busca semântica perfeita. A realidade é mais bagunçada.</p>
<p>Falsos vizinhos é o primeiro. Cosine similarity premia similaridade tópica, não relevância. Você pergunta &ldquo;como tratamos erro de autenticação&rdquo; e o DB devolve todo chunk que menciona autenticação. O chunk que efetivamente responde pode estar em décimo lugar, ou nem ter aparecido porque o redator do doc usou &ldquo;login&rdquo; em vez de &ldquo;auth&rdquo;.</p>
<p>Chunking é o segundo, e é um desastre disfarçado. Janela de 512 tokens, overlap de 64, parece razoável até você perceber que sua tabela importante foi cortada no meio, a definição de uma função ficou separada do uso, e o pedaço da documentação com o comando exato ficou órfão sem o contexto da seção. A fronteira do chunk costuma ser exatamente onde a resposta morava.</p>
<p>Quando falha, falha sem deixar pista. Quando o BM25 não acha, você sabe o porquê: a palavra não tá lá. Quando o vector DB devolve lixo, você recebe um chunk plausível e errado, sem nenhum sinal diagnóstico. Boa sorte debugando isso em produção às duas da manhã.</p>
<p>Índice envelhece. Cada update do documento pede re-embedding. Se você tem 10 mil docs e uns 200 mudam por dia, isso vira processo de batch, monitoramento, fila, retry, custo de API de embedding, e uma janela de inconsistência inevitável entre o que tá no disco e o que tá no índice. Grep não tem nada disso. Arquivo mudou? Próxima query já vê.</p>
<p>E tem o custo de operação que ninguém soma. Pinecone cobra por vector. Weaviate pede cluster pra manter. pgvector evita servidor novo mas você continua com schema, índice e pipeline de re-embedding. Cada uma dessas coisas pede tempo de engenheiro, monitoramento, teste, deploy. Tudo isso pra fazer uma busca que muitas vezes o <code>rg</code> resolve em 200ms.</p>
<h2>Comparando a complexidade<span class="hx:absolute hx:-mt-20" id="comparando-a-complexidade"></span>
    <a href="#comparando-a-complexidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olha o desenho:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/06/rag/rag-vs-grep-complexity.png" alt="Complexidade: RAG clássico vs grep &#43; contexto longo"  loading="lazy" /></p>
<p>De um lado, oito etapas, quatro ou cinco serviços, índice externo que precisa ser mantido e atualizado. Do outro, quatro etapas, zero infraestrutura nova. Não é caricatura: é literalmente o que você precisa montar pra cada caso.</p>
<p>A pergunta honesta é: a coluna da esquerda compensa? Em 2023, sim, porque a coluna da direita não existia (não tinha LLM com janela de 200k). Em 2026, na maior parte dos casos, não.</p>
<h2>Prós e contras de cada lado<span class="hx:absolute hx:-mt-20" id="prós-e-contras-de-cada-lado"></span>
    <a href="#pr%c3%b3s-e-contras-de-cada-lado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>RAG clássico (vector DB)<span class="hx:absolute hx:-mt-20" id="rag-clássico-vector-db"></span>
    <a href="#rag-cl%c3%a1ssico-vector-db" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>A favor:</strong></p>
<ul>
<li>Funciona pra bases de documento gigantes, da ordem de centenas de GB, onde nem <code>rg</code> resolve sem indexação prévia</li>
<li>Acerta consultas paráfrase pesada e cross-lingual (&ldquo;como cancelo&rdquo; vs &ldquo;encerramento de assinatura&rdquo;), onde o vocabulário do usuário não bate com o do documento</li>
<li>Funciona pra modalidades não-textuais (imagem, áudio) onde grep não tem o que olhar</li>
<li>Economiza tokens de input se você tá apertado de orçamento ou de latência absoluta</li>
</ul>
<p><strong>Contra:</strong></p>
<ul>
<li>Stack complexa: embedding, vector DB, chunking, reranker, pipeline de re-indexação</li>
<li>Falhas opacas, difíceis de debugar</li>
<li>Chunking destrói contexto de tabelas, código, definições longas</li>
<li>Overhead operacional (índice, fila, monitoramento, custo de re-embedding)</li>
<li>A busca semântica vendida no marketing raramente funciona como o marketing promete</li>
</ul>
<h3>Grep + contexto longo<span class="hx:absolute hx:-mt-20" id="grep--contexto-longo"></span>
    <a href="#grep--contexto-longo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>A favor:</strong></p>
<ul>
<li>Praticamente zero infraestrutura nova: ripgrep, sqlite, ou um simples <code>LIKE</code> em Postgres</li>
<li>Sempre fresco: o arquivo mudou, a próxima query já vê</li>
<li>Falhas transparentes: a palavra está ou não está</li>
<li>Carrega o documento em pedaços generosos, o modelo faz a filtragem fina com semântica de verdade</li>
<li>Mais barato em dev e ops, mais barato pra pivotar de domínio</li>
</ul>
<p><strong>Contra:</strong></p>
<ul>
<li>Não escala pra terabytes de texto bruto sem alguma indexação</li>
<li>Sofre quando o usuário usa vocabulário muito diferente do documento</li>
<li>Não funciona pra modalidade não-textual</li>
<li>Latência por query é maior em termos absolutos (carregar 100k tokens sempre custa mais que carregar 5k)</li>
<li>Custo de input por query é mais alto se você não tem prompt caching</li>
</ul>
<h2>Mas e o custo?<span class="hx:absolute hx:-mt-20" id="mas-e-o-custo"></span>
    <a href="#mas-e-o-custo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é o argumento que mais me jogam na cara quando defendo a tese do &ldquo;carrega tudo no contexto&rdquo;. &ldquo;Vai ficar caríssimo, 200k tokens de input por query é absurdo.&rdquo; Vamos fazer a conta de verdade.</p>
<p>No <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">meu artigo do benchmark de LLMs de ontem</a> eu mapeei o preço por token de cada modelo. Pega o Claude Sonnet 4.6: $3 por milhão de tokens de input, $15 por milhão de output. Pega o GLM 5 (que provei que funciona): $0.60 input, $2.20 output. Pega o GPT 5.4 Pro lá em cima: $15 input, $180 output (esse aí machuca, eu sei).</p>
<p>Antes de fazer a conta de &ldquo;200k tokens&rdquo; em dólar, vale aterrissar isso em algo tangível, porque &ldquo;100k tokens&rdquo; não diz nada pra ninguém. Token, na média, é mais ou menos 0,75 palavra em inglês (em português é parecido, talvez um pouco mais por causa de palavras maiores). Então, traduzindo:</p>
<ul>
<li><strong>100k tokens</strong> ≈ 75 mil palavras ≈ um romance curto inteiro, tipo <em>O Velho e o Mar</em> do Hemingway com sobra, ou uns três artigos longos da Wikipedia juntos.</li>
<li><strong>200k tokens</strong> ≈ 150 mil palavras ≈ um romance grande, tipo <em>Crime e Castigo</em> na íntegra, ou metade do primeiro livro de <em>Game of Thrones</em> (que tem ~298k palavras, daria uns 400k tokens).</li>
<li><strong>400k tokens</strong> ≈ 300 mil palavras ≈ <em>A Game of Thrones</em> completo, livro 1 da série inteiro na janela.</li>
<li><strong>1M tokens</strong> ≈ 750 mil palavras ≈ a trilogia inteira de <em>O Senhor dos Anéis</em> mais <em>O Hobbit</em>, ou a Bíblia inteira (King James ~783k palavras, daria por volta de 1M tokens), ou cerca de dois livros e meio de <em>Game of Thrones</em> empilhados.</li>
</ul>
<p>Então quando eu falo &ldquo;joga 200k tokens de input no modelo&rdquo;, o que isso significa no mundo real é &ldquo;joga <em>Crime e Castigo</em> inteiro como contexto da pergunta&rdquo;. É muita coisa. E é exatamente isso que torna o argumento desse post viável: os modelos de hoje conseguem ler um romance inteiro de uma vez e ainda responder uma pergunta específica sobre ele. Em 2023, isso era ficção científica. Em 2026, virou o caso base.</p>
<p>Imagina então uma query que joga 200k tokens de input (lá vai <em>Crime e Castigo</em> de novo) e produz 2k tokens de output (umas três páginas de resposta):</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: right">Input ($)</th>
          <th style="text-align: right">Output ($)</th>
          <th style="text-align: right">Total por query</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">$0.60</td>
          <td style="text-align: right">$0.03</td>
          <td style="text-align: right"><strong>$0.63</strong></td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">$3.00</td>
          <td style="text-align: right">$0.15</td>
          <td style="text-align: right"><strong>$3.15</strong></td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">$0.12</td>
          <td style="text-align: right">$0.0044</td>
          <td style="text-align: right"><strong>$0.12</strong></td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">$0.40</td>
          <td style="text-align: right">$0.024</td>
          <td style="text-align: right"><strong>$0.42</strong></td>
      </tr>
      <tr>
          <td>GPT 5.4 Pro</td>
          <td style="text-align: right">$3.00</td>
          <td style="text-align: right">$0.36</td>
          <td style="text-align: right"><strong>$3.36</strong></td>
      </tr>
  </tbody>
</table>
<p>Agora bota prompt caching no meio. O Claude tem cache que faz o input cacheado custar uma fração do preço cheio (na ordem de 10%, dependendo do modelo). O Gemini tem mecanismo similar. Quando você faz uma sequência de queries em cima do mesmo dump de 200k tokens, o custo das queries seguintes despenca pra centavos. Com Sonnet cacheado, dá pra falar em uns $0.10 por query subsequente sem inventar muito.</p>
<p>Agora compara isso com o custo de manter um Pinecone, ou um Weaviate, ou um pgvector. Ignorando o preço da assinatura em si (que varia bastante), você precisa de engenheiro pra montar a pipeline, manter, monitorar, lidar com falha de embedding, refazer chunking quando o domínio muda. Conservadoramente, fala em algo entre 40 e 80 horas de engenharia pra deixar a coisa estável. A R$ 200/hora, isso é entre R$ 8.000 e R$ 16.000. Em USD, na faixa de $1.600 a $3.200 só pra colocar de pé.</p>
<p>Com $3.200, no Sonnet 4.6 com prompt caching, você roda algo na ordem de 30 mil queries de 200k tokens. Trinta mil queries, dependendo da escala do projeto, dão vários meses ou até um ano inteiro de uma ferramenta interna média. E você não pagou engenheiro pra montar pipeline. Não tem servidor de vector DB pra manter. Se o documento mudar, o sistema já vê na próxima query.</p>
<p>A conta do &ldquo;RAG é mais barato em tokens&rdquo; ignora que token é a coisa mais barata da equação inteira. Engenheiro custa caro, servidor custa caro, bug em produção custa muito caro. Token virou commodity, e tá ficando mais barato a cada release de modelo novo.</p>
<p>O argumento clássico do RAG era &ldquo;modelo é caro, retrieval é barato&rdquo;. Hoje é o oposto: modelo é a parte barata da pilha, retrieval inteligente é o que sai caro pra montar e manter.</p>
<h2>Os pontos onde a tese não cola<span class="hx:absolute hx:-mt-20" id="os-pontos-onde-a-tese-não-cola"></span>
    <a href="#os-pontos-onde-a-tese-n%c3%a3o-cola" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não quero parecer fanboy. Tem casos onde RAG clássico ainda ganha:</p>
<ol>
<li><strong>Bases gigantescas.</strong> Se você tem 500 GB de texto bruto, nem <code>rg</code> resolve em tempo aceitável. Aí precisa de algum tipo de indexação. Pode ser BM25 indexado (Tantivy, Elasticsearch), pode ser vector DB. Mas observa: a primeira opção ainda é lexical, não vetorial.</li>
<li><strong>Vocabulário muito disperso.</strong> Suporte ao cliente, onde o usuário fala &ldquo;tô sem net&rdquo; e a documentação fala &ldquo;perda de conectividade na camada física&rdquo;. BM25 não pega isso. Embedding pega. Aí vector DB ganha o ponto.</li>
<li><strong>Modalidade não-textual.</strong> Busca de imagem por imagem, áudio por áudio. Embedding é obrigatório.</li>
<li><strong>Latência absoluta crítica.</strong> Se você precisa responder em 100ms e tem 5k de orçamento de input, dump generoso não cabe. Aí pré-filtragem é necessária.</li>
<li><strong>Compliance e auditoria.</strong> Se você precisa provar que tal documento foi consultado pra responder tal query, ter chunks indexados rastreáveis ajuda. Dump de 200k de contexto é mais opaco em auditoria.</li>
</ol>
<p>Pra esses casos, RAG clássico continua fazendo sentido. Mas observa o tamanho da lista. São casos específicos. O caso geral, tipo &ldquo;chat com nossos documentos internos&rdquo; ou &ldquo;pergunte ao manual do produto&rdquo;, quase todo cai no balde do &ldquo;grep + contexto longo resolve melhor&rdquo;.</p>
<h2>Lazy retrieval: a receita que eu defendo<span class="hx:absolute hx:-mt-20" id="lazy-retrieval-a-receita-que-eu-defendo"></span>
    <a href="#lazy-retrieval-a-receita-que-eu-defendo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu fosse construir uma ferramenta de &ldquo;chat com docs&rdquo; hoje, do zero, seria mais ou menos assim:</p>
<ol>
<li><strong>Mantém os documentos brutos.</strong> Markdown, PDF convertido, código, o que for. No disco mesmo, organizado em pastas que façam sentido pro domínio.</li>
<li><strong>Filtro lexical rápido.</strong> <code>ripgrep</code> com regex, ou BM25 com Tantivy/SQLite FTS5, ou um <code>LIKE</code> no Postgres se já tiver. Devolve uns 100-300 hits.</li>
<li><strong>Carrega generosamente.</strong> Pega não só o trecho que bateu, mas o arquivo inteiro, ou uma janela grande em volta. Joga tudo no contexto.</li>
<li><strong>Deixa o LLM fazer a parte fina.</strong> Passa a pergunta original, manda o modelo encontrar o que importa, descartar o resto, e responder com citações.</li>
<li><strong>(Opcional) Adiciona embeddings só pra classes de query onde lexical falha</strong>, depois de você ter dados reais mostrando que falha.</li>
</ol>
<p>Isso é o oposto do conselho antigo (&ldquo;comece com vetores, fallback pra keyword&rdquo;). É: <strong>comece com keyword, e adicione vetor só se sentir falta</strong>. Na maioria dos projetos, você nunca vai sentir.</p>
<h2>Uma implementação de brinquedo em Ruby<span class="hx:absolute hx:-mt-20" id="uma-implementação-de-brinquedo-em-ruby"></span>
    <a href="#uma-implementa%c3%a7%c3%a3o-de-brinquedo-em-ruby" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra deixar concreto. Eis um script Ruby usando o gem <a href="https://github.com/crmne/ruby_llm"target="_blank" rel="noopener"><code>ruby_llm</code></a> (o mesmo do benchmark de ontem) que faz exatamente esse fluxo: grep nos arquivos, carrega os trechos com contexto, manda pro Claude, recebe a resposta. Sem vector DB, sem chunking, sem embedding, sem LangChain.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env ruby</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;ruby_llm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;open3&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">DOCS_DIR</span> <span class="o">=</span> <span class="no">ARGV</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;./docs&#34;</span>
</span></span><span class="line"><span class="cl"><span class="no">QUERY</span>    <span class="o">=</span> <span class="no">ARGV</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="ow">or</span> <span class="nb">abort</span> <span class="s2">&#34;uso: ./ask.rb &lt;pasta&gt; &lt;pergunta&gt;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Filtro lexical rápido com ripgrep.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    -i case insensitive, -l só nomes de arquivo, --type-add cobre md/txt/pdf-extraído.</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">lexical_search</span><span class="p">(</span><span class="n">dir</span><span class="p">,</span> <span class="n">query</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">terms</span> <span class="o">=</span> <span class="n">query</span><span class="o">.</span><span class="n">downcase</span><span class="o">.</span><span class="n">scan</span><span class="p">(</span><span class="sr">/\w{4,}/</span><span class="p">)</span><span class="o">.</span><span class="n">uniq</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>  <span class="c1"># palavras com 4+ letras</span>
</span></span><span class="line"><span class="cl">  <span class="n">pattern</span> <span class="o">=</span> <span class="n">terms</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;|&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;rg&#34;</span><span class="p">,</span> <span class="s2">&#34;-l&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">,</span> <span class="n">dir</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="no">Open3</span><span class="o">.</span><span class="n">capture2</span><span class="p">(</span><span class="o">*</span><span class="n">cmd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Carrega os arquivos inteiros (até um teto razoável).</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load_context</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="ss">max_chars</span><span class="p">:</span> <span class="mi">600_000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">body</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">total</span> <span class="o">+</span> <span class="n">body</span><span class="o">.</span><span class="n">size</span> <span class="o">&gt;</span> <span class="n">max_chars</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">+=</span> <span class="n">body</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;## </span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="se">\n\n</span><span class="si">#{</span><span class="n">body</span><span class="si">}</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="o">.</span><span class="n">compact</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">---</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Manda pro Claude com a pergunta e os documentos.</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">ask</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4-6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">prompt</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">PROMPT</span>
</span></span><span class="line"><span class="cl">    <span class="no">Você</span> <span class="n">tem</span> <span class="n">acesso</span> <span class="n">aos</span> <span class="n">documentos</span> <span class="n">abaixo</span><span class="o">.</span> <span class="no">Responda</span> <span class="n">a</span> <span class="n">pergunta</span> <span class="k">do</span> <span class="n">usuário</span>
</span></span><span class="line"><span class="cl">    <span class="n">usando</span> <span class="n">apenas</span> <span class="n">o</span> <span class="n">que</span> <span class="n">está</span> <span class="n">nos</span> <span class="n">documentos</span><span class="o">.</span> <span class="no">Cite</span> <span class="n">o</span> <span class="n">nome</span> <span class="k">do</span> <span class="n">arquivo</span> <span class="n">nas</span>
</span></span><span class="line"><span class="cl">    <span class="n">referências</span><span class="o">.</span> <span class="no">Se</span> <span class="n">a</span> <span class="n">resposta</span> <span class="n">não</span> <span class="n">estiver</span> <span class="n">nos</span> <span class="n">documentos</span><span class="p">,</span> <span class="n">diga</span> <span class="n">isso</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="o">---</span> <span class="no">DOCUMENTOS</span> <span class="o">---</span>
</span></span><span class="line"><span class="cl">    <span class="c1">#{context}</span>
</span></span><span class="line"><span class="cl">    <span class="o">---</span> <span class="no">FIM</span> <span class="no">DOS</span> <span class="no">DOCUMENTOS</span> <span class="o">---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="ss">Pergunta</span><span class="p">:</span> <span class="c1">#{query}</span>
</span></span><span class="line"><span class="cl">  <span class="no">PROMPT</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span><span class="o">.</span><span class="n">content</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">files</span> <span class="o">=</span> <span class="n">lexical_search</span><span class="p">(</span><span class="no">DOCS_DIR</span><span class="p">,</span> <span class="no">QUERY</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">abort</span> <span class="s2">&#34;nenhum arquivo bateu&#34;</span> <span class="k">if</span> <span class="n">files</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="s2">&#34;Encontrei </span><span class="si">#{</span><span class="n">files</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2"> arquivos. Carregando contexto...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">context</span> <span class="o">=</span> <span class="n">load_context</span><span class="p">(</span><span class="n">files</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="n">ask</span><span class="p">(</span><span class="no">QUERY</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>São umas 40 linhas. Sem dependência de Pinecone, sem schema de vector, sem pipeline de re-indexação. Você roda como <code>./ask.rb ./docs &quot;como configurar o webhook do pagamento&quot;</code> e pronto.</p>
<p>Esse exemplo aí é one-shot. Você roda, ele responde, acabou. Pra chat de verdade, com várias perguntas em sequência em cima dos mesmos documentos, o desenho muda. Em vez de fazer o <code>lexical_search</code> lá no começo e empurrar tudo de uma vez pro contexto, você expõe a busca como tool pro modelo. Aí é o agente que decide quando precisa puxar mais doc, que termo vai procurar, qual arquivo vale a pena abrir inteiro. É assim que o Claude Code funciona, na real: <code>Glob</code>, <code>Grep</code> e <code>Read</code> são tools, e o modelo é quem escolhe a sequência. O <code>ruby_llm</code> suporta tool calling, então dá pra fazer a mesma coisa em Ruby. A cara fica mais ou menos assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;ruby_llm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;open3&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">DOCS_DIR</span> <span class="o">=</span> <span class="s2">&#34;./docs&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SearchFiles</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Procura arquivos cujo conteúdo casa com o padrão dado (regex). Retorna lista de paths.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:pattern</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Padrão regex pra busca lexical (case-insensitive)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">pattern</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="no">Open3</span><span class="o">.</span><span class="n">capture2</span><span class="p">(</span><span class="s2">&#34;rg&#34;</span><span class="p">,</span> <span class="s2">&#34;-l&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">,</span> <span class="no">DOCS_DIR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ReadFile</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Lê o conteúdo completo de um arquivo do projeto.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:path</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Caminho relativo do arquivo&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">path</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;erro: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4-6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">with_tools</span><span class="p">(</span><span class="no">SearchFiles</span><span class="p">,</span> <span class="no">ReadFile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">with_instructions</span><span class="p">(</span><span class="o">&lt;&lt;~</span><span class="no">SYS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">              <span class="no">Você</span> <span class="n">responde</span> <span class="n">perguntas</span> <span class="n">sobre</span> <span class="n">os</span> <span class="n">documentos</span> <span class="n">em</span> <span class="c1">#{DOCS_DIR}.</span>
</span></span><span class="line"><span class="cl">              <span class="no">Use</span> <span class="n">search_files</span> <span class="n">pra</span> <span class="n">encontrar</span> <span class="n">arquivos</span> <span class="n">relevantes</span> <span class="n">e</span> <span class="n">read_file</span>
</span></span><span class="line"><span class="cl">              <span class="n">pra</span> <span class="n">ler</span> <span class="n">o</span> <span class="n">conteúdo</span><span class="o">.</span> <span class="no">Sempre</span> <span class="n">cite</span> <span class="n">o</span> <span class="n">arquivo</span> <span class="n">na</span> <span class="n">resposta</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">            <span class="no">SYS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kp">loop</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">print</span> <span class="s2">&#34;&gt; &#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">msg</span> <span class="o">=</span> <span class="nb">gets</span><span class="o">&amp;.</span><span class="n">chomp</span>
</span></span><span class="line"><span class="cl">  <span class="k">break</span> <span class="k">if</span> <span class="n">msg</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">msg</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl">  <span class="nb">puts</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span><span class="o">.</span><span class="n">content</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O modelo recebe a pergunta, decide se precisa procurar, chama <code>search_files</code>, vê o que voltou, decide se precisa abrir algum arquivo, chama <code>read_file</code>, e só depois responde. Em pergunta seguinte ele já tem o contexto anterior na sessão e pode pedir mais coisa se precisar. O contexto vai recebendo só o que o modelo pediu, não o despejo inteiro do grep que o exemplo anterior fazia.</p>
<p>A mesma ideia funciona pra banco de dados: troca o <code>rg</code> por uma query SQL com <code>LIKE</code> ou <code>tsvector</code> (full-text do Postgres), carrega as linhas relevantes, joga no contexto. Se você tiver 10k registros num banco interno, isso resolve. Se tiver 10 milhões, aí você começa a precisar de paginação inteligente ou de uma camada de pré-filtragem mais séria. Mas a estrutura mental é a mesma: <strong>filtro burro + leitor inteligente</strong>.</p>
<h2>O ponto que importa<span class="hx:absolute hx:-mt-20" id="o-ponto-que-importa"></span>
    <a href="#o-ponto-que-importa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O mais interessante nisso tudo nem é a economia de Pinecone. É que a natureza do gargalo mudou. Em 2023, o gargalo era retrieval: o leitor era pequeno, lento, caro, e você precisava de um retriever esperto pra encher a janela com o mínimo possível. Em 2026, o gargalo é raciocínio sobre contexto bagunçado: o leitor é grande, relativamente rápido, e barato. Aí faz mais sentido um retriever burrão de alta cobertura e deixar o modelo fazer o trabalho pesado.</p>
<p>Quem ainda projeta sistema com a cabeça de 2023 tá pagando caro pra resolver um problema que mudou de forma. RAG não morreu, o &ldquo;R&rdquo; ficou mais burro e mais barato, e isso é uma melhoria. Quem vende vector DB não vai te contar, mas é o caminho que a galera mais experiente vem seguindo na surdina.</p>
<p>A próxima onda de aplicação LLM, na minha aposta, vai ser dominada por quem entendeu essa inversão. Stack menor, infraestrutura mais simples, contexto generoso, e muito menos LangChain.</p>
<h2>O que a literatura recente diz<span class="hx:absolute hx:-mt-20" id="o-que-a-literatura-recente-diz"></span>
    <a href="#o-que-a-literatura-recente-diz" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de fechar, fui dar uma olhada no que a galera de pesquisa publicou sobre isso. Achismo de blog nessa área envelhece em três meses, então melhor olhar paper.</p>
<p>O <a href="https://arxiv.org/abs/2407.16833"target="_blank" rel="noopener"><strong>Retrieval Augmented Generation or Long-Context LLMs?</strong></a>, do Google DeepMind, publicado na EMNLP 2024, é o mais citado no debate. A conclusão deles: quando o modelo tem recurso suficiente, long context bate RAG na média de qualidade, mas RAG continua sendo bem mais barato em tokens. Eles propõem o Self-Route, uma abordagem onde o próprio modelo decide se precisa do retrieval ou se manda direto pelo contexto. A economia de tokens é grande e a perda de qualidade é pequena.</p>
<p>Já o <a href="https://openreview.net/forum?id=CLF25dahgA"target="_blank" rel="noopener"><strong>LaRA</strong></a>, apresentado na ICML 2025, é mais comedido. Os autores montaram 2326 casos de teste em quatro tipos de tarefa de QA e três tipos de contexto longo, rodaram em 11 LLMs diferentes, e a conclusão foi: não tem bala de prata. A escolha entre RAG e long context depende do modelo, do tamanho do contexto, do tipo de tarefa e da característica do retrieval. RAG ganha em diálogo e queries genéricas, long context ganha em QA estilo Wikipedia.</p>
<p>O <a href="https://arxiv.org/abs/2501.01880"target="_blank" rel="noopener"><strong>Long Context vs. RAG for LLMs: An Evaluation and Revisits</strong></a>, de janeiro de 2025, é o que mais reforça a tese deste post. Long context costuma bater RAG nos benchmarks de QA, especialmente quando o documento base é estável. Retrieval baseado em sumarização chega perto, e retrieval baseado em chunk fica atrás. Ou seja: o jeito antigo, chunk mais embed mais top-k, é o que sai pior.</p>
<p>Tem que ter no radar também o original <a href="https://arxiv.org/abs/2307.03172"target="_blank" rel="noopener"><strong>Lost in the Middle</strong></a> (Liu et al., 2023, publicado no TACL em 2024). Foi o paper que mostrou que mesmo modelos com janela grande têm performance dependente da posição da informação relevante. Coisa no começo ou no fim do contexto é encontrada fácil; coisa no meio degrada. Por muito tempo isso foi usado como argumento contra long context, mas o paper é de 2023, com modelos de 2023. Os modelos de hoje, tipo Claude 4.x e Gemini 3.x, lidam muito melhor com a parte do meio. Não é problema resolvido, mas é bem menor do que era.</p>
<p>Pelo lado de retrieval lexical, o <a href="https://arxiv.org/abs/2104.08663"target="_blank" rel="noopener"><strong>BEIR</strong></a> continua sendo a referência canônica. O resultado clássico é que BM25, lá dos anos 90, segue competitivíssimo em cenário out-of-domain. Os modelos densos só ganham consistentemente quando você tem dados do próprio domínio pra fine-tunar os embeddings. Em cenário zero-shot, que é onde a maioria dos projetos vive, BM25 é difícil de bater sem trabalho pesado.</p>
<p>Pra fechar, o post da <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener"><strong>Anthropic sobre Contextual Retrieval</strong></a>, de setembro de 2024, é a peça mais prática da lista. Eles mostram que combinando embedding contextualizado com BM25 contextualizado, dá pra cair de 5.7% pra 2.9% de taxa de falha no top-20. Adicionando reranker, cai pra 1.9%. Detalhe importante: BM25 é peça central do resultado deles, não auxiliar. A leitura correta é &ldquo;lexical mais vetor mais reranker é a combinação que funciona&rdquo;. Quem só pode escolher um, escolhe BM25 e ainda chega longe.</p>
<p>Resumindo o que dá pra cravar: a literatura não diz que &ldquo;RAG morreu&rdquo;. Diz que long context, quando dá pra usar, costuma vencer em qualidade. Diz que o custo de RAG ainda é o argumento principal a favor dele. Diz que BM25 lexical é bem mais forte do que a propaganda de vector DB faz parecer. E diz que, quando você realmente precisa de retrieval pesado, a combinação robusta é hybrid (lexical mais vetor mais reranker), não vetor puro. Tudo isso bate com o que eu venho defendendo na prática.</p>
<h2>Fontes<span class="hx:absolute hx:-mt-20" id="fontes"></span>
    <a href="#fontes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Li, Z. et al. (2024). <a href="https://arxiv.org/abs/2407.16833"target="_blank" rel="noopener">Retrieval Augmented Generation or Long-Context LLMs? A Comprehensive Study and Hybrid Approach</a>. EMNLP 2024 Industry Track.</li>
<li>Yuan, K. et al. (2025). <a href="https://openreview.net/forum?id=CLF25dahgA"target="_blank" rel="noopener">LaRA: Benchmarking Retrieval-Augmented Generation and Long-Context LLMs – No Silver Bullet for LC or RAG Routing</a>. ICML 2025.</li>
<li>Yu, T. et al. (2025). <a href="https://arxiv.org/abs/2501.01880"target="_blank" rel="noopener">Long Context vs. RAG for LLMs: An Evaluation and Revisits</a>. arXiv:2501.01880.</li>
<li>Liu, N. F. et al. (2023). <a href="https://arxiv.org/abs/2307.03172"target="_blank" rel="noopener">Lost in the Middle: How Language Models Use Long Contexts</a>. TACL 2024.</li>
<li>Thakur, N. et al. (2021). <a href="https://arxiv.org/abs/2104.08663"target="_blank" rel="noopener">BEIR: A Heterogenous Benchmark for Zero-shot Evaluation of Information Retrieval Models</a>. NeurIPS Datasets and Benchmarks 2021.</li>
<li>Anthropic (2024). <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener">Introducing Contextual Retrieval</a>. Blog técnico.</li>
<li>Akita, F. (2026). <a href="/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/">O código fonte do Claude Code vazou. O que achamos dentro.</a> — minha cobertura do leak, com mais detalhe sobre arquitetura de memória, KAIROS e <code>autoDream</code>.</li>
</ul>
]]></content:encoded><category>llm</category><category>rag</category><category>vibecoding</category><category>ai</category></item><item><title>Testando LLMs Open Source e Comerciais - Quem Consegue Bater o Claude Opus?</title><link>https://www.akitaonrails.com/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/</guid><pubDate>Sun, 05 Apr 2026 18:00:00 GMT</pubDate><description>&lt;p&gt;&lt;strong&gt;Update 16 de Abril, 2026:&lt;/strong&gt; Adicionamos Claude Opus 4.7, Qwen 3.6 e GPT 5.4 via Codex CLI (xHigh reasoning). Opus 4.7 é melhoria incremental sobre 4.6 (28 testes vs 16, mesma API correta) e passa a ser o novo baseline. GPT 5.4, que estava no Tier 1 pelo meu aval pessoal, agora tem dados objetivos e caiu pro Tier 2 — gastou 7.6M tokens (~$16/run, 15x mais caro que Opus) e errou a convenção de chamada do &lt;code&gt;add_message&lt;/code&gt; no multi-turn. Qwen 3.6 Plus continua Tier 3 com a mesma alucinação de API do 3.5. A conclusão se mantém: se quer segurança, Opus.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Update 16 de Abril, 2026:</strong> Adicionamos Claude Opus 4.7, Qwen 3.6 e GPT 5.4 via Codex CLI (xHigh reasoning). Opus 4.7 é melhoria incremental sobre 4.6 (28 testes vs 16, mesma API correta) e passa a ser o novo baseline. GPT 5.4, que estava no Tier 1 pelo meu aval pessoal, agora tem dados objetivos e caiu pro Tier 2 — gastou 7.6M tokens (~$16/run, 15x mais caro que Opus) e errou a convenção de chamada do <code>add_message</code> no multi-turn. Qwen 3.6 Plus continua Tier 3 com a mesma alucinação de API do 3.5. A conclusão se mantém: se quer segurança, Opus.</p>
<hr>
<p><strong>TL;DR:</strong> Se você não quer ler a análise inteira: os únicos modelos que geraram código que funciona de verdade no nosso benchmark foram Claude Opus 4.7, Claude Opus 4.6, GLM 5 e GLM 5.1 (da Z.AI, ~89% mais baratos que Opus). O Sonnet também funciona nesse benchmark, mas na prática falha em projetos que exijam raciocínio mais profundo — detalhes na conclusão. O GPT 5.4, que antes estava no Tier 1 pelo meu aval pessoal, agora tem dados objetivos via Codex CLI: gastou 7.6M tokens ($16/run) e errou a convenção de chamada do <code>add_message</code> — funciona na primeira mensagem, quebra no multi-turn. Caiu pro Tier 2. O resto — Kimi, DeepSeek, MiniMax, Qwen, Gemini, Grok 4.20 — inventou APIs que não existem ou ignorou o gem pedido.</p>
<p>Tem uma novidade nesse update: refiz a parte local do benchmark numa RTX 5090 (em vez do AMD Strix Halo) e adicionei um lote de modelos Qwen, incluindo um Qwen 3.5 27B distilado direto do Claude 4.6 Opus. Isso reabriu a conversa sobre rodar modelo open source localmente. A banda de memória da 5090 muda o jogo de &ldquo;inviável&rdquo; pra &ldquo;viável com 1-2 prompts de correção&rdquo;. E o gargalo dos modelos open source virou falta de conhecimento factual sobre bibliotecas, coisa que eu explico em detalhe na nova seção sobre a família Qwen. A aposta da distilação do Claude, aliás, deu um resultado bem frustrante que eu nunca tinha visto documentado nesses termos.</p>
<hr>
<p>Se você acompanhou meus <a href="/tags/vibecoding/">artigos anteriores sobre vibe coding</a>, sabe que passei os últimos dois meses numa maratona de mais de 500 horas usando Claude Opus como coding agent principal. Os resultados foram bons, como reportei na <a href="https://akitaonrails.com/2026/03/05/37-dias-de-imers%c3%a3o-em-vibe-coding-conclus%c3%a3o-quanto-a-modelos-de-neg%c3%b3cio/"target="_blank" rel="noopener">conclusão sobre modelos de negócio</a>. Mas ficou uma coceira: será que eu estou preso num único modelo? Tem alternativa real ao Claude Opus pra uso diário em projetos de verdade?</p>
<p>Tenho uma RTX 5090 com 32 GB de GDDR7. Sei que posso rodar os modelos open source mais recentes. Comprei um <a href="https://akitaonrails.com/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"target="_blank" rel="noopener">Minisforum MS-S1</a> com AMD Ryzen AI Max 395 e 128 GB de memória unificada, e montei um <a href="https://akitaonrails.com/2026/03/31/migrando-meu-home-server-com-claude-code/"target="_blank" rel="noopener">home server com Docker</a> pra servir modelos locais. A infraestrutura estava pronta. Faltava testar de verdade.</p>
<p>Construí um benchmark automatizado pra comparar modelos open source e comerciais em condições idênticas. 33 modelos configurados ao todo (25 da rodada original mais 8 adicionados na rerodada na NVIDIA), 27 executados, 16 completados de alguma forma. O código está no <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
<h2>O gargalo que ninguém explica: VRAM e KV Cache<span class="hx:absolute hx:-mt-20" id="o-gargalo-que-ninguém-explica-vram-e-kv-cache"></span>
    <a href="#o-gargalo-que-ningu%c3%a9m-explica-vram-e-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de falar dos resultados, preciso explicar por que rodar modelos grandes localmente é muito mais difícil do que parece.</p>
<p>Pega o Qwen3 32B como exemplo. O modelo em FP16 (precisão total) ocupa ~64 GB. Quantizado em Q4 (4 bits), cai pra ~19 GB. Então cabe nos 32 GB da minha RTX 5090, certo? Errado. Esse é só o peso do modelo. Falta a parte que ninguém conta pra você: o <strong>KV Cache</strong>.</p>
<p>KV Cache é a memória que o modelo usa pra &ldquo;lembrar&rdquo; o que já leu. Cada vez que processa um token (uma palavra ou pedaço de palavra), ele calcula dois vetores — K (key) e V (value) — pra cada camada de atenção. Esses vetores ficam armazenados pra que não precise recalcular tudo quando gera o próximo token. Sem isso, a geração seria quadraticamente lenta.</p>
<p>O KV Cache escala linearmente com o tamanho do contexto. A fórmula:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Memória KV = 2 × Camadas × Cabeças_KV × Dimensão_Cabeça × Bytes_por_Elemento × Tokens_no_Contexto</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra um modelo como o Llama 3.1 70B em BF16, isso dá ~0.31 MB por token. Parece pouco, até você perceber que um contexto de 128K tokens consome <strong>40 GB</strong> só de KV Cache. O modelo em si + KV Cache = muito mais VRAM do que a maioria das GPUs tem.</p>
<p>E pra uso real com coding agents, 128K tokens não é luxo — é necessidade. O agent precisa ler arquivos, manter histórico de conversação, receber output de comandos. Em sessões longas de benchmark, nossos modelos consumiram entre 39K e 156K tokens. Menos de 100K de contexto não é prático pra projetos do dia a dia.</p>
<p>O Google publicou o <a href="https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/"target="_blank" rel="noopener">TurboQuant</a> (ICLR 2026), que comprime o KV Cache pra 3 bits sem perda de acurácia — redução de 6x na memória e até 8x de speedup. Usa rotação aleatória de vetores (PolarQuant) seguida de um algoritmo de 1 bit nos resíduos. Funciona online durante inferência, comprimindo na escrita e descomprimindo na leitura. Ainda não está implementado nos runtimes que usamos (llama.cpp, Ollama), mas quando chegar vai mudar bastante a equação.</p>
<p>Pra quem quer se aprofundar nessa matemática de VRAM, recomendo <a href="https://x.com/TheAhmadOsman/status/2040103488714068245"target="_blank" rel="noopener">este link do Ahmad Osman</a> pro artigo &ldquo;GPU Memory Math for LLMs (2026 Edition)&rdquo;.</p>
<h2>O problema do hardware: memória não é toda igual<span class="hx:absolute hx:-mt-20" id="o-problema-do-hardware-memória-não-é-toda-igual"></span>
    <a href="#o-problema-do-hardware-mem%c3%b3ria-n%c3%a3o-%c3%a9-toda-igual" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>&ldquo;Mas eu tenho 128 GB de RAM!&rdquo; Legal, mas não é isso que importa. O que importa é largura de banda de memória, e a diferença entre tipos é absurda:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/memory-bandwidth.png" alt="Largura de banda de memória por tipo"  loading="lazy" /></p>
<p>A RTX 5090 tem 7x mais bandwidth que a memória LPDDR5x do meu Minisforum. Isso significa que mesmo que o modelo caiba na RAM unificada do AMD, a inferência vai ser proporcionalmente mais lenta. No meu Minisforum com LPDDR5x a 256 GB/s, o Qwen3 32B roda a ~7 tok/s. Na RTX 5090 a 1.792 GB/s, seria muito mais rápido — se coubesse inteiro na VRAM junto com o KV Cache.</p>
<p>A maioria das pessoas rodando modelos locais ainda está em DDR4. A 50 GB/s, modelos de 32B ficam praticamente unusáveis. E tem outro fator que pouca gente lembra: storage. Quando a RAM não dá conta e o sistema faz swap, a velocidade do armazenamento vira o gargalo:</p>
<table>
  <thead>
      <tr>
          <th>Storage</th>
          <th style="text-align: right">Velocidade Sequencial</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SATA SSD</td>
          <td style="text-align: right">~550 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen3</td>
          <td style="text-align: right">~3.500 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen4</td>
          <td style="text-align: right">~7.000 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen5</td>
          <td style="text-align: right">~12.000 MB/s</td>
      </tr>
  </tbody>
</table>
<p>De SATA pra NVMe Gen5 são 22x de diferença. Se você está fazendo offloading parcial pro disco (comum quando o modelo não cabe inteiro na GPU), NVMe Gen4 ou Gen5 faz diferença real. SATA é inviável.</p>
<p>Resumindo: rodar modelos locais não é só &ldquo;ter RAM suficiente&rdquo;. Precisa do tipo certo de memória, com a bandwidth certa, e storage rápido como fallback. Pra muita gente, um Mac Studio com memória unificada de alta bandwidth (até 800 GB/s nos M4 Ultra com 512 GB) seria a opção mais prática, mas custa mais de US$ 10.000. O AMD Ryzen AI Max é a alternativa mais acessível com memória unificada, mas o LPDDR5 fica limitado a 256 GB/s.</p>
<h2>Ollama vs llama.cpp: por que o Ollama falha pra benchmarks<span class="hx:absolute hx:-mt-20" id="ollama-vs-llamacpp-por-que-o-ollama-falha-pra-benchmarks"></span>
    <a href="#ollama-vs-llamacpp-por-que-o-ollama-falha-pra-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://ollama.com/"target="_blank" rel="noopener">Ollama</a> é a forma mais popular de rodar modelos locais. Instala, puxa o modelo, roda. Pra uso casual funciona. Mas quando tentei usar pra benchmarks automatizados, sessões longas sem intervenção humana, quebrou de 6 formas diferentes em 8 modelos:</p>
<ol>
<li>Descarrega o modelo no meio da sessão. Em runs longos, o Ollama decide que o modelo não está sendo usado e descarrega da GPU. O agent fica esperando resposta de um modelo que não existe mais.</li>
<li>Ignora o contexto solicitado. Você pede <code>num_ctx=131072</code>, o Ollama aceita, e no meio do run reverte pro padrão sem avisar.</li>
<li>Lifecycle instável. Pedir <code>keep_alive: 0</code> pra descarregar nem sempre funciona. O modelo fica residente e bloqueia o próximo.</li>
<li>Formatos incompatíveis. Variantes bf16 nativas do Ollama falhavam, enquanto o mesmo modelo como GGUF Q8 do HuggingFace funcionava sem problemas.</li>
</ol>
<p>A solução: migrar pro <a href="https://github.com/mostlygeek/llama-swap"target="_blank" rel="noopener">llama-swap</a>, um wrapper Go que gerencia processos llama.cpp com hot-swap. Chega um request pra um modelo diferente do que está carregado, ele mata o processo atual e inicia o novo. Sem negociação de contexto, sem lifecycle instável.</p>
<p>O llama-swap resolveu o carregamento de 6 dos 8 modelos que falharam no Ollama:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Ollama</th>
          <th>llama-swap</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>HTTP 500</td>
          <td>47.6 tok/s</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>Sem output</td>
          <td>47.4 tok/s</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>Descarregou</td>
          <td>17.5 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Output off-spec</td>
          <td>49.7 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Context drift</td>
          <td>23.1 tok/s</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Model not found</td>
          <td>78.3 tok/s</td>
      </tr>
  </tbody>
</table>
<p>Mas o llama-swap não é mágico.</p>
<h2>Por que &ldquo;só usar llama.cpp&rdquo; não resolve tudo<span class="hx:absolute hx:-mt-20" id="por-que-só-usar-llamacpp-não-resolve-tudo"></span>
    <a href="#por-que-s%c3%b3-usar-llamacpp-n%c3%a3o-resolve-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O llama.cpp resolve os problemas de lifecycle do Ollama mas traz os próprios:</p>
<p>Cada modelo precisa de flags específicas. GLM e Qwen 3.5 emitem <code>&lt;think&gt;</code> tags que quebram clientes se você não passar <code>--reasoning-format none</code>. Gemma 4 precisa de build b8665+ pro parser de tool calls funcionar.</p>
<p>Nem todo modelo suporta tool calling. O llama.cpp precisa de um parser dedicado pro formato de tool call de cada modelo. O Llama 4 Scout usa um formato &ldquo;pythonic&rdquo; (<code>[func(param=&quot;value&quot;)]</code>) que o llama.cpp simplesmente não parseia, emite como texto puro. O vLLM tem parser pra isso, o llama.cpp não.</p>
<p>E tem os repetition loops. O Gemma 4, mesmo com o parser certo, entra em loop infinito depois de ~11 tool calls em sessões longas. É um <a href="https://github.com/ggml-org/llama.cpp/issues/21375"target="_blank" rel="noopener">bug conhecido</a> que o PR #21418 não resolveu completamente.</p>
<p>Compatibilidade de tool calling por modelo:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Tool Calling</th>
          <th>Flags Necessárias</th>
          <th>Resultado no Benchmark</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>Parcial (b8665+)</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Loop infinito após ~11 steps</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>Sim</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>2029 arquivos, terminou mid-tool-call</td>
      </tr>
      <tr>
          <td>Qwen 3.5 (35B, 122B)</td>
          <td>Sim</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Completou com sucesso</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Sim</td>
          <td><code>--jinja</code></td>
          <td>Completou (melhor resultado local)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Sim</td>
          <td><code>--jinja</code></td>
          <td>Tool calls ok, mas app no diretório errado</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>Não</td>
          <td>—</td>
          <td>Sem parser no llama.cpp</td>
      </tr>
  </tbody>
</table>
<p>No fim das contas, llama.cpp é melhor que Ollama pra runs automatizados, mas &ldquo;plug and play&rdquo; não é. Cada modelo exige configuração específica, e alguns simplesmente não funcionam pra agentic coding ainda.</p>
<h2>Reasoning: modelos que pensam vs modelos que chutam<span class="hx:absolute hx:-mt-20" id="reasoning-modelos-que-pensam-vs-modelos-que-chutam"></span>
    <a href="#reasoning-modelos-que-pensam-vs-modelos-que-chutam" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma diferença entre modelos que vale explicar: reasoning. A ideia é que o modelo &ldquo;pensa antes de responder&rdquo; em vez de gerar tokens direto da esquerda pra direita. Modelos com reasoning fazem uma etapa interna de cadeia de pensamento (chain-of-thought) onde avaliam o problema, consideram alternativas, planejam, e só depois emitem a resposta.</p>
<p>Na prática, isso aparece como <code>&lt;think&gt;...&lt;/think&gt;</code> tags no output, blocos de texto que o modelo gera pra si mesmo, que não devem ir pro usuário final. Claude Opus 4.6, GPT 5.4, DeepSeek V3.2 e a linha Qwen 3.5 suportam reasoning nativamente. Os menores (Gemma 4, GPT OSS 20B, modelos mais antigos) não têm essa capacidade.</p>
<p>Por que importa pra coding? Quando um coding agent recebe &ldquo;construa um app Rails com 9 componentes&rdquo;, ele precisa decompor a tarefa em passos, decidir a ordem, antecipar dependências, adaptar quando algo falha. Sem reasoning, o modelo gera código sequencialmente sem planejamento. Funciona pra tarefas simples, desmorona em projetos com partes interdependentes.</p>
<p>No benchmark, a diferença ficou clara:</p>
<ul>
<li>GPT OSS 20B (sem reasoning, 20B parâmetros) criou o app no diretório errado. Não conseguiu manter o contexto das instruções de workspace enquanto gerava código.</li>
<li>Qwen 3 32B tem reasoning, mas a 7 tok/s era lento demais. Os tokens de &ldquo;pensamento&rdquo; aumentam o tempo de geração.</li>
<li>Gemma 4 31B, sem reasoning treinado pra uso agentic, entrou em loops repetitivos de tool calling.</li>
<li>GLM 5 (cloud, 745B MoE) com reasoning e 44B parâmetros ativos, completou limpo e usou a API correta.</li>
</ul>
<p>Tem um trade-off: reasoning consome tokens extras (os <code>&lt;think&gt;</code> blocks), que ocupam VRAM no KV Cache e desaceleram a geração. Por isso flags como <code>--reasoning-format none</code> são necessárias no llama.cpp. Alguns clientes não sabem o que fazer com reasoning tokens e quebram. Modelos que emitem reasoning sem que o runtime espere podem gerar lixo no output.</p>
<p>E reasoning não é algo que você &ldquo;liga&rdquo; num modelo qualquer. É uma capacidade treinada com reinforcement learning em cima do modelo base, usando dados de problemas que exigem raciocínio multi-step. Os modelos open source menores (20B-35B) tipicamente não passaram por esse treinamento, ou passaram em escala menor. Eles sabem gerar código, mas não sabem <em>planejar</em> código. Em tarefas que exigem 50+ tool calls coordenadas, essa diferença mata.</p>
<h2>O benchmark: metodologia<span class="hx:absolute hx:-mt-20" id="o-benchmark-metodologia"></span>
    <a href="#o-benchmark-metodologia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra comparar modelos de forma justa, construí um harness automatizado em Python. Cada modelo recebe exatamente o mesmo prompt: construir uma aplicação Ruby on Rails completa, um chat SPA tipo ChatGPT usando o gem RubyLLM, com Hotwire/Stimulus/Turbo Streams, Tailwind CSS, testes Minitest, ferramentas de CI (Brakeman, RuboCop, SimpleCov, bundle-audit), Dockerfile, docker-compose e README.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/crush-screenshot.png" alt="Crush — CLI de coding da Charm"  loading="lazy" /></p>
<p>O runner é o <a href="https://github.com/sst/opencode"target="_blank" rel="noopener">opencode</a>, uma das CLIs de coding open source mais populares, competindo com Claude Code e Codex. Vale um esclarecimento: o autor original saiu do projeto e foi tocar o <a href="https://github.com/charmbracelet/crush"target="_blank" rel="noopener">Crush</a> junto com a equipe da <a href="https://charm.sh/"target="_blank" rel="noopener">Charm</a> (o pessoal por trás do Bubble Tea, Lip Gloss e várias outras ferramentas de terminal em Go), mas o restante do time original continuou evoluindo o opencode normalmente — o projeto não foi descontinuado. Hoje os dois coexistem. Se você leu <a href="https://akitaonrails.com/2026/01/09/omarchy-3-um-dos-melhores-agentes-pra-programacao-crush/"target="_blank" rel="noopener">meu artigo sobre o Crush</a>, já conhece essa vertente. Ambos rodam em tudo: macOS, Linux, Windows, Android, FreeBSD.</p>
<p>Na verdade, tentei usar o Crush primeiro pro benchmark. O problema: ele anunciava uma flag <code>--yolo</code> no help pra auto-aprovar todas as ações (essencial pra runs automatizados sem intervenção humana), mas na hora de rodar, rejeitava a flag. Sem auto-approve, não dá pra fazer benchmark desacompanhado. O opencode, por outro lado, tinha o modo <code>opencode run --agent build --format json</code> que emite eventos JSON com session IDs e contagem de tokens, perfeito pra automação. Então ficamos com o opencode.</p>
<p>Escolhi o opencode (e não Claude Code ou Codex) por dois motivos:</p>
<ol>
<li>Neutralidade. Claude Code é otimizado pra modelos Anthropic. Codex é otimizado pra modelos OpenAI. O opencode é agnóstico, mesma interface pra todos.</li>
<li>Automação. O opencode expõe um formato JSON legível por máquina. Claude Code e Codex não têm interface equivalente pra benchmarking externo.</li>
</ol>
<p>Modelos cloud rodaram em duas fases: fase 1 (construir o app) e fase 2 (validar boot local, docker build, docker compose). Modelos locais rodaram só fase 1.</p>
<p>Detalhe que vale mencionar: o benchmark inteiro custou menos de $10 em tokens no OpenRouter. Tirando o GPT 5.4 Pro que torrou $7.20 pra falhar, os outros 11 modelos cloud somaram uns $2.50 no total. Modelos locais custam só eletricidade. O ponto é: rodar seu próprio benchmark é barato. Se você quer saber se um modelo funciona pro seu caso de uso, gaste os $2 e teste. O código do harness está no GitHub, é só trocar o prompt pelo seu projeto.</p>
<h2>Por que o GPT 5.4 falhou no benchmark (mas não na vida real)<span class="hx:absolute hx:-mt-20" id="por-que-o-gpt-54-falhou-no-benchmark-mas-não-na-vida-real"></span>
    <a href="#por-que-o-gpt-54-falhou-no-benchmark-mas-n%c3%a3o-na-vida-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O GPT 5.4 Pro é o único modelo cloud que falhou consistentemente no nosso benchmark. Duas runs separadas, mesmo resultado: o modelo gerou arquivos mas nunca chegou a <code>finish_reason: stop</code>. Sempre terminava com <code>finish_reason: tool-calls</code> — queria continuar chamando tools mas o loop se quebrava.</p>
<p>Pra quem não sabe: tool calling é quando um LLM precisa executar uma ação (ler um arquivo, rodar um comando, editar código) e emite uma &ldquo;chamada de ferramenta&rdquo; num formato estruturado. O client (opencode, Claude Code, Codex) interpreta, executa, e devolve o resultado pro modelo. Cada provedor tem seu formato: a Anthropic usa <code>tool_use</code> blocks, a OpenAI usa <code>function_calling</code> com JSON schemas, o Google usa <code>FunctionCall</code>.</p>
<p>O GPT 5.4 é fortemente treinado pro formato nativo de function calling da OpenAI — <code>tool_choice</code>, <code>tools</code> com JSON schemas proprietários. Quando o benchmark rota por opencode → OpenRouter → GPT 5.4, os schemas de tools são traduzidos em cada hop. Se o GPT emite tool calls num formato que o OpenRouter ou opencode não parseia corretamente, o loop do agent quebra.</p>
<p>A evidência: todos os outros modelos cloud (Claude Opus, Claude Sonnet, Kimi K2.5, DeepSeek V3.2, MiniMax M2.7, GLM 5, Qwen 3.6 Plus, Step 3.5 Flash) terminaram com <code>finish_reason: stop</code>. Só o GPT termina com <code>finish_reason: tool-calls</code>.</p>
<p>Comparação justa do GPT 5.4 exigiria rodar no ambiente nativo. E agora temos essa comparação: construímos suporte pra automatizar o Codex CLI (<code>codex exec</code> com <code>--dangerously-bypass-approvals-and-sandbox</code> e reasoning effort <code>xhigh</code>) e rodamos o mesmo benchmark. O GPT 5.4 completou em 22 minutos, gerou todos os 9 artefatos, escreveu 22 testes com a arquitetura mais sofisticada do benchmark inteiro: injeção de dependência do client RubyLLM, PORO models pra <code>ChatMessage</code> e <code>PromptSubmission</code>, session-backed <code>ChatSession</code> com TTL e trimming de mensagens, bin/ci script.</p>
<p>Mas o código quebra na segunda mensagem. O GPT 5.4 usa <code>chat.add_message(role:, content:)</code> com keyword arguments em vez de hash posicional <code>chat.add_message({role:, content:})</code> — isso causa <code>ArgumentError: wrong number of arguments (given 0, expected 1)</code> na primeira troca multi-turn. A primeira mensagem funciona (usa <code>chat.ask</code> direto), o multi-turn não.</p>
<p>E o custo: <strong>7.6 milhões de tokens</strong> no reasoning effort xHigh. São 65x mais tokens que o Opus 4.7 (118K) pro mesmo benchmark. Custo estimado de ~$16 por run, contra ~$1.10 do Opus. Gastou 15x mais e ainda errou a convenção de chamada. Nem budget de token gigantesco nem reasoning effort máximo garante acerto factual numa API de gem. Conhecimento de API é memória binária, não é função de quanto o modelo &ldquo;pensa&rdquo;.</p>
<p>Com dados objetivos em mãos, o GPT 5.4 sai do Tier 1 e vai pro Tier 2. A arquitetura que ele gera é melhor que a do Opus em termos de design patterns. Mas o código precisa de correção pra rodar multi-turn, e o custo por token é proibitivo.</p>
<p>O Sonnet e o Opus via opencode/OpenRouter também provavelmente não foram usados ao máximo da capacidade. O Claude Code oferece suporte nativo de tools que o opencode não replica — o que significa que os resultados do benchmark representam um piso, não um teto, pra esses modelos.</p>
<h2>Modelos open source: a realidade vs a narrativa<span class="hx:absolute hx:-mt-20" id="modelos-open-source-a-realidade-vs-a-narrativa"></span>
    <a href="#modelos-open-source-a-realidade-vs-a-narrativa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem muita gente dizendo que modelos open source já alcançaram os comerciais e que dá pra rodar seu próprio &ldquo;Claude&rdquo; em casa. Na prática, não é bem assim.</p>
<p>A escala não é comparável. Modelos frontier como Claude Opus 4.6 e GPT 5.4 são closed-source, mas estimativas indicam que estão na faixa de centenas de bilhões a trilhões de parâmetros, treinados com compute e dados que nenhuma empresa open source replica. Os melhores modelos que cabem num hardware razoável são:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: right">Parâmetros Totais</th>
          <th style="text-align: right">Parâmetros Ativos</th>
          <th>Arquitetura</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: right">35B</td>
          <td style="text-align: right">3B</td>
          <td>MoE (A3B)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B</td>
          <td style="text-align: right">27B</td>
          <td style="text-align: right">27B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: right">32B</td>
          <td style="text-align: right">32B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: right">122B</td>
          <td style="text-align: right">122B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: right">20B</td>
          <td style="text-align: right">20B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">31B</td>
          <td style="text-align: right">31B</td>
          <td>Dense</td>
      </tr>
  </tbody>
</table>
<p>Correção pós-publicação: o Qwen 3.5 35B na verdade é o <strong>35B-A3B</strong>, um MoE com só 3B de parâmetros ativos por token (não denso, como eu tinha colocado originalmente). Isso explica por que ele roda relativamente rápido pro tamanho. E pra quem tem 24 GB de VRAM, o modelo recomendado pela própria <a href="https://unsloth.ai/docs/models/qwen3.5#qwen3.5-27b"target="_blank" rel="noopener">Unsloth</a> é o <strong>Qwen 3.5 27B</strong> denso — esse eu não cheguei a testar no benchmark, mas fica como recomendação. Pra quem quer se aprofundar em modelos locais, vale acompanhar o trabalho do <a href="https://x.com/sudoingX"target="_blank" rel="noopener">@sudoingX</a>, que tem feito experimentação séria nessa frente. Valeu <a href="https://x.com/thpmacedo/status/2041105305111502927"target="_blank" rel="noopener">@thpmacedo</a> pelo toque.</p>
<p>Mesmo os maiores modelos open source MoE (Mixture of Experts) que as empresas disponibilizam publicamente ativam poucos parâmetros por token:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: right">Parâmetros Totais</th>
          <th style="text-align: right">Parâmetros Ativos</th>
          <th>Notas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">1T</td>
          <td style="text-align: right">32B</td>
          <td>384 experts, top-8 + shared</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">745B</td>
          <td style="text-align: right">44B</td>
          <td>256 experts, 8 ativados</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">671B</td>
          <td style="text-align: right">37B</td>
          <td>Sparse Attention</td>
      </tr>
      <tr>
          <td>Qwen 3.5 397B</td>
          <td style="text-align: right">397B</td>
          <td style="text-align: right">17B</td>
          <td>MoE, cloud-only</td>
      </tr>
  </tbody>
</table>
<p>Esses modelos grandes não são self-hostáveis. O Kimi K2.5 com 1T parâmetros precisa de clusters de GPUs com centenas de GBs de VRAM. O GLM 5 com 745B idem. Mesmo que a Alibaba ou a Z.AI liberem os pesos (e algumas liberam), ninguém tem hardware doméstico pra rodar eles.</p>
<p>O que cabe na sua GPU doméstica são os modelos de 20B-35B — e esses têm limitações reais.</p>
<h3>O que cada modelo local fez no benchmark<span class="hx:absolute hx:-mt-20" id="o-que-cada-modelo-local-fez-no-benchmark"></span>
    <a href="#o-que-cada-modelo-local-fez-no-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Resultados da rodada original no AMD Strix Halo:</p>
<p><strong>Qwen 3 Coder Next (30B)</strong> — Completou em 17 minutos no Strix, gerou 1675 arquivos, app Rails com todos os artefatos. Mas só 3 testes. E o mais importante: inventou <code>RubyLLM::Client.new</code>, uma classe que não existe no gem. O app não roda.</p>
<p><strong>Qwen 3.5 35B</strong> — Completou em 28 minutos no Strix, 1478 arquivos, 11 testes. Usou <code>RubyLLM.chat</code> sem parâmetro de modelo — funciona só se o default estiver configurado. Sem mocking de LLM nos testes.</p>
<p><strong>Qwen 3.5 122B</strong> — Completou em 43 minutos no Strix, 1503 arquivos, 16 testes. Mas ignorou o gem RubyLLM completamente e construiu um cliente HTTP customizado pro OpenRouter. O prompt pedia explicitamente pra usar ruby_llm.</p>
<p><strong>GLM 4.7 Flash (local, Strix)</strong> — Produziu 2029 arquivos com todos os artefatos, mas a sessão terminou mid-tool-call. O modelo cloud (GLM 5) funciona perfeitamente.</p>
<p><strong>Gemma 4 31B (Strix)</strong> — Loop infinito de tool calls depois de ~11 steps produtivos. Bug conhecido do llama.cpp.</p>
<p><strong>GPT OSS 20B (Strix)</strong> — Criou o app Rails no diretório errado (<code>project/app/</code> em vez de <code>project/</code>). Um modelo de 20B não segue instruções de workspace de forma confiável.</p>
<p><strong>Qwen 3 32B (Strix)</strong> — Lento demais (7.3 tok/s). Hardware não dá conta.</p>
<p>E os resultados da rerodada na NVIDIA RTX 5090 (todos com Q3_K_M ou Q4_K_M e contexto entre 64k e 128k pra caber nos 32 GB de VRAM):</p>
<p><strong>Qwen 3.5 35B-A3B (5090)</strong> — 5 minutos a 273 tok/s. Projeto Rails reconhecível, entry point <code>RubyLLM.chat(model:)</code> está certo, mas alucina <code>chat.add_message(role:, content:)</code> e <code>chat.complete</code> em vez de <code>.ask</code>. Dá pra arrumar em 1-2 follow-ups. O melhor candidato a &ldquo;OSS local que vale a pena tentar&rdquo;.</p>
<p><strong>Qwen 3.5 27B Claude-distilado (5090)</strong> — 12 minutos a 129 tok/s. Estilo Claude impecável, alucinação total da API (<code>RubyLLM::Chat.new.with_model{}</code>, <code>add_message</code>, <code>response.text</code>). Mais detalhes na seção de distilação abaixo.</p>
<p><strong>Qwen 3 Coder 30B (5090)</strong> — 6 minutos a 145 tok/s. Devolveu uma string mockada hardcoded em vez de chamar a API. Tier 3 inutilizável.</p>
<p><strong>Qwen 2.5 Coder 32B (5090)</strong> — 90 minutos de timeout, zero arquivos. Modelo girou sem nunca chamar tool de write.</p>
<p><strong>Qwen 3 32B (5090)</strong> — 4 minutos a 69 tok/s, scaffold parcial, errors. Versão geral é melhor que a Coder mas ainda quebra.</p>
<p><strong>Gemma 4 31B (5090)</strong> — 8 minutos a 213 tok/s. Mesmo loop de repetição que tinha no Strix. Bug do llama.cpp não é hardware.</p>
<p><strong>Qwen 3.5 27B Sushi Coder RL (5090)</strong> — Falha de infra (<code>ProviderModelNotFoundError</code>), não foi possível avaliar. Refazer numa próxima rodada.</p>
<p><strong>GPT OSS 20B (5090)</strong> — Tirado da rodada por regressão recente do llama.cpp main no parser de tool calls da família harmony. Os logs mostram <code>Failed to parse input at pos 755: &lt;|channel|&gt;...</code> em sessões multi-turn. Funcionava no Strix com o llama.cpp <code>b8643</code>, quebrou no main de hoje. Aguardando upstream consertar.</p>
<h2>Modelos cloud: o que funciona de verdade<span class="hx:absolute hx:-mt-20" id="modelos-cloud-o-que-funciona-de-verdade"></span>
    <a href="#modelos-cloud-o-que-funciona-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dos 12 modelos que completaram o benchmark, todos geraram um projeto Rails reconhecível com todos os artefatos pedidos (Gemfile, routes, views, JS, testes, README, Dockerfile, docker-compose). 9 de 9 no checklist de completude.</p>
<p>Mas aí vem a pergunta que importa: o código roda?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/cost-vs-quality.png" alt="Custo vs tempo — e o código funciona?"  loading="lazy" /></p>
<p>A API correta do RubyLLM é simples:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="s2">&#34;Hello&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</span>  <span class="c1"># =&gt; &#34;Hi there!&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>8 dos 12 modelos inventaram APIs que não existem. O padrão mais comum: alucinação de uma interface que não é a do gem real:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>O que Inventou</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V3.2</td>
          <td><code>RubyLLM::Client.new</code> — classe inexistente</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td><code>RubyLLM::Client.new</code> — mesmo erro</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td><code>Openrouter::Client</code> — gem inexistente</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td><code>add_message()</code> e <code>complete()</code> — métodos inexistentes</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td><code>RubyLLM.chat(messages: [...])</code> — assinatura inexistente</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td><code>chat.add_message()</code> — método inexistente</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td><code>RubyLLM::Chat.new()</code> e <code>add_message()</code> — API interna, não pública</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td>Ignora o gem completamente — usa <code>OpenAI::Client</code> (ruby-openai) batendo direto na URL do OpenRouter</td>
      </tr>
  </tbody>
</table>
<p>Os modelos que acertaram — os dois Claudes, o GLM 5 e o GLM 5.1 — usaram o padrão simples de duas etapas (<code>chat = RubyLLM.chat(model:)</code> depois <code>chat.ask(message)</code>). Os que erraram tentaram fazer o RubyLLM parecer com o SDK Python da OpenAI, que é outra coisa. O Grok 4.20 foi o caso mais cínico: nem tentou usar o gem, foi direto pro <code>OpenAI::Client</code> apontando pra URL do OpenRouter, ignorando o pedido explícito do prompt.</p>
<p>E os testes? Só Opus, Sonnet, GLM 5 e GLM 5.1 fizeram mocking correto das chamadas LLM. Todos os outros ou batiam na API real (que falha sem chave) ou mockavam a API inventada (testes passam mas não provam nada). Contagem de testes é uma métrica enganosa: o Kimi K2.5 escreveu 37 testes, mais que qualquer outro, mas nenhum testa a funcionalidade real porque a API que ele usa não existe.</p>
<h3>Tabela de viabilidade real<span class="hx:absolute hx:-mt-20" id="tabela-de-viabilidade-real"></span>
    <a href="#tabela-de-viabilidade-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: center">API Correta?</th>
          <th style="text-align: center">Roda?</th>
          <th style="text-align: center">Mocking nos Testes?</th>
          <th>Problema</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Claude Opus 4.7</strong></td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center"><strong>Sim</strong></td>
          <td style="text-align: center">Sim (FakeChat)</td>
          <td>Implementação limpa, 28 testes</td>
      </tr>
      <tr>
          <td><strong>Claude Sonnet 4.6</strong></td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center"><strong>Sim</strong></td>
          <td style="text-align: center">Sim (mocha)</td>
          <td>Implementação limpa</td>
      </tr>
      <tr>
          <td><strong>Claude Opus 4.6</strong></td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center"><strong>Sim</strong></td>
          <td style="text-align: center">Sim (mocha)</td>
          <td>Implementação limpa</td>
      </tr>
      <tr>
          <td><strong>GLM 5</strong></td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center"><strong>Sim</strong></td>
          <td style="text-align: center">Sim (mocha)</td>
          <td>API correta, funciona</td>
      </tr>
      <tr>
          <td><strong>GLM 5.1</strong></td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center"><strong>Sim</strong></td>
          <td style="text-align: center">Sim</td>
          <td>API correta, funciona</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Parcial</td>
          <td style="text-align: center">Só 1ª msg</td>
          <td style="text-align: center">Sim (FakeChat)</td>
          <td><code>add_message(role:, content:)</code> com keyword args em vez de hash posicional — quebra no multi-turn</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Sim</strong>*</td>
          <td style="text-align: center">Não</td>
          <td>Bypassa RubyLLM, usa HTTP direto</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Sim</strong>*</td>
          <td style="text-align: center">Não</td>
          <td>Bypassa RubyLLM, usa <code>OpenAI::Client</code> direto</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Parcial</td>
          <td style="text-align: center">Só 1ª msg</td>
          <td style="text-align: center">Não</td>
          <td><code>add_message()</code> não existe</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td style="text-align: center">Parcial</td>
          <td style="text-align: center">Talvez</td>
          <td style="text-align: center">Não</td>
          <td>Sem parâmetro de modelo</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Não</td>
          <td><code>add_message()</code>/<code>complete()</code> inventados</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Não</td>
          <td>Assinatura de <code>RubyLLM.chat</code> errada</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Não</td>
          <td><code>RubyLLM::Client</code> inexistente</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Não</td>
          <td><code>RubyLLM::Client</code> inexistente</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Mock errado</td>
          <td><code>RubyLLM::Chat.new()</code> e <code>add_message()</code> inexistentes</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center"><strong>Não</strong></td>
          <td style="text-align: center">Não</td>
          <td><code>Openrouter::Client</code> gem inexistente</td>
      </tr>
  </tbody>
</table>
<p>*Step 3.5 Flash funciona chamando a API REST do OpenRouter direto com <code>Net::HTTP</code>, contornando completamente o gem que o prompt pedia.</p>
<p>Agora, isso não quer dizer que esses modelos são inúteis. Se você pegar o Kimi K2.5 ou o DeepSeek V3.2 e mandar &ldquo;a classe RubyLLM::Client não existe, corrija pra usar a API real do gem&rdquo;, provavelmente vai corrigir. Um ou dois follow-ups e o projeto fica funcional. A maioria dos modelos que falharam aqui conseguiria entregar um projeto rodando com mais algumas rodadas de conversa.</p>
<p>Só que aí está o trade-off. Com Opus ou GPT 5.4, o primeiro output já funciona. Pede, entrega, testa, roda. Com os modelos mais baratos, você vai gastar tempo corrigindo alucinações de API, debugando código que &ldquo;parece certo&rdquo; mas crasha, guiando o modelo na direção certa. Cada rodada dessas são 10-30 minutos. Três rodadas extras e você gastou uma hora do seu tempo pra economizar $0.90 de tokens.</p>
<p>Economiza dólar, gasta tempo. E tempo é dinheiro. Pra quem está aprendendo ou explorando sem pressa, essa troca pode fazer sentido. Pra quem precisa entregar, os modelos frontier se pagam rápido.</p>
<h3>Comparação dos modelos que funcionam<span class="hx:absolute hx:-mt-20" id="comparação-dos-modelos-que-funcionam"></span>
    <a href="#compara%c3%a7%c3%a3o-dos-modelos-que-funcionam" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Provedor</th>
          <th style="text-align: right">Tempo</th>
          <th style="text-align: right">Testes</th>
          <th style="text-align: right">Custo/Run</th>
          <th>vs Opus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">28</td>
          <td style="text-align: right">~$1.10</td>
          <td>Novo baseline</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">30</td>
          <td style="text-align: right">~$0.63</td>
          <td>40% mais barato, mais testes</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">~$1.05</td>
          <td>Baseline anterior</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">7</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% mais barato</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direto</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% mais barato, mais testes que o GLM 5</td>
      </tr>
  </tbody>
</table>
<h3>Ranking completo por tempo e tokens<span class="hx:absolute hx:-mt-20" id="ranking-completo-por-tempo-e-tokens"></span>
    <a href="#ranking-completo-por-tempo-e-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/time-to-complete.png" alt="Tempo de conclusão por modelo"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Provedor</th>
          <th style="text-align: right">Tempo</th>
          <th style="text-align: right">Tokens Totais</th>
          <th style="text-align: right">Tok/s</th>
          <th style="text-align: right">Custo/Run</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Grok 4.20</td>
          <td>OpenRouter</td>
          <td style="text-align: right">8m</td>
          <td style="text-align: right">63.457</td>
          <td style="text-align: right">412.54</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">104.034</td>
          <td style="text-align: right">128.28</td>
          <td style="text-align: right">~$0.50</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">79.743</td>
          <td style="text-align: right">574.52</td>
          <td style="text-align: right">~$0.05</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">136.806</td>
          <td style="text-align: right">347.18</td>
          <td style="text-align: right">~$1.05</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">127.067</td>
          <td style="text-align: right">532.26</td>
          <td style="text-align: right">~$0.63</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">59.378</td>
          <td style="text-align: right">400.01</td>
          <td style="text-align: right">~$0.11</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">88.940</td>
          <td style="text-align: right">182.91</td>
          <td style="text-align: right">Grátis</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">118.216</td>
          <td style="text-align: right">328.24</td>
          <td style="text-align: right">~$1.10</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direto</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">81.666</td>
          <td style="text-align: right">166.62</td>
          <td style="text-align: right">~$0.13</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td>Codex CLI</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">7.643.800</td>
          <td style="text-align: right">5.824.56</td>
          <td style="text-align: right">~$16.00</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Local</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">39.054</td>
          <td style="text-align: right">37.49</td>
          <td style="text-align: right">Eletricidade</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Local</td>
          <td style="text-align: right">28m</td>
          <td style="text-align: right">76.919</td>
          <td style="text-align: right">46.03</td>
          <td style="text-align: right">Eletricidade</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">29m</td>
          <td style="text-align: right">63.638</td>
          <td style="text-align: right">160.14</td>
          <td style="text-align: right">~$0.07</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td>OpenRouter</td>
          <td style="text-align: right">38m</td>
          <td style="text-align: right">156.267</td>
          <td style="text-align: right">242.11</td>
          <td style="text-align: right">~$0.02</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Local</td>
          <td style="text-align: right">43m</td>
          <td style="text-align: right">57.472</td>
          <td style="text-align: right">22.41</td>
          <td style="text-align: right">Eletricidade</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td>OpenRouter</td>
          <td style="text-align: right">60m</td>
          <td style="text-align: right">115.278</td>
          <td style="text-align: right">53.37</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
  </tbody>
</table>
<p>O DeepSeek V3.2 é o mais lento apesar de ser cloud — não tem prompt caching, então reenvia o contexto completo a cada turno.</p>
<h3>Eficiência de tokens e cache<span class="hx:absolute hx:-mt-20" id="eficiência-de-tokens-e-cache"></span>
    <a href="#efici%c3%aancia-de-tokens-e-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Modelos com prompt caching pagam muito menos tokens efetivos:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-efficiency.png" alt="Eficiência de tokens: cache vs novos"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: right">Tokens Totais</th>
          <th style="text-align: right">Cache Lido</th>
          <th style="text-align: right">Tokens Novos Efetivos</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">127.067</td>
          <td style="text-align: right">126.429</td>
          <td style="text-align: right">638</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: right">118.216</td>
          <td style="text-align: right">116.824</td>
          <td style="text-align: right">1.392</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">136.806</td>
          <td style="text-align: right">135.976</td>
          <td style="text-align: right">830</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">59.378</td>
          <td style="text-align: right">58.240</td>
          <td style="text-align: right">1.138</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: right">81.666</td>
          <td style="text-align: right">81.216</td>
          <td style="text-align: right">450</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: right">63.457</td>
          <td style="text-align: right">62.400</td>
          <td style="text-align: right">1.057</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">104.034</td>
          <td style="text-align: right">98.129</td>
          <td style="text-align: right">5.905</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: right">7.643.800</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">7.643.800</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">115.278</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">115.278</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">63.638</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">63.638</td>
      </tr>
  </tbody>
</table>
<h2>Velocidade: o abismo entre cloud e local<span class="hx:absolute hx:-mt-20" id="velocidade-o-abismo-entre-cloud-e-local"></span>
    <a href="#velocidade-o-abismo-entre-cloud-e-local" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem um aspecto que as tabelas de custo escondem: velocidade de inferência. E a diferença é brutal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/speed-comparison.png" alt="Velocidade de inferência por modelo"  loading="lazy" /></p>
<p>O Claude Sonnet gera 532 tok/s. O Qwen 3.5 122B rodando local no meu Minisforum (AMD Strix Halo) gera 22 tok/s. São 24x de diferença. Na prática, o que o Sonnet faz em 16 minutos, o Qwen 3.5 122B leva 43 minutos. O Qwen 3 Coder Next a 37 tok/s é o mais rápido dos locais no Strix e mesmo assim é 14x mais lento que o Sonnet.</p>
<p>E não é só tempo de relógio. Quando você está num loop de coding interativo — pede uma mudança, espera o output, testa, pede outra — a velocidade do modelo define seu ritmo. A 37 tok/s, cada resposta longa te faz esperar 30-60 segundos. A 530 tok/s, aparece quase instantaneamente. Ao longo de um dia, você sente.</p>
<p>O DeepSeek V3.2 é um caso curioso: é cloud mas roda a 53 tok/s, mais lento que o Qwen 3.5 35B local no Strix (46 tok/s). O motivo é que o DeepSeek não tem prompt caching — reenvia o contexto completo a cada turno, estrangulando o throughput. Pagar por um modelo cloud mais lento que rodar local não faz sentido nenhum.</p>
<p>Modelos locais são grátis em tokens, mas pagam em tempo. No AMD Strix, essa conta era inviável pra todos os Qwen que testei: dois minutos esperando uma resposta longa, multiplicado por 50 turnos, é tarde inteira. Mas isso muda quando o hardware muda, e foi por isso que rerodei a parte local do benchmark numa máquina diferente.</p>
<h2>AMD Strix Halo vs NVIDIA RTX 5090: o que muda quando a banda de memória dobra<span class="hx:absolute hx:-mt-20" id="amd-strix-halo-vs-nvidia-rtx-5090-o-que-muda-quando-a-banda-de-memória-dobra"></span>
    <a href="#amd-strix-halo-vs-nvidia-rtx-5090-o-que-muda-quando-a-banda-de-mem%c3%b3ria-dobra" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra checar se o gargalo era hardware ou modelo, peguei os mesmos modelos Qwen e refiz o benchmark numa workstation com NVIDIA RTX 5090 (Blackwell, 32 GB GDDR7, 1.792 GB/s de banda). Os números mudam de uma forma que vale ver com calma.</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: right">AMD Strix (LPDDR5x)</th>
          <th style="text-align: right">NVIDIA 5090 (GDDR7)</th>
          <th style="text-align: right">Speedup</th>
          <th style="text-align: right">Tempo total no 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3 32B (denso)</td>
          <td style="text-align: right">7 tok/s</td>
          <td style="text-align: right">69 tok/s</td>
          <td style="text-align: right">~10x</td>
          <td style="text-align: right">4 min</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B (Coder)</td>
          <td style="text-align: right">37 tok/s</td>
          <td style="text-align: right">145 tok/s</td>
          <td style="text-align: right">~4x</td>
          <td style="text-align: right">6 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B (MoE)</td>
          <td style="text-align: right">46 tok/s</td>
          <td style="text-align: right"><strong>273 tok/s</strong></td>
          <td style="text-align: right">~6x</td>
          <td style="text-align: right">5 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude (distilado)</td>
          <td style="text-align: right">timeout 90m</td>
          <td style="text-align: right">129 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">12 min</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">(não testei no Strix)</td>
          <td style="text-align: right">213 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">8 min</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: right">(não testei no Strix)</td>
          <td style="text-align: right">2.86 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">timeout 90m</td>
      </tr>
  </tbody>
</table>
<p>Pra contextualizar essas velocidades, lembra que no cloud o Sonnet roda a 532 tok/s, o Opus a 347 tok/s, o Step 3.5 Flash a 242 tok/s, o Gemini 3.1 Pro a 128 tok/s e o Kimi K2.5 a 160 tok/s. O Qwen 3.5 35B-A3B na 5090, a 273 tok/s, está na faixa do Step 3.5 Flash, mais rápido que Gemini, Kimi e GLM 5.1. O Qwen 3 Coder 30B a 145 tok/s está na faixa do Gemini. A frase clássica &ldquo;modelo local é dez vezes mais lento que cloud&rdquo; parou de valer no momento que a 5090 entrou na conta.</p>
<p>A consequência prática é que o argumento &ldquo;tempo é dinheiro&rdquo; muda de figura. No Strix, &ldquo;esperar 1 hora pra um Qwen 3.5 122B fazer o que o Sonnet faz em 16 minutos&rdquo; é pura perda. Na 5090, esperar 5 minutos pra o Qwen 3.5 35B-A3B fazer o trabalho, mais 10-15 minutos pra você fazer 1-2 prompts de correção, dá um total na faixa de 20-25 minutos. O Sonnet faz em 16 minutos com zero correção. A diferença ficou pequena o suficiente pra que, se custo importa muito, valha a pena.</p>
<p>A pegadinha: pra que isso valha a pena, o modelo precisa estar perto o suficiente da resposta certa pra que 1-2 prompts de correção resolvam. Quando o erro é do tipo &ldquo;o modelo decidiu não usar o gem que pedi e devolveu uma string mockada&rdquo;, como fez o Qwen 3 Coder 30B, nenhum prompt de correção fácil arruma. Isso é refazer.</p>
<h3>Antes que você gaste em hardware achando que é a saída<span class="hx:absolute hx:-mt-20" id="antes-que-você-gaste-em-hardware-achando-que-é-a-saída"></span>
    <a href="#antes-que-voc%c3%aa-gaste-em-hardware-achando-que-%c3%a9-a-sa%c3%adda" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Tenho que dar um aviso, porque é o erro de compra mais comum que vejo agora. Toda hora aparece alguém dizendo que vai pegar um Ryzen AI Max porque tem 128 GB de memória unificada e isso &ldquo;permite rodar modelos enormes&rdquo;. Tecnicamente, sim — o modelo cabe. Na prática, é quase inutilizável. A memória é LPDDR5x a 256 GB/s, sete vezes mais lenta que a GDDR7 da 5090. O que cabe não roda em velocidade humana. Meu próprio Strix com Qwen 3.5 122B chegou a 22 tok/s e o run levou 43 minutos. Pra fazer qualquer coisa de verdade no dia a dia, isso é impraticável.</p>
<p>A 5090 é claramente superior, e começa a fazer sentido até pra modelos menores justamente por causa da banda de memória. Mac Studio com memória unificada de alta velocidade (até 800 GB/s nos M4 Ultra) é a outra opção viável, e custa proporcionalmente o mesmo. Mas nenhuma das duas chega perto de bater os modelos comerciais em qualidade — e o preço de Claude, GPT ou GLM por token, somado à velocidade brutal de inferência deles, faz a conta ser difícil de justificar pra quem não é entusiasta ou pesquisador. Hardware caro de IA local é hobby de fim de semana, ferramenta pra quem precisa rodar offline por compliance, ou playground de pesquisa. Pra trabalho de produção dia após dia, no momento atual, cloud ainda é a escolha racional. Ryzen AI Max com 128 GB pode parecer tentador na planilha, mas se a ideia é coding agent sério, é dinheiro mal gasto.</p>
<h2>A família Qwen: Coder vs Geral, distilação, e por que nada é bala de prata<span class="hx:absolute hx:-mt-20" id="a-família-qwen-coder-vs-geral-distilação-e-por-que-nada-é-bala-de-prata"></span>
    <a href="#a-fam%c3%adlia-qwen-coder-vs-geral-distila%c3%a7%c3%a3o-e-por-que-nada-%c3%a9-bala-de-prata" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Com tantos Qwens diferentes rodando nessa rerodada, dá pra fazer uma análise mais focada. O que aprendi é capaz de surpreender quem segue benchmark de modelo no Twitter.</p>
<h3>Antes de ir aos resultados: o que é quantização e o que é distilação<span class="hx:absolute hx:-mt-20" id="antes-de-ir-aos-resultados-o-que-é-quantização-e-o-que-é-distilação"></span>
    <a href="#antes-de-ir-aos-resultados-o-que-%c3%a9-quantiza%c3%a7%c3%a3o-e-o-que-%c3%a9-distila%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os dois conceitos aparecem o tempo todo nessa discussão e merecem uma explicação curta.</p>
<p><strong>Quantização</strong> é a técnica de comprimir os pesos do modelo pra ocupar menos memória. Um modelo treinado em FP16 (16 bits por peso) pode ser quantizado pra Q8 (8 bits), Q4 (4 bits), Q3_K_M (3 bits, mas com agrupamentos médios), e por aí vai. Cada passo divide o tamanho do modelo em disco e na VRAM, ao custo de uma certa perda de precisão. Q8 é praticamente lossless. Q4 já perde alguma coisa mensurável. Q3 perde mais. Q2 é o limite onde o modelo começa a falar besteira de verdade. A regra de ouro é que pra coding e raciocínio multi-step, vale ficar em Q4 ou acima. Q3_K_M é o mínimo que ainda funciona pra muitos modelos, e é o que cabe num 27B na 5090 com 128k de contexto.</p>
<p>A surpresa do meu teste, e olha que isso vai contra o consenso, é que a quantização não foi o gargalo aqui. Rodei o Qwen 3.5 27B Claude-distilado em duas versões: Q8 no AMD Strix (~27 GB de pesos) e Q3_K_M na 5090 (~12 GB de pesos). Ambos alucinaram exatamente as mesmas APIs falsas do RubyLLM. O Q3_K_M até gerou um Gemfile mais limpo. A limitação do modelo estava no que esses pesos sabem, não na precisão com que eles foram comprimidos.</p>
<p><strong>Distilação</strong> é a técnica de treinar um modelo menor (o &ldquo;aluno&rdquo;) pra imitar a saída ou o comportamento de um modelo maior (o &ldquo;professor&rdquo;). A versão clássica é distilação de logits — o aluno aprende a aproximar as distribuições de probabilidade do professor. A versão moderna, mais popular pra coding agents, é distilação de <strong>traços de raciocínio</strong> (reasoning traces): você pega cadeias de pensamento do modelo grande pra problemas reais e treina o menor a reproduzir o mesmo estilo de raciocínio.</p>
<p>O hype do momento é distilar Claude e GPT em modelos open source. A promessa é que dá pra ter &ldquo;Claude-em-casa&rdquo; rodando local. Eu queria testar isso, e foi por isso que coloquei no benchmark o <strong><a href="https://huggingface.co/Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled"target="_blank" rel="noopener">Jackrong&rsquo;s Qwen 3.5 27B distilado do Claude 4.6 Opus</a></strong>. Se algum modelo open source ia conseguir usar o RubyLLM corretamente, era essa aposta — afinal, no benchmark inteiro, Claude e GLM 5 são os únicos que acertam a API.</p>
<h3>O que o Claude-distilled aprendeu (e o que não aprendeu)<span class="hx:absolute hx:-mt-20" id="o-que-o-claude-distilled-aprendeu-e-o-que-não-aprendeu"></span>
    <a href="#o-que-o-claude-distilled-aprendeu-e-o-que-n%c3%a3o-aprendeu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Rodei a mesma distilação duas vezes: uma em Q8 no AMD Strix (que estourou no timeout de 90 minutos), outra em Q3_K_M na 5090 (completou em 12 minutos). Em ambas, o resultado é a mesma frustração elegante.</p>
<p>O código que sai parece Claude. Tem <code># frozen_string_literal: true</code> no topo de todo arquivo. Tem uma classe <code>Response</code> separada como value object com leitores de atributo explícitos. Tem separação clara entre service, controller e model. Tem comentário de doc no topo de cada arquivo. Comenta corretamente coisas como <code>active_record</code>, <code>active_job</code> e <code>action_mailer</code> no <code>application.rb</code>. Tem <code>case</code> defensivo tentando múltiplos formatos de retorno. Estilisticamente, é Claude.</p>
<p>Funcionalmente, é alucinação completa do RubyLLM. Olha o serviço gerado pelo run da 5090:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">RubyLLM</span><span class="o">::</span><span class="no">Chat</span><span class="o">.</span><span class="n">new</span><span class="o">.</span><span class="n">with_model</span><span class="p">(</span><span class="vi">@model</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">chat</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">conversation_history</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">msg</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">.</span><span class="n">add_message</span><span class="p">(</span><span class="ss">role</span><span class="p">:</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="n">msg</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">Response</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">,</span> <span class="ss">usage</span><span class="p">:</span> <span class="n">build_usage</span><span class="p">(</span><span class="n">response</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada primitiva nesse código é inventada:</p>
<ul>
<li><code>RubyLLM::Chat.new</code> — o construtor não é público, o entry correto é <code>RubyLLM.chat(model:)</code></li>
<li><code>.with_model(@model) do |chat| ... end</code> — não existe API de bloco assim</li>
<li><code>chat.add_message(role:, content:)</code> — não existe</li>
<li><code>response.text</code> — a API real expõe <code>response.content</code></li>
<li><code>response.usage.prompt_tokens</code> — o objeto não tem essa forma</li>
</ul>
<p>Isso vai estourar <code>NoMethodError</code> na primeira request. O initializer também tenta <code>config.openrouter_api_base=</code> que não existe no <code>RubyLLM.configure</code>, então o app provavelmente nem boota.</p>
<p>A versão Q8 no AMD Strix faz exatamente o mesmo, com uma diferença: a chamada de entrada é <code>RubyLLM.chat(model:, provider: :openrouter)</code> — o entry point está certo, mas o <code>provider:</code> é inventado e logo em seguida vem o mesmo <code>chat.add_message(role:, content:)</code> falso. Pior, o Gemfile do run de 90 minutos lista <code>gem &quot;ruby-openai&quot;</code> (gem errada!), <code>gem &quot;minitest&quot;, &quot;~&gt; 6.0&quot;</code> (não existe minitest 6.0) e <code>gem &quot;tailwindcss&quot;</code> (nome errado, é <code>tailwindcss-rails</code>). O Gemfile não inclui o gem que o próprio service tenta usar.</p>
<p>Pra comparar, olha o Claude Opus 4.6 baseline real, no mesmo benchmark, acertando tudo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="n">model_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="vi">@chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Doze linhas no service inteiro. Sem alucinação. Inclui streaming via bloco. O modelo distilado produziu o triplo do volume de código e errou a API.</p>
<p>A leitura honesta é que distilação transferiu uma camada e parou. A camada que veio junto foi a do estilo: organização do código, comentários, estrutura de classes, ordem das coisas. A camada que ficou pra trás foi a da memória factual sobre bibliotecas específicas. Isso faz sentido quando você pensa: traços de raciocínio do Claude, mesmo escritos com calma, raramente contêm referências repetidas a <code>chat.ask(msg).content</code> num gem Ruby obscuro. O aluno só aprende o que o professor repete, e Claude nunca teve motivo pra ficar sussurrando &ldquo;use ask, não use complete&rdquo; ao longo das suas cadeias de pensamento. Conhecimento de API de biblioteca é memória binária de recall, do tipo que ou está nos pesos ou não está. Decompor isso em passos de raciocínio é impossível porque não é raciocínio, é só lembrança crua.</p>
<p>Pra fechar com a recomendação prática: se você precisa que o modelo realmente use o RubyLLM, ou qualquer biblioteca menos popular que seja, distilação de Claude não te salva. Use Claude de verdade ou GLM 5. Os &ldquo;Claude-stand-ins&rdquo; open source vão falhar do mesmo jeito que o Qwen base falharia, só que com letrinha mais bonita.</p>
<h3>Coder vs Geral: a surpresa dos modelos &ldquo;pra coding&rdquo;<span class="hx:absolute hx:-mt-20" id="coder-vs-geral-a-surpresa-dos-modelos-pra-coding"></span>
    <a href="#coder-vs-geral-a-surpresa-dos-modelos-pra-coding" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A intuição de quase todo mundo é que modelos com &ldquo;Coder&rdquo; no nome são os melhores pra programação. Faz sentido, foram fine-tunados especificamente em código. Mas no benchmark, foi exatamente o oposto.</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Tipo</th>
          <th>Hardware</th>
          <th style="text-align: right">Tempo</th>
          <th>Resultado</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>Geral (MoE)</td>
          <td>5090</td>
          <td style="text-align: right">5 min</td>
          <td>Roda Rails, alucina <code>add_message</code>/<code>complete</code> (1-2 follow-ups arrumam)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Devolveu uma string mockada hardcoded em vez de chamar o RubyLLM</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">timeout 90m</td>
          <td>Zero arquivos, modelo travou</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td>Geral</td>
          <td>5090</td>
          <td style="text-align: right">4 min</td>
          <td>Scaffold parcial, errors</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilado</td>
          <td>Geral + distilado</td>
          <td>5090</td>
          <td style="text-align: right">12 min</td>
          <td>Roda Rails, alucina API toda</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Sushi Coder RL</td>
          <td>Coder (RL)</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Falha de infra, não pôde testar</td>
      </tr>
  </tbody>
</table>
<p>Dos três Coders dedicados, dois falharam catastroficamente (timeout total e mock string hardcoded) e um nem rodou direito por bug de infra. Já o Qwen 3.5 35B-A3B, que é o modelo geral da linha (não o Coder), foi o que mais perto chegou de algo aproveitável: 5 minutos de execução, projeto Rails reconhecível, e o problema dele se conserta em 1-2 prompts.</p>
<p>O Qwen 3 Coder 30B é particularmente decepcionante. Ele passou longe de qualquer tentativa séria de usar a API: o controller que ele gerou tem isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">V1</span><span class="o">::</span><span class="no">MessagesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span>
</span></span><span class="line"><span class="cl">    <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">response</span><span class="p">:</span> <span class="s2">&#34;This is a mock response. In a real implementation, this would connect to RubyLLM with Claude Sonnet via OpenRouter.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Gemfile lista <code>gem &quot;ruby_llm&quot;</code> mas nada importa. O service layer é inexistente. O modelo decidiu que era mais fácil devolver uma string fake e seguir a vida. Isso é Tier 3 garbage de uma forma que nem prompt de correção arruma — tem que mandar reescrever do zero.</p>
<p>O Qwen 2.5 Coder 32B é ainda pior: 90 minutos rodando, zero arquivos. O <code>opencode-output.ndjson</code> de 1.8 MB mostra o modelo girando sem conseguir escrever nada. Provavelmente entrou em loop de planejamento sem nunca chamar as ferramentas de write. Total perda de slot.</p>
<p>Por que os &ldquo;Coder&rdquo; Qwens foram tão mal? Minha leitura é que o fine-tuning específico pra coding deles foi treinado em problemas mais isolados (Codeforces, Leetcode, snippets curtos), longe dos fluxos agentic com tool calling de longa duração. O modelo geral Qwen 3.5 35B-A3B tem treinamento mais amplo e se vira melhor com a parte de orquestração. A intuição popular &ldquo;Coder = melhor pra coding agent&rdquo; tá errada pra esse tipo de tarefa. O caso de uso onde os Coders se saem bem é &ldquo;completar uma função isolada&rdquo;, que é exatamente pro que eles foram treinados, e isso é uma fração pequena do que um coding agent faz no dia a dia.</p>
<h3>A pergunta que eu queria responder<span class="hx:absolute hx:-mt-20" id="a-pergunta-que-eu-queria-responder"></span>
    <a href="#a-pergunta-que-eu-queria-responder" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Era essa: rodando local na 5090, qual modelo Qwen vale o 1-2 prompts de correção pra entregar código que funcione?</p>
<p>A resposta honesta é: só o Qwen 3.5 35B-A3B, e talvez o Claude-distilado se você não se importa de gastar 12 minutos a mais.</p>
<ul>
<li>Qwen 3.5 35B-A3B na 5090: 5 minutos, entry point certo (<code>RubyLLM.chat(model:)</code>), erros nas chamadas seguintes. Total realista até funcionar: na faixa de 15-20 minutos com 1-2 follow-ups. Bate cloud OSS na conta de custo.</li>
<li>Qwen 3.5 27B Claude-distilado na 5090: 12 minutos, alucinação mais profunda (entry point inventado também). Total realista: 25-30 minutos com 2-3 follow-ups. Ainda compete em custo, e perde em tempo absoluto pro Claude real.</li>
<li>Os outros (Coder 30B, Coder 2.5 32B, 3 32B): não recompensam o tempo de correção. Cada um tem um problema estrutural que pede reescrita inteira do zero.</li>
</ul>
<p>Pra quem tem hardware nessa categoria e quer fugir do vendor lock-in da Anthropic, agora dá. Não dava na 5090 do ano passado, no Strix Halo então, esquece. Em 2026, na NVIDIA Blackwell, com o modelo certo, dá. Pra quem tem hardware de banda baixa (LPDDR5x, DDR4, DDR5), continua sendo perda de tempo: o relógio sozinho derruba qualquer plano de tornar isso prático.</p>
<h3>Qwen 3.6: o que mudou em relação ao 3.5<span class="hx:absolute hx:-mt-20" id="qwen-36-o-que-mudou-em-relação-ao-35"></span>
    <a href="#qwen-36-o-que-mudou-em-rela%c3%a7%c3%a3o-ao-35" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Testamos dois sabores do Qwen 3.6: o <strong>3.6 Plus</strong> (cloud, OpenRouter, grátis) e o <strong>3.6 35B</strong> (local, NVIDIA 5090, Q3_K_M).</p>
<p>O Qwen 3.6 Plus (cloud) completou em 17 minutos com 88.940 tokens e 9/9 no checklist de artefatos. Completou rápido e de graça. Mas o serviço gerado usa <code>chat.add_message()</code>, método que não existe no RubyLLM. A primeira mensagem funciona, a segunda quebra. O mesmo problema do 3.5.</p>
<p>O Qwen 3.6 35B (local, 5090) é mais interessante. Completou em 4.7 minutos a 240 tok/s, 169 arquivos, entry point <code>RubyLLM.chat(model:, provider:)</code> correto e <code>chat.ask(message)</code> correto. O bug é mais sutil: retorna <code>response</code> em vez de <code>response.content</code> e não faz replay de histórico. Correção de 1 linha. Isso é uma melhoria real sobre o Qwen 3.5 35B-A3B (que alucinava <code>add_message</code> e <code>complete</code>). É o resultado Qwen mais limpo que vimos até agora.</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Versão</th>
          <th>Hardware</th>
          <th style="text-align: center">API correta?</th>
          <th style="text-align: center">Multi-turn funciona?</th>
          <th style="text-align: right">Tempo</th>
          <th style="text-align: right">Tok/s</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point sim, <code>add_message</code>/<code>complete</code> não</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">273</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilado</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Não (entry point inventado)</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: right">12m</td>
          <td style="text-align: right">129</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td>3.6</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point sim, <code>chat.ask</code> sim, falta <code>.content</code></td>
          <td style="text-align: center">Não (sem replay)</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">240</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>3.6</td>
          <td>Cloud</td>
          <td style="text-align: center">Entry point sim, <code>add_message</code> não</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">183</td>
      </tr>
  </tbody>
</table>
<p>O gap diminuiu mas não fechou. O 3.6 35B local está mais perto de funcionar que qualquer Qwen anterior — o bug é um <code>.content</code> esquecido, não uma API inteira inventada. Mas continua sem multi-turn funcional de primeira. Na prática, o Qwen 3.6 35B local sobe do Tier 3 pro Tier 2: é o modelo open source local que mais perto chega de entregar código correto no primeiro try, a uma correção de distância.</p>
<h2>O Deep Code Review: Sonnet vs GLM 5 vs Gemini vs Kimi vs MiniMax<span class="hx:absolute hx:-mt-20" id="o-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax"></span>
    <a href="#o-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As tabelas acima medem completude estrutural. Mas o projeto funciona? Fiz code review detalhado dos modelos que completaram o benchmark.</p>
<p><strong>Claude Sonnet 4.6 — funciona e é o mais completo.</strong> Respostas síncronas via Turbo Stream. Histórico de chat persistido em session cookie com replay completo das mensagens anteriores a cada request. Mocking correto do LLM nos testes com mocha (30 testes em 328 linhas). Lógica de LLM extraída pra um <code>LlmChatService</code> separado. Views decompostas em 9 partials. Problemas menores: constante de modelo duplicada, leak no event listener do auto-resize. Nenhum é blocker. Dos projetos gerados, é o que mais se aproxima de algo que você colocaria em produção.</p>
<p><strong>GLM 5 — funciona, mas é o mínimo viável.</strong> Usa a API correta (<code>RubyLLM.chat(model:)</code> depois <code>.ask()</code>), faz mocking com mocha nos testes. Só que o projeto é bem mais enxuto que o do Sonnet: controller de 21 linhas (vs 52 do Sonnet), sem service layer (lógica LLM inline no controller), sem persistência de histórico de chat, cada mensagem tratada isoladamente. A primeira mensagem funciona, mas o app não mantém contexto de conversação, então não dá pra ter um diálogo multi-turn. Os testes existem (7 métodos) mas são esqueléticos: o <code>ruby_llm_test.rb</code> só checa se o módulo está carregado, o <code>chat_flow_test.rb</code> é cópia do controller test. O Dockerfile, por outro lado, é o melhor dos quatro: multi-stage, non-root, jemalloc. Mas como app de chat? É mais uma prova de conceito do que algo funcional. Detalhe engraçado: o README diz &ldquo;Powered by Claude Sonnet 4&rdquo; em vez do modelo que realmente gerou o projeto.</p>
<p><strong>Gemini 3.1 Pro — o mais rápido, mas tropeça na API.</strong> Completou em 14 minutos, o mais rápido junto com o MiniMax. O código Rails em si é bem feito: usa <code>Rails.cache</code> com session ID e expiração de 2 horas pra manter estado (em vez de banco de dados), Turbo Streams bem integrados, Stimulus controller pra scroll automático, e o Dockerfile é o melhor do grupo (multi-stage, non-root, jemalloc). O problema é o de sempre: usa <code>RubyLLM::Chat.new()</code> em vez de <code>RubyLLM.chat()</code>, e chama <code>add_message()</code> que não existe. O app boota, o Docker roda, o health check passa, mas a primeira mensagem de chat dá 500. Os testes (5 métodos) mockam com um <code>FakeChat</code> que replica a assinatura errada, então passam. É frustrante porque o resto do código é o mais &ldquo;Rails way&rdquo; dos modelos que não são Anthropic. Corrigir seriam 3 linhas, mas o benchmark mede o que sai de primeira.</p>
<p><strong>Kimi K2.5 — ambicioso mas quebrado.</strong> Tentou a arquitetura mais sofisticada: ActionCable streaming, modelos configuráveis, dual Dockerfiles, 37 testes em 374 linhas. Problema: o streaming depende do ActionCable, que está comentado no <code>config/application.rb</code>. O guard <code>return unless defined?(ActionCable)</code> faz o método não fazer nada. O assistente nunca responde. O Stimulus controller tem bug de escopo: o <code>submitTarget</code> referencia um botão fora da subtree do controller. Storage thread-unsafe com hash em variável de classe. O Kimi escreveu mais testes que qualquer outro modelo (37), mas nenhum mocka as chamadas LLM — então os testes passam sem provar que a funcionalidade funciona de verdade.</p>
<p><strong>Grok 4.20 — rápido e errado.</strong> Foi o mais rápido do benchmark inteiro: 8 minutos, 412 tok/s. Só que foi rápido porque cortou caminho. O prompt pedia explicitamente o gem <code>ruby_llm</code>, e o Grok ignorou. Foi direto no <code>OpenAI::Client</code> da gem <code>ruby-openai</code> apontando pra URL do OpenRouter. Tecnicamente a primeira mensagem volta, então sim, &ldquo;funciona&rdquo;. Mas é o mesmo truque do Step 3.5 Flash e do Qwen 3.5 122B: pula a peça que tava sendo testada. Sem histórico, controller de 33 linhas chamando o cliente HTTP no braço, dois testes, nenhum mock de verdade. Foi rápido porque fez menos do que pediram.</p>
<p><strong>MiniMax M2.7 — parece certo, crasha.</strong> Chama <code>RubyLLM.chat(model: '...', messages: [...])</code> — essa assinatura não existe. Sem persistência de mensagens. HTML duplicado (DOCTYPE dentro do layout). master.key commitada. E os testes? Mockam a API errada, então passam mas não provam nada.</p>
<p>Resumo do code review:</p>
<table>
  <thead>
      <tr>
          <th>Aspecto</th>
          <th style="text-align: center">Sonnet 4.6</th>
          <th style="text-align: center">GLM 5</th>
          <th style="text-align: center">Gemini 3.1 Pro</th>
          <th style="text-align: center">Kimi K2.5</th>
          <th style="text-align: center">MiniMax M2.7</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API correta</td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center">Não</td>
      </tr>
      <tr>
          <td>Histórico de chat</td>
          <td style="text-align: center">Session cookie</td>
          <td style="text-align: center">Nenhum</td>
          <td style="text-align: center">Rails.cache (2h)</td>
          <td style="text-align: center">Broken (ActionCable off)</td>
          <td style="text-align: center">Nenhum</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td style="text-align: center">LlmChatService</td>
          <td style="text-align: center">Inline no controller</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">ChatService (API errada)</td>
      </tr>
      <tr>
          <td>Testes (métodos)</td>
          <td style="text-align: center">30</td>
          <td style="text-align: center">7</td>
          <td style="text-align: center">5</td>
          <td style="text-align: center">37</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td>Mocking LLM</td>
          <td style="text-align: center">Sim (mocha)</td>
          <td style="text-align: center">Sim (mocha)</td>
          <td style="text-align: center">FakeChat (API errada)</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center">Mock da API errada</td>
      </tr>
      <tr>
          <td>Dockerfile</td>
          <td style="text-align: center">Multi-stage</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Dual (dev/prod)</td>
          <td style="text-align: center">Single-stage</td>
      </tr>
      <tr>
          <td>Roda de verdade?</td>
          <td style="text-align: center">Sim</td>
          <td style="text-align: center">Sim (sem histórico)</td>
          <td style="text-align: center">Não (500 no chat)</td>
          <td style="text-align: center">Não</td>
          <td style="text-align: center">Não</td>
      </tr>
  </tbody>
</table>
<h3>GLM 5 vs GLM 5.1: o que mudou<span class="hx:absolute hx:-mt-20" id="glm-5-vs-glm-51-o-que-mudou"></span>
    <a href="#glm-5-vs-glm-51-o-que-mudou" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O GLM 5 foi um dos poucos modelos que cuspiu código funcional de primeira, então era óbvio testar a versão nova. Um detalhe que importa antes dos números: o GLM 5 rodou via OpenRouter, o GLM 5.1 ainda não tava lá quando rodei o teste, então usei a API direta da Z.AI. Provedor diferente, infra diferente, cache diferente. Os números abaixo são referência, não medida exata.</p>
<table>
  <thead>
      <tr>
          <th>Aspecto</th>
          <th>GLM 5</th>
          <th>GLM 5.1</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Provedor</td>
          <td>OpenRouter</td>
          <td>Z.AI direto</td>
      </tr>
      <tr>
          <td>Tempo total</td>
          <td>17m</td>
          <td>22m</td>
      </tr>
      <tr>
          <td>Tok/s (final phase)</td>
          <td>400</td>
          <td>167</td>
      </tr>
      <tr>
          <td>Tokens efetivos novos</td>
          <td>1.138</td>
          <td>450</td>
      </tr>
      <tr>
          <td>Cache lido</td>
          <td>58.240</td>
          <td>81.216</td>
      </tr>
      <tr>
          <td>API RubyLLM correta</td>
          <td>Sim</td>
          <td>Sim</td>
      </tr>
      <tr>
          <td>Mocking nos testes</td>
          <td>Sim (mocha)</td>
          <td>Sim</td>
      </tr>
      <tr>
          <td>Testes</td>
          <td>7</td>
          <td>24</td>
      </tr>
      <tr>
          <td>Histórico de chat</td>
          <td>Não</td>
          <td>Sim (in-memory)</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td>Inline no controller</td>
          <td><code>ChatSession</code> model com <code>add_user_message</code>/<code>add_assistant_message</code></td>
      </tr>
  </tbody>
</table>
<p>O projeto do GLM 5.1 ficou bem mais completo. 24 testes contra 7. Separação real entre <code>ChatSession</code>, <code>ChatMessage</code> e o controller, em vez do GLM 5 botando tudo inline. Histórico de chat persistido em memória durante a sessão, então dá pra ter uma conversa multi-turn de verdade (o GLM 5 tratava cada mensagem como se fosse a primeira). E a API do RubyLLM continua correta, mesmo padrão <code>RubyLLM.chat(model:, provider:)</code> seguido de <code>c.user</code>/<code>c.assistant</code> pra montar o contexto. Tem até teste cobrindo a constante <code>MODEL</code>, coisa que normalmente ninguém faz.</p>
<p>O preço foi velocidade. 22 minutos contra 17, e a vazão caiu de 400 pra 167 tok/s. Pode ser provedor (Z.AI direto não é a mesma infra do OpenRouter), pode ser um run mais carregado no servidor, pode ser que o 5.1 raciocine mais. Não rodei várias vezes pra fazer média, então não vou dizer que o 5.1 é &ldquo;mais lento&rdquo;. Uma run só não prova regressão. O que dá pra dizer é que, no meu teste, o 5.1 entregou um projeto melhor estruturado e demorou um pouco mais pra fazer isso.</p>
<p>Pra quem quer fugir da Anthropic sem perder qualidade, GLM 5 e GLM 5.1 são as duas opções que funcionam. Se você precisa de billing centralizado no OpenRouter, GLM 5. Se consegue usar a Z.AI direto e quer um projeto mais redondo de primeira, GLM 5.1.</p>
<h2>Custos: API vs Assinatura<span class="hx:absolute hx:-mt-20" id="custos-api-vs-assinatura"></span>
    <a href="#custos-api-vs-assinatura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Primeiro, o preço por token de cada modelo no OpenRouter:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-pricing.png" alt="Preço por token no OpenRouter"  loading="lazy" /></p>
<p>O GPT 5.4 Pro cobra $180 por milhão de tokens de output. O Claude Opus cobra $25. O GLM 5 cobra $2.30. E o Qwen 3.6 Plus é grátis (com rate limit). A escala logarítmica no gráfico esconde um pouco a brutalidade da diferença: de Qwen grátis pra GPT 5.4 Pro são ordens de magnitude.</p>
<p>Mas preço por token não é a história completa. Se você usa Claude ou GPT diariamente pra coding, a assinatura mensal pode sair muito mais barata que pagar por token via API:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/monthly-pricing.png" alt="Assinatura vs API: quanto custa usar Claude e GPT por mês"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Abordagem</th>
          <th style="text-align: right">Est. $/mês*</th>
          <th>Notas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.6 Plus (OpenRouter)</td>
          <td style="text-align: right">$0</td>
          <td>Grátis mas rate-limited</td>
      </tr>
      <tr>
          <td>Modelos locais</td>
          <td style="text-align: right">Eletricidade</td>
          <td>Precisa de hardware</td>
      </tr>
      <tr>
          <td>Claude Pro</td>
          <td style="text-align: right">$20</td>
          <td>~44K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Plus</td>
          <td style="text-align: right">$20</td>
          <td>Inclui Codex</td>
      </tr>
      <tr>
          <td>Claude Max 5x</td>
          <td style="text-align: right">$100</td>
          <td>~88K tokens/5hr</td>
      </tr>
      <tr>
          <td>Claude Sonnet (OpenRouter API)</td>
          <td style="text-align: right">~$150</td>
          <td>Sem cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>Claude Max 20x</td>
          <td style="text-align: right">$200</td>
          <td>~220K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Pro</td>
          <td style="text-align: right">$200</td>
          <td>GPT 5.4 Pro ilimitado</td>
      </tr>
      <tr>
          <td>Claude Opus (OpenRouter API)</td>
          <td style="text-align: right">~$450</td>
          <td>Sem cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>GPT 5.4 Pro (OpenRouter API)</td>
          <td style="text-align: right">~$990</td>
          <td>Absurdamente caro</td>
      </tr>
  </tbody>
</table>
<p>*Estimativa pra uso moderado de coding (~15M input + ~3M output tokens/mês).</p>
<p>O ponto principal: se você usa GPT 5.4 Pro, a assinatura ChatGPT Pro a $200/mês com uso ilimitado é 5x mais barata que pagar por token na API. Pra Claude, o Pro a $20/mês cobre uso leve, mas pra quem usa pesado (maratona de coding como a minha), o Max 20x a $200/mês sai mais barato que pagar Opus por token no OpenRouter (~$450/mês). Os modelos open source no OpenRouter ficam todos abaixo de $2.50/M output tokens, mas como vimos, a maioria gera código que não roda.</p>
<h2>O que funciona pra uso real<span class="hx:absolute hx:-mt-20" id="o-que-funciona-pra-uso-real"></span>
    <a href="#o-que-funciona-pra-uso-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois de testar 33 modelos ao longo das duas rodadas e olhar o código gerado em detalhe:</p>
<p>Tier 1 (funciona plug and play):</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Qualidade</th>
          <th style="text-align: right">Custo/Run</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>Novo baseline (28 testes, FakeChat, 96.7% cobertura)</td>
          <td style="text-align: right">~$1.10</td>
          <td>Incremental sobre 4.6, novo gold standard</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>Melhor que Opus 4.6 no opencode (30 vs 16 testes)</td>
          <td style="text-align: right">~$0.63</td>
          <td>Mais barato, mas no Claude Code o Opus pode se sair melhor</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>Gold standard anterior</td>
          <td style="text-align: right">~$1.05</td>
          <td>Baseline anterior</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>Bom (7 testes, API correta)</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% mais barato, alternativa non-Anthropic/OpenAI que funciona</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Bom (24 testes, histórico, API correta)</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% mais barato, projeto mais completo que o GLM 5</td>
      </tr>
  </tbody>
</table>
<p>Tier 2 (funciona com ressalvas):</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: right">Custo/Run</th>
          <th>Ressalva</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$16.00</td>
          <td>Arquitetura impressionante (22 testes, injeção de dependência, PORO models), mas <code>add_message</code> com keyword args em vez de hash posicional quebra multi-turn. 7.6M tokens, 15x mais caro que Opus</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.02</td>
          <td>Bypassa o gem pedido, lento (38m)</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.04</td>
          <td>Bypassa o gem pedido (vai direto pro <code>OpenAI::Client</code>), mas é o mais rápido do benchmark</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Grátis</td>
          <td>Entry point e <code>chat.ask</code> corretos, falta <code>.content</code>. Correção de 1 linha. ~10-15 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Grátis</td>
          <td>Entry point certo, alucina <code>add_message</code>/<code>complete</code>. Dá pra arrumar em 1-2 follow-ups. ~15-20 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilado</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Grátis</td>
          <td>Estilo Claude, alucinação completa da API. 2-3 follow-ups pra arrumar. ~25-30 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B (local)</td>
          <td style="text-align: center">AMD Strix</td>
          <td style="text-align: right">Grátis</td>
          <td>Funciona se default configurado, sem mocking, e lento</td>
      </tr>
  </tbody>
</table>
<p>Tier 3 (código quebrado, mesmo com prompts de correção é mais fácil refazer):</p>
<p>Kimi K2.5, MiniMax M2.7, DeepSeek V3.2, Gemini 3.1 Pro, Qwen 3 Coder Next (Strix), Qwen 3 Coder 30B (5090, devolveu mock string hardcoded), Qwen 3.5 122B, Qwen 3.6 Plus — todos inventam APIs que não existem ou nem tentam usar o gem.</p>
<p>Tier 4 (não completaram):</p>
<p>Gemma 4 (loop infinito nos dois hardwares), Llama 4 Scout (sem parser), GPT OSS 20B (diretório errado no Strix, regressão de parser na 5090), Qwen 3 32B (lento demais no Strix, scaffold parcial na 5090), Qwen 2.5 Coder 32B (timeout 90m com zero arquivos).</p>
<h3>Ranking simplificado (qualidade, tempo, preço)<span class="hx:absolute hx:-mt-20" id="ranking-simplificado-qualidade-tempo-preço"></span>
    <a href="#ranking-simplificado-qualidade-tempo-pre%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra quem quer só o resumão em forma de boletim escolar. Qualidade é se o código roda e quão completo está. Tempo é o tempo total do run. Preço é o custo estimado por execução no opencode. <strong>Hardware</strong> indica onde o modelo rodou — Cloud, Strix (AMD Strix Halo, LPDDR5x 256 GB/s) ou 5090 (NVIDIA RTX 5090, GDDR7 1792 GB/s). Modelos cloud rodaram via OpenRouter ou API direta do provedor.</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: center">Tipo</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: center">Qualidade</th>
          <th style="text-align: center">Tempo</th>
          <th style="text-align: center">Preço</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">F</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A−</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilado</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">C+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">B</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (grátis)</td>
      </tr>
  </tbody>
</table>
<p>Critério de qualidade: A+ funciona e o código está bem estruturado. A/B funciona com ressalvas pequenas a médias. C roda mas pula requisito do prompt ou tem problema estrutural sério. D quebra na primeira mensagem por API inventada. F não completou o benchmark ou produziu lixo. O GPT 5.4 via Codex caiu de A+ pra B+: a arquitetura é a mais sofisticada do benchmark (injeção de dependência, PORO models, 22 testes), mas a primeira mensagem funciona e o multi-turn quebra por convenção de chamada errada no <code>add_message</code>. Gastou 7.6M tokens (~$16/run) sem atingir o nível de acerto do Opus. &ldquo;Tipo&rdquo; separa modelos commercial (pesos fechados) dos OSS (pesos abertos, mesmo quando você usa via API hospedada). Os Qwens aparecem duas vezes quando rodaram nos dois hardwares, porque os resultados são diferentes o suficiente pra justificar — o Qwen 3.5 35B-A3B no 5090 sobe pro Tier B, no Strix continua no Tier C por causa do tempo de espera. Dos 33 modelos configurados ao longo das duas rodadas, alguns ficam de fora dessa tabela porque nem chegaram a executar (sem quota, runner quebrado, falha de infra, ou timeout antes da primeira mensagem).</p>
<h3>O veredito<span class="hx:absolute hx:-mt-20" id="o-veredito"></span>
    <a href="#o-veredito" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se quer o melhor resultado e não quer pensar: <strong>Claude Opus</strong>. O Opus 4.7 é o novo baseline — 28 testes, API correta, 96.7% de cobertura, FakeChat pattern nos testes. É melhoria incremental sobre o 4.6 (que tinha 16 testes), não uma revolução. Mas não precisa ser revolução. O 4.6 já funcionava, o 4.7 funciona igual e entrega um projeto um pouco mais polido. Se você estava no 4.6, atualizar pro 4.7 é trocar pra um modelo que faz a mesma coisa com mais capricho nos testes e na estrutura.</p>
<p>Um aviso sobre o Sonnet: ele ganhou do Opus nesse benchmark (30 testes vs 16 do Opus 4.6, vs 28 do Opus 4.7). Mas esse benchmark é um app web pequeno e bem definido. Na minha experiência real com projetos maiores, o Sonnet falha quando o raciocínio precisa ir mais fundo. Não estou falando de projetos imensos — basta ir um pouco além desse benchmark (mais controllers, mais integrações, decisões arquiteturais que dependem umas das outras) e o Sonnet começa a perder o fio da meada. O Opus tem teto de output de 128K tokens contra 64K do Sonnet, e o treinamento dele foi especificamente pra tarefas de longo prazo, planejamento multi-etapas e raciocínio profundo sobre código complexo. Num projeto pequeno como o do benchmark, esses músculos ficam parados, e nesse cenário Sonnet ganha por ser mais rápido e mais barato. Mas se você extrapolar isso pra &ldquo;Sonnet é melhor que Opus&rdquo;, vai levar um susto na primeira tarefa que exija raciocínio sustentado. Pode experimentar o Sonnet, é mais barato e pros projetos pequenos funciona. Mas pra projetos reais, você provavelmente vai acabar no Opus de qualquer jeito.</p>
<p>Sobre o GPT 5.4: agora temos dados objetivos. Rodei via Codex CLI com reasoning effort xHigh. A arquitetura que ele gera é a mais sofisticada do benchmark — injeção de dependência, PORO models, session management com TTL. Mas gastou 7.6M tokens (~$16/run, 15x mais caro que Opus) e errou a convenção de chamada do <code>add_message</code> (keyword args em vez de hash posicional), quebrando o multi-turn. Gastou mais, errou igual. Caiu do Tier 1 pro Tier 2. O padrão se repete: acerto de API é memória binária nos pesos. Não escala com budget de tokens nem com reasoning effort.</p>
<p>Se custo importa e você quer sair da Anthropic: <strong>GLM 5 ou GLM 5.1</strong> são as alternativas plug-and-play que funcionam. API correta, mocking nos testes, ~$0.11-$0.13 por run, ~88-89% mais barato que Opus. O GLM 5.1 entregou um projeto mais completo (24 testes, histórico de chat) ao custo de uns 5 minutos a mais.</p>
<p>Se quer evitar vendor lock-in total e tem hardware decente: <strong>Qwen 3.5 35B-A3B</strong> rodando local numa NVIDIA RTX 5090. Cinco minutos de execução a 273 tok/s, projeto Rails que arranca, e o erro de API se conserta em 1-2 follow-ups. Total realista até funcionar: ~15-20 minutos. Bate o Sonnet em custo (zero) e fica perto em tempo total. Essa opção simplesmente não existia na rodada anterior do benchmark, e marca o ponto onde &ldquo;rodar OSS local&rdquo; deixa de ser brincadeira e vira alternativa real. Importante: isso é específico de hardware com banda de memória alta. Numa RTX 4090 deve funcionar parecido. Num laptop com LPDDR5x ou num desktop com DDR4, esquece — você vai esperar 10x mais e o tempo total mata o argumento.</p>
<p>Se quer evitar vendor lock-in mas está em hardware fraco: <strong>GLM 5 ou GLM 5.1</strong> continuam sendo a escolha. São cloud, é verdade, mas a $0.11-$0.13 por run é praticamente preço de eletricidade.</p>
<p>Se quer testar a aposta &ldquo;Claude em casa&rdquo; via distilação: o <strong>Qwen 3.5 27B Claude-distilado</strong> tá lá pra brincar, mas já avisei que ele alucina exatamente as mesmas APIs falsas do Qwen base. Distilação transferiu o estilo do Claude, não o conhecimento factual sobre bibliotecas. Vale como experimento, não como produção.</p>
<p>Sim, talvez com dias de tweaking no llama.cpp, calibrando flags, ajustando prompts, testando builds diferentes, dê pra fazer o Gemma 4 ou outros modelos funcionarem melhor. Pra maioria das pessoas, isso não é realista. A distância entre modelos frontier (Claude, GPT) e modelos open source self-hosted é real. Não é marketing. O gap está diminuindo, mas continua existindo, e a natureza dele mudou: hoje o que falta nos open source é conhecimento factual sobre bibliotecas específicas, não capacidade bruta de raciocínio. Hardware deixou de ser o gargalo, pelo menos pra quem tem GPU recente.</p>
<p>No fim, o que importa é se o código roda. Um modelo pode gerar 3.405 arquivos, escrever 37 testes, produzir um README de 181 linhas, e ainda assim o app não funcionar porque a API que ele usa não existe. Métricas de completude e contagem de testes são necessárias mas não suficientes. O único sinal confiável é se o modelo usa APIs reais corretamente.</p>
<p>O benchmark completo, com código, configuração, prompts e resultados por modelo, está no <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
]]></content:encoded><category>llm</category><category>benchmark</category><category>open-source</category><category>claude</category><category>ai</category><category>self-hosting</category></item><item><title>Transformando YouTube num App de Karaoke | Frank Karaoke</title><link>https://www.akitaonrails.com/2026/04/05/transformando-youtube-num-app-de-karaoke-frank-karaoke/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/05/transformando-youtube-num-app-de-karaoke-frank-karaoke/</guid><pubDate>Sun, 05 Apr 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;Projeto no GitHub: &lt;a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener"&gt;github.com/akitaonrails/frank_karaoke&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/scoring-in-action.jpg" alt="Pontuação em tempo real sobre um vídeo de karaokê do YouTube" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Eu sempre gostei de karaokê. De vez em quando saio pra cantar com família ou amigos. Em São Paulo tem bons lugares na Liberdade e no Bom Retiro, por exemplo, com cabines no estilo japonês. Se você nunca foi num karaokê desses: você aluga uma sala privada por hora, tem um catálogo enorme de músicas, dois microfones, e um sistema de pontuação que avalia seu canto em tempo real. Os melhores sistemas são japoneses, como o &lt;a href="https://www.joysound.com/"target="_blank" rel="noopener"&gt;Joysound&lt;/a&gt; e o &lt;a href="https://www.clubdam.com/"target="_blank" rel="noopener"&gt;DAM&lt;/a&gt;. Nota acima de 90 (de 100) é considerada avançada. O DAM, na série LIVE DAM Ai, até usa IA pra dar notas mais &amp;ldquo;humanas&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Projeto no GitHub: <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">github.com/akitaonrails/frank_karaoke</a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/scoring-in-action.jpg" alt="Pontuação em tempo real sobre um vídeo de karaokê do YouTube"  loading="lazy" /></p>
<p>Eu sempre gostei de karaokê. De vez em quando saio pra cantar com família ou amigos. Em São Paulo tem bons lugares na Liberdade e no Bom Retiro, por exemplo, com cabines no estilo japonês. Se você nunca foi num karaokê desses: você aluga uma sala privada por hora, tem um catálogo enorme de músicas, dois microfones, e um sistema de pontuação que avalia seu canto em tempo real. Os melhores sistemas são japoneses, como o <a href="https://www.joysound.com/"target="_blank" rel="noopener">Joysound</a> e o <a href="https://www.clubdam.com/"target="_blank" rel="noopener">DAM</a>. Nota acima de 90 (de 100) é considerada avançada. O DAM, na série LIVE DAM Ai, até usa IA pra dar notas mais &ldquo;humanas&rdquo;.</p>
<p>Mas nem todo lugar tem esse nível.</p>
<h2>O problema com karaokê no Brasil<span class="hx:absolute hx:-mt-20" id="o-problema-com-karaokê-no-brasil"></span>
    <a href="#o-problema-com-karaok%c3%aa-no-brasil" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Brasil a gente cresceu com o <a href="https://www.videoland.com.br/wwwroot/historia.asp"target="_blank" rel="noopener">Videokê</a>, a marca que o coreano Seok Ha Hwang trouxe pro país em 1996, importando equipamento da Coreia. Virou febre nos anos 90 e 2000, apareceu em todo bar, churrasco e festa de aniversário. O problema é que esses aparelhos pararam no tempo. Os modelos atuais, como o VSK 5.0, vêm com uns 12-13 mil músicas no catálogo, que você expande comprando cartuchos ou pacotes de música. Na prática, o repertório é velho, a interface é dos anos 2000, e se a música que você quer cantar saiu depois de 2015, boa sorte.</p>
<p>A solução que muitos bares adotaram foi permitir Chromecast ou espelhamento de tela pra que os clientes busquem músicas direto no YouTube. Faz sentido: no YouTube você encontra karaokê de qualquer música. Versão com letra, versão instrumental, versão com guia vocal.</p>
<p>Mas tem um downgrade: você perde a pontuação. Uma das coisas mais divertidas do karaokê é a competição. Ver sua nota subindo, comparar com os amigos, tentar bater o recorde da noite. Se você está só cantando em cima de um vídeo do YouTube, não tem feedback nenhum. É como jogar boliche sem placar.</p>
<p>E comprar um sistema profissional pra casa? Importar um Joysound F1 sai por mais de US$ 2.000 só o hardware, fora a assinatura mensal do catálogo. Pra uso casual não faz sentido.</p>
<h2>A ideia: YouTube com pontuação em tempo real<span class="hx:absolute hx:-mt-20" id="a-ideia-youtube-com-pontuação-em-tempo-real"></span>
    <a href="#a-ideia-youtube-com-pontua%c3%a7%c3%a3o-em-tempo-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">Frank Karaoke</a> surgiu dessa frustração. Se o YouTube já tem todas as músicas, por que não fazer um app que funciona como wrapper do YouTube com overlay de pontuação em tempo real? Você busca qualquer vídeo de karaokê, canta junto, e o app analisa sua voz pelo microfone e mostra uma nota ao vivo.</p>
<p>É um app Flutter pra Android. Internamente ele carrega o YouTube numa webview e injeta um overlay em HTML/CSS/JavaScript direto na página. O display de score, o trail de pitch, o painel de configurações, o seletor de modo, tudo renderizado dentro da webview via injeção de JS.</p>
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/karaoke-full-dark.png">
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/karaoke-full-light.png" alt="Frank Karaoke">
</picture>
<h2>Scoring sem referência<span class="hx:absolute hx:-mt-20" id="scoring-sem-referência"></span>
    <a href="#scoring-sem-refer%c3%aancia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora, o problema real. Todos os sistemas profissionais de karaokê dependem de arquivos de referência pré-fabricados pra cada música. Todos.</p>
<p>O <a href="https://en.wikipedia.org/wiki/SingStar"target="_blank" rel="noopener">SingStar</a> da Sony, que vendeu mais de 12 milhões de cópias entre 2004 e o fim do PS3, tinha um track de notas feito à mão pra cada música. Cada nota, cada sílaba, tudo mapeado manualmente. O mecanismo comparava o pitch do cantor via FFT contra essa referência em tempo real. Detalhe que eu achei esperto: a oitava era ignorada. Se a nota certa era um Dó, tanto faz se você cantou Dó3 ou Dó4. Homens cantam músicas femininas sem problema.</p>
<p>O Joysound e o DAM no Japão vão além e avaliam três dimensões separadas: precisão de pitch (音感), ritmo/timing (リズム感) e expressividade/dinâmica de volume (表現力). Tudo baseado em dados MIDI do servidor do operador. O formato open source equivalente é o UltraStar, onde cada música tem um arquivo <code>.txt</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>: 12 4 5 Hel-    (TipoNota BeatInicial Duração Pitch Sílaba)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>Pitch 5</code> = MIDI 65 (F4). A pontuação compara o pitch do cantor com o da nota, módulo oitava, com tolerância de 1 semitom.</p>
<p>O Frank Karaoke funciona com qualquer vídeo do YouTube. Não tem arquivo de referência. Não tem MIDI. Não tem anotação de melodia. Zero metadata sobre qual nota você deveria estar cantando.</p>
<p>Eu não sei nada de pontuação de karaokê. Não sei nada de processamento de áudio, detecção de pitch, teoria musical aplicada a software. Nada. Então pedi pro Claude Code fazer uma pesquisa extensiva sobre o assunto. O que ele trouxe de volta está documentado em <a href="https://github.com/akitaonrails/frank_karaoke/blob/main/docs/scoring.md"target="_blank" rel="noopener"><code>docs/scoring.md</code></a> no repositório, e é bastante coisa: papers acadêmicos sobre avaliação de canto (Nakano et al. 2006, Tsai &amp; Lee 2012, Molina et al. 2013), patentes (a Yamaha tem uma de 1999, US5889224A, que detalha pontuação por MIDI com 3 faixas de tolerância), e o código fonte de projetos open source como UltraStar Deluxe, AllKaraoke, Vocaluxe e Nightingale.</p>
<p>A conclusão da pesquisa: sem referência por música, você precisa avaliar qualidade vocal de forma genérica. Medir <em>como</em> a pessoa canta, não <em>o que</em> ela deveria estar cantando. E como não existe uma métrica única que funcione pra todos os casos, decidimos implementar quatro modos de pontuação diferentes, cada um medindo uma dimensão diferente da qualidade vocal.</p>
<h2>O problema do microfone do celular<span class="hx:absolute hx:-mt-20" id="o-problema-do-microfone-do-celular"></span>
    <a href="#o-problema-do-microfone-do-celular" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes dos modos de pontuação, preciso explicar um problema mais fundamental que a pesquisa revelou: o microfone do celular.</p>
<p>Quando você canta karaokê com o celular, o microfone capta três coisas ao mesmo tempo: sua voz, a música saindo do alto-falante, e ruído ambiente da sala. Sua voz está fisicamente mais perto do microfone, então domina o sinal. Mas não o suficiente pra uma separação limpa.</p>
<p>Tentei várias abordagens pra isolar a voz:</p>
<p>Subtração espectral usando o áudio de referência do YouTube. Abandonei. O CDN do YouTube bloqueia extração direta de áudio por user-agent não-browser, e mesmo com o áudio de referência, a equalização do alto-falante, a reverberação da sala e o delay de Bluetooth tornam o sinal diferente demais do que o microfone capta. Subtração simples gera artefatos piores que nenhuma subtração.</p>
<p>Pré-ênfase + center clipping. Abandonei também. O center clipping destrói a forma de onda que o algoritmo YIN precisa pra autocorrelação, e a pré-ênfase amplifica ruído tanto quanto voz.</p>
<p>O que funciona é um filtro bandpass de 200-3500 Hz: um filtro IIR de segunda ordem (Butterworth, Q=0.707) em cascata. O high-pass em 200 Hz elimina baixo, bumbo, bass guitar do bleed do alto-falante. O low-pass em 3500 Hz elimina pratos, hi-hats, ruído de alta frequência. Os fundamentais da voz humana (85-300 Hz) e os formantes (300-3000 Hz) passam pelo filtro. Não é isolamento perfeito, mas melhora bastante a razão voz/música pra detecção de pitch.</p>
<p>Mas o bandpass sozinho não resolve tudo. Guitarras, sintetizadores e piano produzem sinais periódicos na mesma faixa de frequência da voz, e o YIN detecta pitch neles também. Pra lidar com isso, o app faz uma calibração adaptativa: nos primeiros 5 segundos de warmup (quando ninguém está cantando), ele coleta amostras de RMS do sinal pra estabelecer um baseline do nível do alto-falante. Durante a música, mantém esse baseline atualizado (percentil 25 dos últimos ~4 segundos de frames). Pra um frame ser pontuado, o RMS precisa estar pelo menos 1.3x acima do baseline. A voz está mais perto do microfone, então empurra o RMS acima do nível do alto-falante. A melodia instrumental fica perto do baseline e é filtrada. Nos testes, o cantor original saindo pelo alto-falante marcava uns 37 pontos com dots esparsos no trail, enquanto alguém cantando de verdade marcava ~59 com dots densos.</p>
<p>Outro detalhe chato: no Android, especificamente nos Samsung, o <code>AutomaticGainControl</code> (AGC) do DSP atenua o sinal em vez de amplificar. Nos Galaxy, habilitar AGC reduz o pico do microfone de ~0.06 pra ~0.003. Silêncio pra detecção de pitch. Então o app desabilita AGC, echo cancellation e noise suppression. Quando o pico fica abaixo de 0.01, aplica ganho por software (até 30x) pra trazer o sinal a níveis usáveis.</p>
<h2>O algoritmo YIN<span class="hx:absolute hx:-mt-20" id="o-algoritmo-yin"></span>
    <a href="#o-algoritmo-yin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra detectar o pitch da voz eu uso o <a href="http://audition.ens.fr/adc/pdf/2002_JASA_YIN.pdf"target="_blank" rel="noopener">YIN</a>, de Alain de Cheveigné (IRCAM-CNRS) e Hideki Kawahara (Universidade de Wakayama). É um estimador de frequência fundamental no domínio do tempo. A ideia central é a Cumulative Mean Normalized Difference Function (CMNDF), que basicamente mede o quão periódico é o sinal em cada lag, normaliza pra reduzir falsos positivos, e usa interpolação parabólica pra refinar o resultado. É leve o suficiente pra rodar em tempo real no celular, o que importa aqui.</p>
<p>No app, o threshold do YIN é 0.70 (ajustado pra sinais mistos de voz + música), e frames com confiança abaixo de 0.3 são descartados. Abaixo disso, provavelmente é ruído ou instrumento.</p>
<h2>Os 4 modos de pontuação<span class="hx:absolute hx:-mt-20" id="os-4-modos-de-pontuação"></span>
    <a href="#os-4-modos-de-pontua%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cada modo avalia um aspecto diferente da qualidade vocal. Todos compartilham o mesmo pipeline de áudio (bandpass → YIN → gate de confiança). A diferença é como interpretam o pitch detectado.</p>
<h3>Pitch Match<span class="hx:absolute hx:-mt-20" id="pitch-match"></span>
    <a href="#pitch-match" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mede o quão limpo você sustenta as notas. Usa decaimento gaussiano baseado no desvio padrão dos valores MIDI numa janela rolante de ~15 frames. Notas firmes (desvio &lt; 0.3 semitons) pontuam 85-100%. Voz tremendo (desvio &gt; 2 semitons) pontua perto de zero. Bom pra músicas que você já conhece bem.</p>
<h3>Contour<span class="hx:absolute hx:-mt-20" id="contour"></span>
    <a href="#contour" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mede a forma melódica do seu canto. Não importa qual nota exata você acerta, só a direção e o fluxo. Avalia a amplitude do pitch e movimentos melódicos (saltos &gt; 0.5 semitom) numa janela rolante. Canto monótono pontua ~10%. Movimento melódico suave com amplitude de 2-6 semitons pontua 70-100%. Bom pra quando você está aprendendo uma música nova.</p>
<h3>Intervals<span class="hx:absolute hx:-mt-20" id="intervals"></span>
    <a href="#intervals" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mede a qualidade musical dos saltos entre notas consecutivas. Tom inteiro (2 semitons) pontua mais alto. Terças e quartas pontuam bem. Saltos descontrolados de uma oitava ou mais pontuam baixo. Usa uma curva gaussiana centrada no tom inteiro. Funciona quando você está cantando numa tonalidade diferente da original.</p>
<h3>Streak<span class="hx:absolute hx:-mt-20" id="streak"></span>
    <a href="#streak" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>É o Pitch Match com multiplicador combo. Cada frame consecutivo com score acima de 0.4 incrementa o contador de streak. O streak adiciona pontos bônus (até +0.4 em streak de 30+). Quebrar um streak &gt; 5 frames empurra uma penalidade de 0.05 no EMA. Silêncio congela o streak, então pausas instrumentais não te prejudicam. O modo mais divertido pra festas.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/score-display-detail.jpg" alt="Detalhe do display de pontuação: score ao vivo, score geral, trail de pitch com grid de notas (C3-G5)"  loading="lazy" /></p>
<p>A lógica por trás desses quatro modos veio da pesquisa que o Claude fez nos papers acadêmicos. Cada um mede uma dimensão diferente: afinação, contorno melódico, fraseado e consistência. Nenhum deles sozinho é suficiente, mas juntos cobrem razoavelmente bem o que dá pra avaliar sem ter a melodia de referência da música.</p>
<h2>O Pitch Oracle<span class="hx:absolute hx:-mt-20" id="o-pitch-oracle"></span>
    <a href="#o-pitch-oracle" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Além dos quatro modos puramente vocais, o app tem o que eu chamo de Pitch Oracle. A ideia: em vez de avaliar sua voz isoladamente, o app baixa o áudio de referência do vídeo via <code>youtube_explode_dart</code>, decodifica pra PCM, roda o YIN nele, e constrói um timeline de pitch com timestamp da música inteira. Durante a pontuação, se o pitch do microfone bate com o pitch da referência naquele momento do vídeo, provavelmente é bleed do alto-falante, e é ignorado. Se difere, é sua voz, e é pontuado.</p>
<p>A sincronização funciona pelo <code>currentTime</code> do elemento de vídeo HTML5, enviado pro Dart via um listener JS de <code>timeupdate</code> a cada ~250ms. O oracle consulta o pitch de referência na posição exata do playback, levando em conta pause, seek e mudança de velocidade.</p>
<p>Na primeira vez que você toca uma música, o oracle leva uns 5-15 segundos pra baixar e analisar o áudio. Mas o timeline é salvo como JSON no cache local do app (<code>pitch_oracle/&lt;videoId&gt;.json</code>). Se você tocar a mesma música de novo, carrega instantaneamente do cache, sem request de rede. Isso também resolve o problema de rate limiting do YouTube pras músicas que você mais canta.</p>
<p>Com o oracle ativo, os modos mudam de comportamento. Pitch Match compara a classe de pitch do cantor contra a da referência (agnóstico à oitava, como o SingStar). Contour usa correlação cruzada entre o movimento de pitch do cantor e o da referência. Intervals compara os saltos em semitons contra os da referência.</p>
<p>Quando o YouTube bloqueia o download por rate limiting (acontece depois de muitos requests seguidos do mesmo IP, limpa em 15-30 minutos), o oracle falha silenciosamente e os modos voltam pra análise puramente vocal.</p>
<h2>O caminho até aqui<span class="hx:absolute hx:-mt-20" id="o-caminho-até-aqui"></span>
    <a href="#o-caminho-at%c3%a9-aqui" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O app que você vê agora passou por muita iteração antes de chegar nesse estado.</p>
<p>Primeiro, eu tentei fazer uma versão desktop Linux pra facilitar o debug. Faz sentido, né? Testa no desktop, itera rápido, depois porta pro celular. O problema é que o Flutter não tem backend de webview pra Linux desktop. O <code>webview_flutter</code> simplesmente não funciona. Tentei o <code>webview_cef</code>, que é baseado no Chromium Embedded Framework. O CEF spawna seu próprio processo de GPU, e no Hyprland (compositor Wayland baseado no wlroots) isso conflita com o pipeline de renderização do compositor. No meu setup com NVIDIA, a sessão inteira do Hyprland congelou. Tela travada, sem resposta a teclado, tive que matar pelo TTY. Fora que o CEF exige download de um binário de ~200MB na primeira build. Desisti do CEF e escrevi com o Claude uma bridge nativa em C++ usando WebKitGTK com method channels do Flutter. Funcionou, mas cada peculiaridade do YouTube exigia código separado pro Linux e pro Android. O <code>just_audio</code> também não tem implementação pra Linux desktop. No fim a versão Linux virou peso morto. Deletei ~1.500 linhas de código Linux-específico e foquei só no Android.</p>
<p>Depois veio a saga do microfone no Samsung. No meu Galaxy Z Fold, o microfone captava um sinal absurdamente baixo. Pico de ~0.005, basicamente silêncio pro detector de pitch. Fiquei umas duas horas tentando entender. Baixei thresholds, aumentei ganho por software até 50x, desabilitei preprocessadores de áudio. Nada funcionava direito. Até que descobri o problema real: o <code>AutomaticGainControl</code> do Android. O nome diz &ldquo;controle automático de ganho&rdquo;, o que sugere que ele <em>amplifica</em> sinais fracos. Na implementação DSP dos Samsung, ele faz o oposto. Ele <em>atenua</em> o sinal pra um nível de referência baixo, otimizado pra chamadas de voz. Com AGC ligado, o pico caía de ~0.06 pra ~0.003. Desligar o AGC resolveu. Mas aí o pacote <code>audio_session</code> religava o AGC por baixo dos panos. Removi ele também. Foram três rodadas de fix, cada uma achando mais uma camada do problema.</p>
<p>E a pontuação. A pontuação levou mais tempo que todo o resto junto. A primeira implementação usava média cumulativa, que deixava a nota travada num valor e não respondia ao canto em tempo real. Troquei pra janela rolante. Aí a nota ficava presa em ~50% por causa de um bug no peso da pontuação primária. Consertei, e ela passou a começar em 70% mesmo sem ninguém cantando. Consertei de novo. O modo Streak não resetava direito no silêncio. O chromatic snap dava nota alta pra qualquer coisa. O histórico de pitch não era limpo nos gaps de silêncio e os modos ficavam estagnados. Cada bug corrigido revelava outro. Foram mais de 25 commits só ajustando a pontuação, do primeiro protótipo até o estado atual.</p>
<p>O resultado não é perfeito. Eu sei. Mas funciona o suficiente pra ser divertido, que era o objetivo desde o começo.</p>
<h2>Configurações<span class="hx:absolute hx:-mt-20" id="configurações"></span>
    <a href="#configura%c3%a7%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/settings-panel.jpg" alt="Painel de configurações: presets de microfone, pitch shift, calibração"  loading="lazy" /></p>
<p>O painel de configurações fica no ícone de engrenagem do overlay. Tem três presets de microfone pra diferentes ambientes (microfone externo limpo, sala normal, festa barulhenta), cada um ajustando thresholds de confiança e amplitude. Tem pitch shift pra quando a música é aguda demais pra sua extensão vocal. O ajuste muda tanto o áudio do vídeo quanto a pontuação ao mesmo tempo: usa o <code>playbackRate</code> do elemento HTML5 com <code>preservesPitch=false</code>, então +2 semitons acelera o áudio pra 1.12x (pitch sobe) e -2 semitons desacelera pra 0.89x (pitch desce). A pontuação compensa o offset, então você canta na sua faixa confortável e o sistema avalia corretamente. Tem calibração de microfone, um processo de 3 segundos que mede o ruído da sala e adapta os thresholds. E tem restart pra zerar a pontuação sem recarregar o vídeo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/frank-karaoke/scoring-modes-selector.jpg" alt="Seletor de modos de pontuação"  loading="lazy" /></p>
<p>Pra trocar de modo de pontuação, toque na caixa de score durante a reprodução.</p>
<h2>Fluxo de uso<span class="hx:absolute hx:-mt-20" id="fluxo-de-uso"></span>
    <a href="#fluxo-de-uso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ol>
<li>Abra o app. O YouTube carrega dentro do app com o logo do Frank Karaoke.</li>
<li>Busque um vídeo de karaokê. Qualquer vídeo funciona, mas faixas instrumentais com letra na tela dão melhores resultados.</li>
<li>O vídeo pausa brevemente pra inicializar o microfone, baixar dados da música pro pitch oracle, e preparar o overlay. Na primeira vez com uma música nova isso leva uns 5-15 segundos. Se você já tocou essa música antes, carrega do cache instantaneamente.</li>
<li>Cante. O score &ldquo;live&rdquo; reflete sua performance atual (média exponencial com alpha 0.15, resposta em ~1 segundo). O score &ldquo;overall&rdquo; é a média cumulativa da música inteira.</li>
<li>Quando o vídeo pausa, a pontuação pausa junto (pra não pontuar ruído ambiente). Se fizer seek, o score zera e tem 5 segundos de warmup.</li>
</ol>
<h2>Como instalar<span class="hx:absolute hx:-mt-20" id="como-instalar"></span>
    <a href="#como-instalar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O app ainda não está na Play Store, estou esperando o Google verificar minha identidade de desenvolvedor. Deve aparecer lá nos próximos dias. Enquanto isso, é um projeto aberto e dá pra instalar direto.</p>
<p>O jeito mais fácil é baixar o APK assinado direto da <a href="https://github.com/akitaonrails/frank_karaoke/releases"target="_blank" rel="noopener">página de releases do GitHub</a>. No celular ou tablet Android, baixe o <code>FrankKaraoke-0.2.0-android.apk</code>, abra e toque em Instalar. Se o Android reclamar de &ldquo;fontes desconhecidas&rdquo;, habilite em Configurações &gt; Segurança pro seu navegador. Na primeira execução o app pede permissão de microfone. Depois vá nas configurações (ícone de engrenagem) e calibre o microfone antes de cantar, são 3 segundos.</p>
<p>Se você quer compilar do fonte ou contribuir, o repositório está no <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">GitHub</a>. Precisa de Flutter SDK 3.10+, Android SDK API 24+, e um dispositivo físico pra testar microfone (emulador não dá resultados representativos).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/frank_karaoke.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> frank_karaoke
</span></span><span class="line"><span class="cl">flutter pub get
</span></span><span class="line"><span class="cl">flutter run -d &lt;device_id&gt;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O README tem o resto.</p>
<p>Stack: Flutter + Riverpod pra state management, <code>webview_flutter</code> pro YouTube, <code>youtube_explode_dart</code> pra extração de áudio, <code>record</code> pra captura PCM do microfone, <code>audio_decoder</code> pra decodificação de referência via Android MediaCodec, e o algoritmo YIN implementado em Dart puro.</p>
<p>A documentação técnica do sistema de pontuação está em <a href="https://github.com/akitaonrails/frank_karaoke/blob/main/docs/scoring.md"target="_blank" rel="noopener"><code>docs/scoring.md</code></a> no repositório. Cobre como SingStar, Joysound e DAM funcionam, os papers acadêmicos, a arquitetura do pitch oracle, os problemas de isolamento de voz no Android, e o roadmap.</p>
<h2>A pontuação é experimental<span class="hx:absolute hx:-mt-20" id="a-pontuação-é-experimental"></span>
    <a href="#a-pontua%c3%a7%c3%a3o-%c3%a9-experimental" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Preciso ser direto: o sistema de pontuação é experimental. Sem arquivos de referência por música, a avaliação é aproximada. O app mede se você está afinado, se segue um contorno melódico, se seus intervalos são musicais, se é consistente. Mas não te diz se você está cantando a melodia certa desta música específica (a menos que o pitch oracle consiga baixar o áudio, e nem sempre consegue).</p>
<p>Se você tem experiência com processamento de áudio, detecção de pitch, ou avaliação musical, o repositório está aberto e a documentação de pesquisa em <a href="https://github.com/akitaonrails/frank_karaoke/blob/main/docs/scoring.md"target="_blank" rel="noopener"><code>docs/scoring.md</code></a> detalha o que foi tentado, o que funciona e o que não funciona. Em particular: calibração dos thresholds dos modos, melhorias no isolamento de voz, e integração com o <a href="https://github.com/rakuri255/UltraSinger"target="_blank" rel="noopener">UltraSinger</a> (que gera arquivos de referência a partir de músicas usando Demucs + basic-pitch + WhisperX) são áreas onde contribuição de quem entende do assunto faria diferença. Apreciamos qualquer ajuda de especialistas na calibração desses sistemas.</p>
<p>Ah, e o nome. Frank Karaoke. É uma homenagem ao Sinatra. Quem mais?</p>
<p>Projeto no GitHub: <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">github.com/akitaonrails/frank_karaoke</a></p>
]]></content:encoded><category>flutter</category><category>android</category><category>karaoke</category><category>audio</category><category>pitch-detection</category><category>AI</category><category>open-source</category></item><item><title>Bitcoin no Home Server: Soberania e Privacidade com Coldcard, Sparrow e Fulcrum</title><link>https://www.akitaonrails.com/2026/04/01/bitcoin-no-home-server-soberania-e-privacidade-com-coldcard-sparrow-e-fulcrum/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/01/bitcoin-no-home-server-soberania-e-privacidade-com-coldcard-sparrow-e-fulcrum/</guid><pubDate>Wed, 01 Apr 2026 19:00:00 GMT</pubDate><description>&lt;p&gt;Este post é complemento direto dos meus artigos recentes sobre o &lt;a href="https://www.akitaonrails.com/2026/03/31/migrando-meu-home-server-com-claude-code/"&gt;novo home server com openSUSE MicroOS&lt;/a&gt; e sobre o &lt;a href="https://www.akitaonrails.com/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;Minisforum MS-S1 Max&lt;/a&gt;. Naqueles textos eu falei da base. Aqui eu quero mostrar um uso concreto dela: montar uma stack decente de Bitcoin em casa, com foco em privacidade, soberania operacional e transações seguras do meu lado.&lt;/p&gt;
&lt;p&gt;Antes de mais nada: isto aqui não é texto de evangelismo nem chamada pra day trade. Muito pelo contrário. No momento em que escrevo, em 1 de abril de 2026, o Bitcoin está na faixa de US$ 68 mil e perto de R$ 391 mil, abaixo dos picos de 2025. Muita gente olha pra isso e entra em pânico ou começa a fantasiar trade alavancado. Eu acho as duas reações erradas. Existe uma tese de &amp;ldquo;super cycle&amp;rdquo; baseada em demanda institucional, ETFs spot e efeito defasado do halving. Pode ser. Pode não ser. O que eu sei é que candle de curto prazo não muda a parte que realmente me interessa: infraestrutura. Se você precisa de alavancagem pra &amp;ldquo;acelerar ganhos&amp;rdquo;, você provavelmente está só acelerando sua chance de ser liquidado.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é complemento direto dos meus artigos recentes sobre o <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">novo home server com openSUSE MicroOS</a> e sobre o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">Minisforum MS-S1 Max</a>. Naqueles textos eu falei da base. Aqui eu quero mostrar um uso concreto dela: montar uma stack decente de Bitcoin em casa, com foco em privacidade, soberania operacional e transações seguras do meu lado.</p>
<p>Antes de mais nada: isto aqui não é texto de evangelismo nem chamada pra day trade. Muito pelo contrário. No momento em que escrevo, em 1 de abril de 2026, o Bitcoin está na faixa de US$ 68 mil e perto de R$ 391 mil, abaixo dos picos de 2025. Muita gente olha pra isso e entra em pânico ou começa a fantasiar trade alavancado. Eu acho as duas reações erradas. Existe uma tese de &ldquo;super cycle&rdquo; baseada em demanda institucional, ETFs spot e efeito defasado do halving. Pode ser. Pode não ser. O que eu sei é que candle de curto prazo não muda a parte que realmente me interessa: infraestrutura. Se você precisa de alavancagem pra &ldquo;acelerar ganhos&rdquo;, você provavelmente está só acelerando sua chance de ser liquidado.</p>
<p>Pra mim, a pergunta útil não é &ldquo;vai subir amanhã?&rdquo;. A pergunta útil é: &ldquo;se eu quiser guardar e movimentar Bitcoin sem terceirizar tudo pra exchange, wallet web e API pública, como eu monto isso direito aqui em casa?&rdquo;</p>
<h2>O problema real: conveniência demais custa privacidade demais<span class="hx:absolute hx:-mt-20" id="o-problema-real-conveniência-demais-custa-privacidade-demais"></span>
    <a href="#o-problema-real-conveni%c3%aancia-demais-custa-privacidade-demais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O fluxo padrão da maioria das pessoas é simples: compra em exchange, deixa parado lá, ou então instala uma wallet qualquer no celular e pronto. Funciona. Também concentra risco e vaza metadado pra todo lado.</p>
<p>Se você deixa saldo em exchange, você tem risco de custódia. Se você usa wallet desktop apontando pra servidor público, você tem risco de privacidade. Se você usa hardware wallet de forma relaxada, comprada no Mercado Livre de segunda mão, você tem risco de supply chain. Se mistura tudo isso com pressa, pior ainda.</p>
<p>Por isso eu acabei chegando numa combinação que, pra quem é técnico e quer montar a própria infra, me parece bastante sólida:</p>
<ul>
<li>Coldcard pro armazenamento frio</li>
<li>Sparrow Wallet no Linux como carteira desktop e coordenadora das transações</li>
<li>Fulcrum no home server como servidor Electrum privado</li>
<li>bitcoind no mesmo servidor como full node de verdade, validando a cadeia e fazendo broadcast sem depender de terceiros</li>
</ul>
<p>Não é o caminho mais fácil. Mas esse é justamente o ponto. Segurança de verdade raramente vem do caminho mais fácil.</p>
<h2>Os conceitos que confundem quem está começando<span class="hx:absolute hx:-mt-20" id="os-conceitos-que-confundem-quem-está-começando"></span>
    <a href="#os-conceitos-que-confundem-quem-est%c3%a1-come%c3%a7ando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de entrar no stack, vale alinhar quatro termos que normalmente são jogados no ar como se todo mundo já soubesse:</p>
<table>
  <thead>
      <tr>
          <th>Conceito</th>
          <th>O que é</th>
          <th>Por que importa</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Airgap</td>
          <td>Dispositivo que nunca toca na internet nem por USB de dados</td>
          <td>Reduz a superfície de ataque do signer</td>
      </tr>
      <tr>
          <td>PSBT</td>
          <td>Partially Signed Bitcoin Transaction</td>
          <td>Formato padrão pra preparar, assinar e finalizar transações em etapas</td>
      </tr>
      <tr>
          <td>Watch-only wallet</td>
          <td>Carteira que enxerga saldo/endereço mas não guarda chave privada</td>
          <td>Ótima pra desktop: observa e monta a transação, mas não assina</td>
      </tr>
      <tr>
          <td>Full node</td>
          <td>Nó que valida blocos e regras do protocolo localmente</td>
          <td>Você não precisa &ldquo;acreditar&rdquo; em API de ninguém</td>
      </tr>
      <tr>
          <td>Electrum server</td>
          <td>Camada de índice que responde rápido consultas de carteira</td>
          <td>Sem isso, wallets desktop ficam dependentes de servidores públicos</td>
      </tr>
  </tbody>
</table>
<p>Em português claro, o fluxo fica assim:</p>
<ol>
<li>A Sparrow, no desktop, monta a transação.</li>
<li>Essa transação vira um PSBT.</li>
<li>O PSBT vai pra Coldcard por microSD.</li>
<li>A Coldcard assina offline.</li>
<li>O arquivo assinado volta pra Sparrow.</li>
<li>A Sparrow faz o broadcast via seu próprio servidor, não via infraestrutura pública de terceiros.</li>
</ol>
<p>É isso que as pessoas querem dizer quando falam em &ldquo;airgapped workflow&rdquo;. Não é magia. É só separação de papéis feita de forma disciplinada.</p>
<h2>Coldcard: signer frio, offline, chato do jeito certo<span class="hx:absolute hx:-mt-20" id="coldcard-signer-frio-offline-chato-do-jeito-certo"></span>
    <a href="#coldcard-signer-frio-offline-chato-do-jeito-certo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu uso <a href="https://coldcard.com/"target="_blank" rel="noopener">Coldcard</a> como cold storage. O motivo é simples: ela foi pensada desde o começo como dispositivo Bitcoin-only, com foco forte em operação airgapped por microSD. Isso já elimina uma categoria enorme de &ldquo;comodidades&rdquo; que muita gente acha prática, mas que eu prefiro não ter perto das minhas chaves.</p>
<p><a href="https://coldcard.com/mk4"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/bitcoin-sovereignty/coldcard-mk4-official.png" alt="Coldcard Mk4 no site oficial da Coinkite"  loading="lazy" /></a></p>
<p>Na prática, a Coldcard fica com a parte mais importante do sistema: a chave privada. Ela não precisa saber de servidor, de Electrum, de API pública, de exchange, de nada disso. O trabalho dela é um só: assinar transação offline.</p>
<p>Esse desacoplamento é ótimo por dois motivos:</p>
<ul>
<li>O desktop pode ser conveniente sem virar ponto único de desastre.</li>
<li>O signer continua isolado mesmo se sua máquina principal der problema.</li>
</ul>
<p>E aqui entra um aviso que eu realmente quero deixar em caixa alta mental:</p>
<p><strong>Nunca compre hardware wallet de segunda mão. Nunca.</strong></p>
<p>Não é exagero. Você não tem como saber, de verdade, o que aconteceu com aquele dispositivo antes de chegar na sua mão. Pode ter seed pré-gerada, firmware adulterado, componente trocado, embalagem refeita, supply chain comprometida, ou simplesmente algum truque bobo esperando você baixar a guarda. Hardware wallet é uma daquelas categorias em que economizar R$ 300 comprando usado é insanidade. Compre sempre do site oficial do fabricante ou de revendedor oficialmente autorizado pelo fabricante. E mesmo assim confira lacres, procedência e firmware.</p>
<h2>Dá pra fazer parecido com um celular velho?<span class="hx:absolute hx:-mt-20" id="dá-pra-fazer-parecido-com-um-celular-velho"></span>
    <a href="#d%c3%a1-pra-fazer-parecido-com-um-celular-velho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dá. Mas eu trataria isso como alternativa de estudo ou de orçamento, não como substituto óbvio de uma Coldcard.</p>
<p>O caminho mais sério hoje pra isso é o <a href="https://airgap.it/"target="_blank" rel="noopener">AirGap Vault</a>, que foi justamente desenhado pra usar um smartphone antigo como signer offline, via QR code, mantendo o aparelho fora da rede. A ideia é boa, e pra muita gente pode ser a porta de entrada correta.</p>
<p>Mas tem trade-off:</p>
<ul>
<li>smartphone velho não foi desenhado como hardware wallet dedicada</li>
<li>histórico anterior do aparelho importa</li>
<li>bateria envelhecida, tela ruim e Android abandonado são problemas reais</li>
<li>o modelo de ameaça é menos claro do que num dispositivo dedicado</li>
</ul>
<p>Então minha visão é simples: dá pra usar? Dá. Eu recomendaria como solução principal pra guardar patrimônio relevante? Não. Pra isso eu ainda prefiro hardware dedicado comprado da fonte correta.</p>
<h2>Sparrow Wallet: a melhor peça de desktop desse quebra-cabeça<span class="hx:absolute hx:-mt-20" id="sparrow-wallet-a-melhor-peça-de-desktop-desse-quebra-cabeça"></span>
    <a href="#sparrow-wallet-a-melhor-pe%c3%a7a-de-desktop-desse-quebra-cabe%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Linux, eu uso <a href="https://sparrowwallet.com/"target="_blank" rel="noopener">Sparrow Wallet</a>. Pra mim, hoje, ela é uma das melhores peças de software nesse ecossistema.</p>
<p><a href="https://www.sparrowwallet.com/features/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/bitcoin-sovereignty/sparrow-transactions.png" alt="Sparrow Wallet em execução, mostrando o histórico detalhado da carteira e das transações"  loading="lazy" /></a></p>
<p>O que eu gosto nela:</p>
<ul>
<li>funciona muito bem em desktop Linux</li>
<li>suporta hardware wallets direito</li>
<li>entende PSBT sem drama</li>
<li>deixa muito claro o que está acontecendo na transação</li>
<li>é ótima como watch-only wallet</li>
</ul>
<p>No meu fluxo, a Sparrow faz três coisas:</p>
<ol>
<li>Guarda a carteira watch-only.</li>
<li>Monta a transação com os outputs e taxas.</li>
<li>Recebe de volta a assinatura da Coldcard e faz o broadcast.</li>
</ol>
<p>Essa separação é elegante. O desktop vira coordenador. O signer continua frio.</p>
<h2>Por que Coldcard + Sparrow funciona tão bem<span class="hx:absolute hx:-mt-20" id="por-que-coldcard--sparrow-funciona-tão-bem"></span>
    <a href="#por-que-coldcard--sparrow-funciona-t%c3%a3o-bem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa combinação é boa porque cada peça faz o que faz melhor:</p>
<ul>
<li>a Coldcard protege a chave</li>
<li>a Sparrow organiza o uso humano da carteira</li>
<li>o servidor cuida da infraestrutura</li>
</ul>
<p>Muita wallet tenta fazer tudo. Eu prefiro esse desenho modular. É menos &ldquo;mágico&rdquo;, mais explícito, e mais fácil de raciocinar sobre ele sem autoengano.</p>
<p>Se eu estou no desktop, eu quero visibilidade. Se eu estou no signer, eu quero isolamento. Se eu estou no servidor, eu quero validação e índice local. Essa divisão é limpa.</p>
<h2>O problema da Sparrow sem infra própria<span class="hx:absolute hx:-mt-20" id="o-problema-da-sparrow-sem-infra-própria"></span>
    <a href="#o-problema-da-sparrow-sem-infra-pr%c3%b3pria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora vem o detalhe importante. A Sparrow sozinha não resolve privacidade.</p>
<p>Se você instalar, abrir e simplesmente usar servidores públicos, alguém do outro lado passa a aprender bastante coisa sobre sua carteira: conjunto de endereços, xpubs ou derivadas, saldo, histórico, comportamento de consulta, broadcast. Não é custódia, mas ainda é exposição.</p>
<p>Esse é o buraco que o Fulcrum preenche.</p>
<h2>Fulcrum: o servidor Electrum privado da casa<span class="hx:absolute hx:-mt-20" id="fulcrum-o-servidor-electrum-privado-da-casa"></span>
    <a href="#fulcrum-o-servidor-electrum-privado-da-casa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> é um servidor Electrum. Em vez de deixar a Sparrow perguntando coisas pra servidor público de terceiros, ela pergunta pro meu próprio servidor.</p>
<p>Na prática, isso significa:</p>
<ul>
<li>consulta de saldo local</li>
<li>histórico local</li>
<li>descoberta de endereços local</li>
<li>broadcast local</li>
</ul>
<p>Ou seja: a wallet desktop para de &ldquo;telefonar&rdquo; pro mundo toda vez que você abre o programa.</p>
<p>No meu setup atual, a Sparrow aponta pra um Fulcrum rodando no home server da LAN, com porta <code>50001</code> na rede interna e <code>50002</code> com TLS.</p>
<h2>E por que Fulcrum não basta sozinho<span class="hx:absolute hx:-mt-20" id="e-por-que-fulcrum-não-basta-sozinho"></span>
    <a href="#e-por-que-fulcrum-n%c3%a3o-basta-sozinho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Porque Fulcrum não substitui um full node. Ele indexa em cima de um full node.</p>
<p>Quem realmente valida bloco, regra de consenso, script, transação e cadeia é o <code>bitcoind</code>. O Fulcrum fica na frente como camada de índice, porque Bitcoin Core puro não foi feito pra servir carteira desktop com esse tipo de consulta rápida.</p>
<p>Então a arquitetura correta é:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Coldcard (offline signer)
</span></span><span class="line"><span class="cl">        ^
</span></span><span class="line"><span class="cl">        | microSD / PSBT
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">Sparrow Wallet (desktop watch-only + coordinator)
</span></span><span class="line"><span class="cl">        |
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">Fulcrum (Electrum server privado)
</span></span><span class="line"><span class="cl">        |
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">bitcoind (full node)</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>O que eu subi no home server de verdade<span class="hx:absolute hx:-mt-20" id="o-que-eu-subi-no-home-server-de-verdade"></span>
    <a href="#o-que-eu-subi-no-home-server-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No meu home server, a stack mora numa pasta dedicada do Docker Compose e é composta por dois containers:</p>
<ul>
<li><code>bitcoin-bitcoind</code></li>
<li><code>bitcoin-fulcrum</code></li>
</ul>
<p>O compose é simples. E isso é bom. Infra sensível não ganha nada ficando esperta demais no YAML.</p>
<p>O desenho principal é este:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">bitcoin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">lncm/bitcoind:v28.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">bitcoin-bitcoind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;${BITCOIN_UID}:${BITCOIN_GID}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/data:/data/.bitcoin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8333:8333&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;bitcoin-cli&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-datadir=/data/.bitcoin&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;ping&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">60s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">fulcrum</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cculianu/fulcrum:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">bitcoin-fulcrum</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/fulcrum:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/data:/bitcoin:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;Fulcrum&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;/data/fulcrum.conf&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;50001:50001&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;50002:50002&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">bitcoin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso, a restrição do <code>50001</code> à LAN acontece na camada de rede do host. O YAML acima é o esqueleto do stack, não a política inteira de firewall.</p>
<p>As partes mais importantes disso:</p>
<ul>
<li><code>restart: always</code> porque isso é serviço de longa duração</li>
<li>volume explícito pra não perder estado</li>
<li><code>user: &quot;${BITCOIN_UID}:${BITCOIN_GID}&quot;</code> porque o diretório persistente precisa casar com o ownership real do storage, então eu prefiro fixar UID/GID de forma explícita em vez de confiar no default da imagem</li>
<li>o RPC não é publicado no host; ele fica só na rede interna do Compose, que é tudo o que o Fulcrum precisa</li>
<li>o healthcheck usa o próprio <code>.cookie</code> local do Bitcoin Core, então não precisa espalhar senha fixa em comando</li>
<li>o Fulcrum monta o datadir do node em leitura apenas para autenticar via <code>.cookie</code> sem inventar credencial paralela</li>
<li>no <code>fulcrum.conf</code>, isso vira uma configuração simples: falar com <code>bitcoin:8332</code> e ler o <code>.cookie</code> montado, em vez de repetir credencial em texto puro</li>
<li><code>security_opt: label:disable</code> porque neste host com MicroOS, SELinux e bind mounts sensíveis eu preferi a rota pragmática de desarmar esse atrito específico em vez de perder tempo brigando com label em volume que já está sendo tratado de forma controlada</li>
<li><code>depends_on</code> com <code>service_healthy</code> pra o Fulcrum só subir depois que o RPC do bitcoind responder</li>
<li><code>stop_grace_period: 5m</code> porque bitcoind precisa tempo real pra flushar estado em shutdown gracioso</li>
</ul>
<h2>A versão final<span class="hx:absolute hx:-mt-20" id="a-versão-final"></span>
    <a href="#a-vers%c3%a3o-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Hoje, o desenho que eu quero manter é esse: <code>bitcoind</code> com <code>txindex</code>, <code>dbcache=1024</code>, volume persistente, parada graciosa de 5 minutos, autenticação por <code>.cookie</code>, e Fulcrum na frente servindo Sparrow pela LAN ou via TLS.</p>
<p>A stack atual está assim:</p>
<table>
  <thead>
      <tr>
          <th>Componente</th>
          <th>Estado</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bitcoin Core</td>
          <td><code>28.0</code></td>
      </tr>
      <tr>
          <td>Fulcrum</td>
          <td><code>2.1.0</code></td>
      </tr>
      <tr>
          <td>Stop timeout do container</td>
          <td><code>300</code> segundos</td>
      </tr>
      <tr>
          <td>Data dir do node</td>
          <td>volume persistente dedicado montado em <code>/data/.bitcoin</code></td>
      </tr>
      <tr>
          <td>Rede</td>
          <td><code>8333</code> pra P2P, RPC só na rede interna do Compose, <code>50001/50002</code> pro Electrum privado</td>
      </tr>
  </tbody>
</table>
<p>Não me interessa transformar isso em espetáculo. O ponto é mais simples: a infra final precisa ser chata, previsível e estável.</p>
<h2>Os tunings que importam de verdade<span class="hx:absolute hx:-mt-20" id="os-tunings-que-importam-de-verdade"></span>
    <a href="#os-tunings-que-importam-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não tem mágica aqui. Tem alguns parâmetros que fazem diferença real e um monte de coisa que só serve pra enfeitar compose.</p>
<p>O <code>stop_grace_period: 5m</code> existe porque bitcoind não é container descartável de stateless API. Ele mantém chainstate, índices e cache em memória. Se você não dá tempo pro processo encerrar direito, você cria trabalho desnecessário no próximo start.</p>
<p>O <code>user: &quot;${BITCOIN_UID}:${BITCOIN_GID}&quot;</code> está ali por um motivo bem menos glamouroso e muito mais importante: storage persistente com permissão errada é um jeito excelente de quebrar serviço bom. Então eu prefiro alinhar o container ao ownership real do volume em vez de deixar isso implícito.</p>
<p>O <code>dbcache=1024</code> é o ponto que eu acho mais sensato pra node doméstico sempre ligado. Grande o suficiente pra não ficar sofrendo em I/O o tempo todo, pequeno o suficiente pra não transformar cada restart num parto.</p>
<p>O <code>txindex=1</code> eu mantenho porque quero o node completo, não uma instalação minimalista só pra dizer que &ldquo;roda Bitcoin&rdquo;. Se a ideia aqui é autonomia operacional, eu prefiro ter o índice completo.</p>
<p>O <code>rpcworkqueue=512</code> e <code>rpcthreads=16</code> são o tipo de ajuste que faz sentido quando você sabe que vai colocar Fulcrum perguntando pro node o dia inteiro e quer alguma folga.</p>
<p>No lado do Fulcrum, os parâmetros principais são:</p>
<ul>
<li><code>db_mem = 8192</code></li>
<li><code>db_max_open_files = -1</code></li>
<li><code>bitcoind_clients = 8</code></li>
<li><code>worker_threads = 0</code></li>
<li><code>peering = false</code></li>
</ul>
<p>De novo: nada de esoterismo. Só cache suficiente, paralelismo razoável e nada de anunciar esse servidor como serviço público.</p>
<p>No meu <code>bitcoin.conf</code> atual, o núcleo importante ficou assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">server</span><span class="o">=</span><span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">txindex</span><span class="o">=</span><span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">prune</span><span class="o">=</span><span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcbind</span><span class="o">=</span><span class="s">0.0.0.0</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcallowip</span><span class="o">=</span><span class="s">172.0.0.0/8</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcthreads</span><span class="o">=</span><span class="s">16</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcworkqueue</span><span class="o">=</span><span class="s">512</span>
</span></span><span class="line"><span class="cl"><span class="na">dbcache</span><span class="o">=</span><span class="s">1024</span>
</span></span><span class="line"><span class="cl"><span class="na">maxmempool</span><span class="o">=</span><span class="s">512</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tudo isso faz sentido num servidor com RAM decente e NVMe rápido. Mas o detalhe que mais importa continua sendo o shutdown limpo. Infra de wallet não tem espaço pra mentalidade de &ldquo;depois a gente vê&rdquo;.</p>
<h2>O tamanho real disso tudo<span class="hx:absolute hx:-mt-20" id="o-tamanho-real-disso-tudo"></span>
    <a href="#o-tamanho-real-disso-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é outro ponto que muita gente subestima.</p>
<p>Se você olhar documentação mais antiga do Bitcoin Core, vai encontrar números como 350 GB de disco para um node com configuração padrão. Isso já ficou pra trás. Dados mais atuais do tamanho da blockchain apontam algo em torno de <strong>725.82 GB em 11 de março de 2026</strong>, e isso é só a cadeia bruta, sem contar os índices extras que muita gente técnica vai querer manter.</p>
<p>E aqui entra a pegadinha: o stack que eu estou descrevendo não é &ldquo;Bitcoin Core pelado só pra dizer que roda um node&rdquo;. É <code>bitcoind</code> com <code>txindex</code>, mais Fulcrum, mais margem pra rebuild, logs, snapshots e crescimento normal da rede.</p>
<p>Por isso, pra montar algo parecido hoje, eu pensaria assim:</p>
<ul>
<li>abaixo de 1 TB: eu nem começaria</li>
<li>1 TB: mínimo pragmático</li>
<li>2 TB: faixa confortável</li>
<li>acima disso: se você quer folga de longo prazo, snapshots e menos ansiedade operacional</li>
</ul>
<p>E aqui vai a observação mais importante de todas em self-hosting: não assuma que persistência, montagem e backup estão certos só porque o YAML está bonito. Verifique de verdade.</p>
<p>Outra coisa que eu não esqueceria num host com btrfs: colocar o banco do Fulcrum (<code>fulc2_db</code>) em subvolume separado. O motivo é bem mundano. Esse diretório cresce, muda o tempo inteiro e não tem nada a ver com snapshot automático genérico de <code>/var</code>. Se você mistura tudo, acaba arrastando índice grande e reconstruível junto com snapshot de sistema, queimando espaço e deixando a manutenção mais irritante do que precisava. Índice do Fulcrum não é configuração delicada. É dado pesado, volátil e reconstruível. Eu trato exatamente assim.</p>
<h2>Hardening: o que já deixei aplicado<span class="hx:absolute hx:-mt-20" id="hardening-o-que-já-deixei-aplicado"></span>
    <a href="#hardening-o-que-j%c3%a1-deixei-aplicado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui é onde aparece a diferença entre &ldquo;rodou no meu notebook&rdquo; e &ldquo;eu confiaria nisso pra operar minha carteira&rdquo;.</p>
<p>No estado atual da stack, os pontos que eu considero importantes ficaram assim:</p>
<ul>
<li>o RPC do Bitcoin Core não depende mais de exposição desnecessária no host; o Fulcrum fala com o <code>bitcoind</code> pela rede interna do Docker, que é o que interessa de verdade</li>
<li><code>50001</code> fica restrita ao uso interno da LAN</li>
<li><code>50002</code> fica disponível com TLS, que é a forma certa quando você precisa sair do plaintext</li>
<li>o shutdown é gracioso, com <code>stop_grace_period: 5m</code>, pra o <code>bitcoind</code> ter tempo de flushar estado em vez de morrer de qualquer jeito</li>
<li>a montagem do storage não fica na base do &ldquo;depois eu vejo&rdquo;; existe checagem de mount antes do Docker subir, justamente pra evitar drift silencioso</li>
</ul>
<p>Cada um desses itens existe por um motivo bem concreto.</p>
<p>Tirar o RPC da superfície do host reduz ataque sem custo nenhum. O Fulcrum já está no mesmo Compose e já consegue falar com o serviço pelo nome interno. Não existe ganho real em deixar essa porta aparecendo onde ela não precisa aparecer.</p>
<p>Separar <code>50001</code> e <code>50002</code> também ajuda a manter a casa em ordem. Dentro de LAN controlada, plaintext é aceitável. Fora disso, o mínimo razoável é TLS. Misturar os dois cenários costuma virar bagunça.</p>
<p>O <code>stop_grace_period: 5m</code> parece detalhe de container, mas não é. Quem já teve banco de dados, índice ou node de blockchain encerrado no tapa sabe o quanto isso vira horas de trabalho depois. Serviço stateful precisa de parada decente.</p>
<p>E a checagem de mount é uma daquelas coisas chatas que salvam você de si mesmo. O YAML pode estar lindo. Se o storage não montou e o serviço subiu gravando onde não devia, você acabou de fabricar um problema bem irritante.</p>
<p>Tem também um ponto que eu gosto bastante nesta versão final do stack: o Fulcrum autentica no <code>bitcoind</code> via arquivo <code>.cookie</code>, não via senha fixa em texto puro. Isso é interessante por dois motivos:</p>
<ul>
<li>você não precisa deixar credencial estática aparecendo em compose, inspect, healthcheck ou documentação</li>
<li>a autenticação fica mais alinhada com o próprio jeito que o Bitcoin Core já sabe operar localmente</li>
</ul>
<p>Em termos práticos, isso reduz vazamento acidental de segredo operacional. Não é uma solução mágica pra tudo, mas é muito melhor do que espalhar <code>rpcuser</code> e <code>rpcpassword</code> em arquivo, log e comando.</p>
<p>O único tipo de hardening que eu tento evitar aqui é o hardening performático demais no YAML e frouxo demais na operação. Eu prefiro menos &ldquo;engenharia de palco&rdquo; e mais disciplina básica:</p>
<ul>
<li>rede mínima</li>
<li>segredo mínimo</li>
<li>privilégio mínimo</li>
<li>shutdown limpo</li>
<li>storage verificado</li>
<li>subvolume separado pra dado grande e reconstruível, como o índice do Fulcrum</li>
</ul>
<p>E, de novo, documente tudo. Infra boa não é a que só funciona hoje. É a que continua funcionando quando você volta nela seis meses depois.</p>
<h2>Por que isso melhora transações do seu lado<span class="hx:absolute hx:-mt-20" id="por-que-isso-melhora-transações-do-seu-lado"></span>
    <a href="#por-que-isso-melhora-transa%c3%a7%c3%b5es-do-seu-lado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando eu monto uma transação na Sparrow e assino na Coldcard, a cadeia de confiança fica muito melhor definida:</p>
<ul>
<li>a chave privada não toca internet</li>
<li>a wallet desktop não precisa confiar em servidor público</li>
<li>o broadcast pode sair do meu próprio node</li>
<li>o histórico de endereços não precisa ir parar em Electrum server de terceiro</li>
</ul>
<p>Isso não torna nada invulnerável. Ainda existe risco de malware no desktop, seed mal armazenada, erro humano, engenharia social e desastre físico. Mas o desenho fica bem mais coerente.</p>
<h2>E o Lightning? Especialmente no Brasil?<span class="hx:absolute hx:-mt-20" id="e-o-lightning-especialmente-no-brasil"></span>
    <a href="#e-o-lightning-especialmente-no-brasil" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aí eu separo as coisas.</p>
<p>Reserva e transação de valor maior eu trato de um jeito. Gasto cotidiano eu trato de outro.</p>
<p>Pra gasto cotidiano, especialmente no Brasil, eu acho burrice operacional carregar muito saldo em wallet quente. Wallet de Lightning e app de gasto têm que ser quase &ldquo;carteira de bolso&rdquo;: só o suficiente pro dia a dia.</p>
<p>Isso vale em dobro se você usar solução híbrida ou custodial, como <a href="https://www.redotpay.com/"target="_blank" rel="noopener">RedotPay</a>. Eu entendo por que ela é interessante pra brasileiro: empresa de Hong Kong, foco internacional, bridge razoavelmente prática entre cripto e gasto com cartão. Pra viagens, compra online e vida fora do eixo bancário brasileiro, faz sentido. Mas eu jamais trataria isso como lugar de guardar patrimônio. Isso é ferramenta de spending, não cofre.</p>
<p>Mesma lógica pro <a href="https://www.bitrefill.com/br/pt/"target="_blank" rel="noopener">Bitrefill Brasil</a>. Eu acho o serviço interessante justamente porque ele resolve uma dor real no Brasil: transformar sats em utilidade concreta sem vender toda posição nem depender de integração bancária o tempo todo. Gift card, recarga, pequenas despesas. Como ferramenta de uso, faz bastante sentido.</p>
<p>Pra wallet Lightning no celular, eu olharia primeiro para:</p>
<ul>
<li><a href="https://phoenix.acinq.co/"target="_blank" rel="noopener">Phoenix</a> pra quem quer algo muito bom e simples</li>
<li><a href="https://breez.technology/"target="_blank" rel="noopener">Breez</a> pra quem quer experiência boa de pagamentos</li>
<li><a href="https://zeusln.com/"target="_blank" rel="noopener">ZEUS</a> se você é mais técnico e eventualmente pretende operar seu próprio node Lightning</li>
</ul>
<p>Todas elas, na minha cabeça, entram na categoria &ldquo;carteira de bolso&rdquo;. Saldo pequeno. Uso diário. Nada de transformar app de celular em cofre de aposentadoria.</p>
<h2>Notícias recentes que reforçam esse raciocínio<span class="hx:absolute hx:-mt-20" id="notícias-recentes-que-reforçam-esse-raciocínio"></span>
    <a href="#not%c3%adcias-recentes-que-refor%c3%a7am-esse-racioc%c3%adnio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu não monto esse tipo de stack porque acho bonito. Eu monto porque terceirização demais dá ruim com frequência demais.</p>
<p>Dois exemplos recentes:</p>
<ul>
<li>o <a href="https://www.cnbc.com/2025/02/21/hackers-steal-1point5-billion-from-exchange-bybit-biggest-crypto-heist.html"target="_blank" rel="noopener">hack da Bybit em 2025</a> mostrou, de novo, o risco básico de deixar custódia relevante em exchange</li>
<li>o <a href="https://techcrunch.com/2025/05/15/coinbase-says-customers-personal-information-stolen-in-data-breach/"target="_blank" rel="noopener">vazamento de dados de clientes da Coinbase em 2025</a> mostrou o outro lado do problema: mesmo quando a custódia não é o foco imediato, sua identidade, saldo e histórico viram superfície de ataque</li>
</ul>
<p>Uma stack como Coldcard + Sparrow + Fulcrum + full node não elimina todo risco do mundo. Mas evita duas classes de problema bem reais:</p>
<ul>
<li>perder soberania de custódia</li>
<li>entregar privacidade de carteira e transação de bandeja pra terceiros</li>
</ul>
<h2>Então vale a pena?<span class="hx:absolute hx:-mt-20" id="então-vale-a-pena"></span>
    <a href="#ent%c3%a3o-vale-a-pena" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra maioria das pessoas, honestamente, provavelmente não no primeiro mês. É trabalhoso, tem curva de aprendizado, e exige disciplina.</p>
<p>Mas pra programador, engenheiro e qualquer pessoa técnica que quer aprender a não depender sempre de serviço alheio, eu acho um exercício excelente.</p>
<p>Você aprende sobre:</p>
<ul>
<li>separação de responsabilidades</li>
<li>persistência e estado</li>
<li>shutdown gracioso</li>
<li>observabilidade</li>
<li>isolamento de segredo</li>
<li>trade-off entre conveniência e segurança</li>
</ul>
<p>E tudo isso vale além de Bitcoin.</p>
<p>No fim das contas, é isso que mais me interessa nesse stack. Não é sair pregando &ldquo;hyperbitcoinization&rdquo; nem posar de profeta de preço. É montar um sistema em casa em que eu consigo confiar mais porque fui eu que instalei, medi, quebrei, consertei e documentei.</p>
<p>Dá trabalho? Dá.</p>
<p>Mas esse tipo de trabalho ensina exatamente o que software moderno tenta fazer você esquecer: depender menos dos outros dá mais trabalho no começo, mas costuma comprar muito mais controle no longo prazo.</p>
]]></content:encoded><category>bitcoin</category><category>homeserver</category><category>self-hosting</category><category>privacy</category><category>security</category><category>lightning</category></item><item><title>Meu Cockpit de Sim Racing - Formula FX1</title><link>https://www.akitaonrails.com/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/</guid><pubDate>Wed, 01 Apr 2026 17:00:00 GMT</pubDate><description>&lt;p&gt;Eu gosto de carros desde que me entendo por gente. Meu primeiro contato real com jogos de corrida foi nos fliperamas dos anos 80 e 90. E quando eu digo &amp;ldquo;real&amp;rdquo;, eu digo sentar num cabinet com volante, pedal, banco de plástico duro e a tela curvando na sua frente. &lt;a href="https://en.wikipedia.org/wiki/Out_Run"target="_blank" rel="noopener"&gt;OutRun&lt;/a&gt; (1986), &lt;a href="https://en.wikipedia.org/wiki/Rad_Mobile"target="_blank" rel="noopener"&gt;Rad Mobile&lt;/a&gt; (1991), &lt;a href="https://en.wikipedia.org/wiki/Virtua_Racing"target="_blank" rel="noopener"&gt;Virtua Racing&lt;/a&gt; (1992), &lt;a href="https://en.wikipedia.org/wiki/Ridge_Racer"target="_blank" rel="noopener"&gt;Ridge Racer&lt;/a&gt; (1993), &lt;a href="https://en.wikipedia.org/wiki/Daytona_USA_%28video_game%29"target="_blank" rel="noopener"&gt;Daytona USA&lt;/a&gt; (1994), &lt;a href="https://en.wikipedia.org/wiki/Scud_Race"target="_blank" rel="noopener"&gt;Scud Race&lt;/a&gt; (1996). Cada um desses jogos me marcou. Mas o Daytona USA ficou gravado de outro jeito. Aquele cabinet twin, duas máquinas lado a lado, a música &amp;ldquo;DAYTONAAA, let&amp;rsquo;s go away&amp;rdquo; estourando no fliperama, o volante vibrando na mão. Eu lembro disso até hoje.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu gosto de carros desde que me entendo por gente. Meu primeiro contato real com jogos de corrida foi nos fliperamas dos anos 80 e 90. E quando eu digo &ldquo;real&rdquo;, eu digo sentar num cabinet com volante, pedal, banco de plástico duro e a tela curvando na sua frente. <a href="https://en.wikipedia.org/wiki/Out_Run"target="_blank" rel="noopener">OutRun</a> (1986), <a href="https://en.wikipedia.org/wiki/Rad_Mobile"target="_blank" rel="noopener">Rad Mobile</a> (1991), <a href="https://en.wikipedia.org/wiki/Virtua_Racing"target="_blank" rel="noopener">Virtua Racing</a> (1992), <a href="https://en.wikipedia.org/wiki/Ridge_Racer"target="_blank" rel="noopener">Ridge Racer</a> (1993), <a href="https://en.wikipedia.org/wiki/Daytona_USA_%28video_game%29"target="_blank" rel="noopener">Daytona USA</a> (1994), <a href="https://en.wikipedia.org/wiki/Scud_Race"target="_blank" rel="noopener">Scud Race</a> (1996). Cada um desses jogos me marcou. Mas o Daytona USA ficou gravado de outro jeito. Aquele cabinet twin, duas máquinas lado a lado, a música &ldquo;DAYTONAAA, let&rsquo;s go away&rdquo; estourando no fliperama, o volante vibrando na mão. Eu lembro disso até hoje.</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/2/24/DaytonaUSA_arcade_SaoPaulo.jpg" alt="Máquinas de Daytona USA num shopping de São Paulo — exatamente o tipo de cabinet twin que ficou gravado na minha memória"  loading="lazy" /></p>
<p>Mas o jogo que realmente me viciou no gênero simcade foi o <a href="https://en.wikipedia.org/wiki/Gran_Turismo_%28video_game%29"target="_blank" rel="noopener">Gran Turismo</a> original, em 1997, no PlayStation 1. &ldquo;The Real Driving Simulator&rdquo; na capa. Eu joguei aquele jogo obsessivamente. Na mesma época eu assistia o anime <a href="https://en.wikipedia.org/wiki/Initial_D"target="_blank" rel="noopener">Initial D</a>, que estreou em 1998 no Japão. Comprei todos os mangás e li tudo do começo ao fim. A história do Takumi Fujiwara descendo o Monte Akina de madrugada entregando tofu no AE86 do pai dele é, pra mim, uma das melhores histórias de automobilismo já contadas em qualquer mídia.</p>
<p>Até hoje eu acompanho o trabalho do Shuichi Shigeno. Depois de Initial D veio <a href="https://kodansha.us/series/mf-ghost/"target="_blank" rel="noopener">MF Ghost</a> (2017-2025), que se passa no mesmo universo mas num futuro próximo onde carros a combustão viraram peça de museu. E agora, desde julho de 2025, estou lendo <a href="https://kmanga.kodansha.com/title/10664/episode/360584"target="_blank" rel="noopener">Subaru and Subaru</a>, a continuação direta que une os universos de Initial D e MF Ghost com duas protagonistas chamadas Subaru — uma de Gunma, outra de Kanagawa — competindo numa nova série de corridas. É o Shigeno no melhor dele.</p>
<p><a href="https://kmanga.kodansha.com/title/10664/episode/360584"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/subaru-x-subaru-cover.png" alt="Subaru and Subaru — o novo mangá do Shigeno, continuação direta de Initial D e MF Ghost"  loading="lazy" /></a></p>
<p>Dois anos atrás eu viajei pro Japão com minha namorada e fiz questão de ir na <a href="https://en.wikipedia.org/wiki/Daikoku_Parking_Area"target="_blank" rel="noopener">Daikoku PA</a>, o famoso parking area na Shuto Expressway em Yokohama onde a cultura JDM se concentra. Como fã antigo de <a href="https://store.steampowered.com/app/2634950/Tokyo_Xtreme_Racer/"target="_blank" rel="noopener">Tokyo Xtreme Racer</a>, da Genki, eu precisava ver Daikoku com meus próprios olhos pelo menos uma vez. E não decepcionou. Em vez de alugar carro, a gente fechou um tour com um guia local no Nissan GT-R preparado dele. Melhor assim. No caminho ele foi explicando a história da Wangan, como a cena funciona, o que é exagero de YouTube e o que é real. Quando chegamos numa sexta à noite e eu vi aquilo tudo ao vivo — Skyline R34, RX-7, Supra, GT-R, kei trucks tunados, bosozoku insanos — a sensação foi estranha no melhor sentido. Parecia Tokyo Xtreme Racer, só que com cheiro de combustível no ar e barulho de escapamento de verdade.</p>
<p>E tem outro detalhe: eu estou jogando o reboot novo de <a href="https://store.steampowered.com/app/2634950/Tokyo_Xtreme_Racer/"target="_blank" rel="noopener">Tokyo Xtreme Racer</a> no PC, e é exatamente o tipo de jogo que entende o próprio público. Campanha single-player forte, progressão viciante, clima certo, e sem aquelas palhaçadas de loot box. Recomendo fácil. Pelo mesmo motivo, eu também estou esperando muito o <a href="https://forza.net/forzahorizon6"target="_blank" rel="noopener">Forza Horizon 6</a>, que dessa vez vai se passar no Japão. Eu já fiz a pré-compra e estou louco pra jogar isso no cockpit novo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/daikoku---nissan.jpg" alt="Na Daikoku PA com um GT-R modificado"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/daikoku---trueno.jpg" alt="Com um AE86 Trueno na Daikoku PA — o carro do Takumi"  loading="lazy" /></p>
<h2>Dirigindo de verdade<span class="hx:absolute hx:-mt-20" id="dirigindo-de-verdade"></span>
    <a href="#dirigindo-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora que estou semi-aposentado, tive a oportunidade de levar meu Mercedes em track days. Já andei no <a href="https://en.wikipedia.org/wiki/Aut%C3%B3dromo_Jos%C3%A9_Carlos_Pace"target="_blank" rel="noopener">Autódromo de Interlagos</a> (o Autódromo José Carlos Pace), circuito de 4.309 km em São Paulo que sedia o GP de F1 do Brasil desde 1973, famoso pelas curvas do S do Senna e pelo desnível maluco do circuito. Também andei no <a href="https://www.velocittapark.com.br/"target="_blank" rel="noopener">Autódromo Velocitta</a>, um circuito moderno de 3.443 km inaugurado em 2014 em Mogi Guaçu, interior de São Paulo, que recebe a Stock Car Brasil e a Porsche Cup.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/velocitta.jpg" alt="No Velocitta com meu Mercedes num track day da AMG"  loading="lazy" /></p>
<p>Em Las Vegas eu já dirigi super carros naquelas experiências de pista. E quando viajei com minha namorada pra Gramado, no RS, fomos na <a href="https://supercarros.cc/"target="_blank" rel="noopener">Super Carros</a>, que fica na Av. das Hortênsias 4635. Eles têm um galpão de 2.400 m² com mais de 50 carros — Ferraris, Lamborghinis, Porsches, GT-Rs, Corvettes, muscle cars americanos. Você escolhe o carro, sai com um instrutor, e dirige um percurso de uns 17 km entre Gramado e Canela. Eu peguei um Nissan GT-R e uma Ferrari California.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/gramado---gtr.jpg" alt="Com um GT-R em Gramado"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/gramado---ferrari.jpg" alt="Com uma Ferrari California em Gramado"  loading="lazy" /></p>
<p>Três anos atrás eu também fui pra Abu Dhabi com minha namorada e fomos no Ferrari World, que tem alguns dos melhores simuladores de corrida que eu já experimentei. Plataforma hidráulica com 6 graus de liberdade, cockpit de F1, a coisa toda. Eu sempre adorei testar simuladores onde quer que eu vá.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/ferrari-park.jpg" alt="Simulador de F1 com plataforma hidráulica no Ferrari World em Abu Dhabi"  loading="lazy" /></p>
<p>Mas dirigir carros de verdade em pistas de verdade é um hobby muito caro. Pneus, combustível, seguro, manutenção, inscrição. E o mais importante: eu sou introvertido. Prefiro estar sozinho. Meu cockpit de simulador é perfeito pra quando eu quero dirigir sem ter que lidar com ninguém. É por isso que eu gosto tanto de rally — sou eu, o co-driver virtual, e a estrada. Nada mais.</p>
<h2>Os jogos que eu jogo<span class="hx:absolute hx:-mt-20" id="os-jogos-que-eu-jogo"></span>
    <a href="#os-jogos-que-eu-jogo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sei que a maioria das pessoas que montam um cockpit como esse fazem pra jogar simuladores sérios — iRacing, Assetto Corsa Competizione, Automobilista 2. Eu respeito, mas não é minha praia. Não gosto de jogar online com outras pessoas. Não tenho nenhuma intenção de começar uma carreira de live streaming. Isso é puramente pro meu divertimento.</p>
<p>Atualmente eu jogo Gran Turismo 7 no PS5, <a href="https://store.steampowered.com/app/2440510/Forza_Motorsport/"target="_blank" rel="noopener">Forza Motorsport</a> (o 8, de 2023) no PC, mas onde eu me divirto mais é nos jogos de rally: <a href="https://store.steampowered.com/app/1849250/EA_SPORTS_WRC/"target="_blank" rel="noopener">EA SPORTS WRC</a>, <a href="https://store.steampowered.com/app/1462810/WRC_10_FIA_World_Rally_Championship/"target="_blank" rel="noopener">WRC 10</a> e <a href="https://store.steampowered.com/app/690790/DiRT_Rally_20/"target="_blank" rel="noopener">DiRT Rally 2.0</a>. Minha primeira experiência com Forza foi no Xbox One com o Forza Motorsport 5 e depois Forza Horizon 4, que me prendeu por centenas de horas.</p>
<p>E eu tenho um carinho enorme por jogos retro. O Colin McRae Rally original de 1998 no PS1 foi meu primeiro jogo de rally. Mas o meu favorito de todos os tempos é o Colin McRae Rally 2.0 (2000), também no PS1. Recentemente eu completei a campanha inteira de novo na versão de PC — dá pra achar repacks que rodam em alta resolução e widescreen, muito melhor que as versões originais de PlayStation. Recomendo pra qualquer um dos títulos da série.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/colin-mcrae-rally-2-gameplay.jpg" alt="Colin McRae Rally 2.0 rodando em widescreen no PC — neve na Suécia com o Ford Focus"  loading="lazy" /></p>
<p>Depois vieram Colin McRae 3 (2002), Colin McRae Rally 04 (2003) e Colin McRae 2005 (2004). Outros arcades que eu revisito com frequência: OutRun 2 SP (2004) e OutRun 2006: Coast 2 Coast (2006) — o melhor OutRun já feito, na minha opinião.</p>
<p>Mas meu jogo do ano, disparado, é o <a href="https://store.steampowered.com/app/3218630/Super_Woden_Rally_Edge/"target="_blank" rel="noopener">Super Woden: Rally Edge</a>. Um indie feito por um desenvolvedor solo (ViJuDa, da Espanha) que lançou em janeiro de 2026 por menos de R$ 60. Oito países, mais de 80 carros, modo carreira, multiplayer local split-screen pra até 4 jogadores, leaderboards online. A câmera atrás do carro em vez do top-down dos Super Woden GP anteriores fez toda a diferença. 96% de reviews positivos no Steam com mais de 1.300 avaliações. É o tipo de jogo que mostra que você não precisa de orçamento milionário pra fazer algo incrível.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/super-woden-rally-edge.jpg" alt="Super Woden: Rally Edge — indie feito por um dev solo que compete com os grandes"  loading="lazy" /></p>
<h2>A evolução do meu setup de volante<span class="hx:absolute hx:-mt-20" id="a-evolução-do-meu-setup-de-volante"></span>
    <a href="#a-evolu%c3%a7%c3%a3o-do-meu-setup-de-volante" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Era Logitech G29 (~2015-2021)<span class="hx:absolute hx:-mt-20" id="era-logitech-g29-2015-2021"></span>
    <a href="#era-logitech-g29-2015-2021" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu sempre quis um volante de corrida. Comecei há mais de 15 anos com algo equivalente ao volante de entrada da Logitech, um G29. O G29 é um bom volante pra começar — force feedback por engrenagens, pedais com embreagem, 900 graus de rotação. Mas o force feedback dele é barulhento e meio grosseiro. Você sente as engrenagens girando.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/logitech-g29-and-myself.jpeg" alt="Eu jogando com o Logitech G29 no sofá — o começo de tudo"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/logitech-g29-with-support.jpg" alt="O G29 com suporte simples na frente do sofá"  loading="lazy" /></p>
<h3>Era Thrustmaster T300RS + Suporte SXT V2 (~2021-2024)<span class="hx:absolute hx:-mt-20" id="era-thrustmaster-t300rs--suporte-sxt-v2-2021-2024"></span>
    <a href="#era-thrustmaster-t300rs--suporte-sxt-v2-2021-2024" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Por volta de 2021 eu fiz o upgrade pro <a href="https://www.thrustmaster.com/products/t300rs-gt-edition/"target="_blank" rel="noopener">Thrustmaster T300RS</a>, um volante por correia que é um salto enorme em relação ao Logitech. O force feedback é muito mais suave e preciso. E comprei o suporte <a href="https://loja.cockpitextremeracing.com.br/products/suporte-para-volantes-sxt-v2?variant=51111119454489"target="_blank" rel="noopener">Extreme Sim Racing SXT V2</a>, que é bem mais robusto que aqueles suportes de mesa genéricos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-support.jpg" alt="O Thrustmaster T300RS montado no suporte SXT V2"  loading="lazy" /></p>
<p>Primeiro eu montei o setup na frente do meu PC desktop, que na época tinha uma RTX 3090. Funcionava bem, mas era muito trabalhoso ficar montando e desmontando o suporte e os cabos toda vez que eu queria jogar.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-pc.jpg" alt="O Thrustmaster na frente do PC desktop — funcional mas trabalhoso"  loading="lazy" /></p>
<p>Depois eu fiz um setup com cabo HDMI de fibra óptica longo pra conectar minha TV de 60&quot; ao PC nos fundos da sala. Movi o suporte pra frente do sofá. Menos trabalhoso, mas eu ainda tinha que desmontar quando queria assistir filme com minha namorada.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-big-tv.jpg" alt="Setup com a TV grande — melhor, mas ainda trabalhoso"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-in-the-living-room.jpg" alt="O suporte na sala de estar — sempre no caminho"  loading="lazy" /></p>
<p>Por volta de 2024 ou 2025 eu troquei meu sofá por um daqueles sofás VIP de cinema da <a href="https://www.starseat.com.br/sofa-cinema-sofa-cinema"target="_blank" rel="noopener">Star Seat</a>, que reclina e tudo. Problema: era muito mais alto que o sofá anterior. Tive que fazer todo tipo de gambiarra pra fazer o suporte funcionar naquela altura. Cheguei a imprimir suportes em 3D e mandar pra PCBWay usinar placas de aço pra prender rodas grandes embaixo do suporte e ganhar uns centímetros de altura. Mas isso deixou o setup muito instável pra dirigir confortavelmente.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-support-sofa-too-high.jpg" alt="O problema: sofá VIP alto demais pro suporte — gambiarra total"  loading="lazy" /></p>
<h3>Era Fanatec CSL DD + Direct Drive (~2024-2025)<span class="hx:absolute hx:-mt-20" id="era-fanatec-csl-dd--direct-drive-2024-2025"></span>
    <a href="#era-fanatec-csl-dd--direct-drive-2024-2025" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nesse meio tempo eu dei o T300 pro meu irmão e fiz o upgrade pra um <a href="https://fanatec.com/eu-en/racing-wheels-wheel-bases/racing-wheels/gran-turismo-dd-pro-5-nm-wheel-base"target="_blank" rel="noopener">Fanatec CSL DD Gran Turismo Edition</a>. O motor direct drive do CSL DD entrega 5 Nm de torque na base, mas o kit Gran Turismo DD Pro já vem com o Boost Kit 180 que sobe pra 8 Nm sustentados sem cooling ativo. Direct drive significa que não tem engrenagem nem correia entre o motor e o volante — o eixo do motor É o eixo do volante. A diferença é absurda. O T300 já era ótimo, muito superior ao Logitech. Mas o Fanatec está em outro patamar. Você sente cada textura do asfalto, cada lombada, cada derrapagem incipiente. Não tem como voltar atrás depois que experimenta.</p>
<p>Comprei junto o kit de <a href="https://fanatec.com/eu-en/pedals/csl-pedals"target="_blank" rel="noopener">pedais CSL com Load Cell</a>, que mede pressão no freio em vez de deslocamento. Faz toda a diferença na frenagem — você aprende a dosar pela força do pé, não pela distância que o pedal percorre. Muito mais natural.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-direct-drive-close-up.jpg" alt="Close-up do motor Fanatec CSL DD direct drive"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-csl-pedals.jpg" alt="Pedais Fanatec CSL com Load Cell Kit"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec---shifter.jpg" alt="O shifter Fanatec — câmbio manual com knob cromado"  loading="lazy" /></p>
<p>Eu também quis experimentar o câmbio manual H-pattern com o <a href="https://fanatec.com/us/en/p/add-ons/crd-9040002-ww/clubsport-shifter-sq"target="_blank" rel="noopener">ClubSport Shifter SQ V1.5</a> da Fanatec e o freio de mão separado. É legal pra experimentar carros antigos com embreagem e câmbio H, mas na prática eu não me adaptei. O suporte SXT V2 já balançava bastante com o direct drive, e usar o shifter naquele setup instável era frustrante. E eu sei que tem gente que quer fazer &ldquo;ponta e calcanhar&rdquo; (heel-toe), mas no simulador eu prefiro ter o pé esquerdo no freio e o direito no acelerador e dosar os dois ao mesmo tempo. Funciona melhor pra mim. Agora que eu tenho o volante McLaren com os paddles analógicos de freio de mão e embreagem direto no volante, o câmbio H e o handbrake externo ficaram aposentados. Pra rally, o handbrake analógico no volante é muito mais natural.</p>
<p>Também comprei o PS5 com Gran Turismo 7 nessa época. Coloquei as <a href="https://dbrand.com/shop/ps5"target="_blank" rel="noopener">dbrand Darkplates</a> matte black pra trocar as placas brancas originais — fica muito mais bonito e discreto.</p>
<p>Mas o setup ainda era o suporte SXT V2 na frente do sofá VIP. A mesma gambiarra. O mesmo balanço. Eu não ia desistir do sofá de cinema, claro. A situação ficou insustentável.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-with-support.jpg" alt="O Fanatec CSL DD no suporte SXT V2 — poderoso demais pro suporte aguentar"  loading="lazy" /></p>
<h2>O computador e o setup de hardware<span class="hx:absolute hx:-mt-20" id="o-computador-e-o-setup-de-hardware"></span>
    <a href="#o-computador-e-o-setup-de-hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma nota sobre meu hardware de gaming. Eu comprei um <a href="https://www.minisforum.com/products/UX790-Pro.html"target="_blank" rel="noopener">Minisforum UX790 Pro</a> pra ser minha máquina dedicada de Steam. É um mini-PC com processador Intel Core Ultra 9 285H, que cabe na palma da mão. Junto comprei o <a href="https://www.minisforum.com/products/minisforum-deg1-egpu-dock"target="_blank" rel="noopener">Minisforum DEG1</a>, um dock de GPU externo que conecta via OCuLink (PCIe 4.0 x4, 64 GT/s). É um design aberto — basicamente uma placa com slot PCIe x16 e espaço pra fonte ATX ou SFX. Não tem limitação de tamanho de placa, então cabe uma RTX 4090 tranquilamente. A perda de performance comparado a um slot PCIe nativo é mínima. Coloquei a RTX 4090 nele. A 4090 veio do meu desktop — no começo de 2025 eu viajei pra Miami e aproveitei pra comprar uma RTX 5090 porque estava usando cada vez mais IA e LLMs locais. Dei a 3090 antiga pra minha namorada usar na edição de vídeo. A 4090 foi pro mini-PC.</p>
<p>Então meu setup de gaming hoje é: Minisforum UX790 Pro + eGPU com RTX 4090 pra Steam e jogos de PC, e PlayStation 5 com Darkplates matte black pra Gran Turismo 7 e exclusivos.</p>
<h2>O cockpit: Formula FX1<span class="hx:absolute hx:-mt-20" id="o-cockpit-formula-fx1"></span>
    <a href="#o-cockpit-formula-fx1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra completar o setup eu também precisava de um monitor decente. Eu já estava acostumado com minha TV OLED Samsung de 80&quot; na sala e não queria dar um downgrade na qualidade de imagem. Então investi no <a href="https://www.samsung.com/br/monitors/gaming/odyssey-oled-g8-g81sf-32-inch-240hz-oled-uhd-ls32fg810snxzd/"target="_blank" rel="noopener">Samsung Odyssey OLED G8 de 32&quot;</a>. É um monitor 4K (3840x2160) OLED com 240 Hz de refresh rate, 0.03 ms de tempo de resposta (GTG), HDR True Black 400, HDR10+, 99% de cobertura DCI-P3, contraste de 1.000.000:1, e FreeSync Premium Pro. Tem 2 entradas HDMI 2.1 e 1 DisplayPort 1.4.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/closed-delivery-box.jpg" alt="A caixa do Samsung Odyssey OLED G8 de 32&#34; quando chegou"  loading="lazy" /></p>
<p>Na prática: as cores explodem, preto é preto de verdade (é OLED, não tem backlight), e com a RTX 4090 eu rodo a maioria dos jogos em 4K a 120 fps tranquilo. Em títulos mais leves como o Super Woden: Rally Edge, chega fácil nos 240 Hz. A fluidez é absurda. Pra um cockpit onde você está a uns 60-70 cm da tela, 32&quot; OLED em 4K é o sweet spot. Maior que isso e você começaria a enxergar pixels. Menor e perde a imersão.</p>
<p>Em janeiro de 2026, depois de anos de gambiarra, eu finalmente encomendei um cockpit dedicado. Pesquisei bastante. Considerei o <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-ax160-horizontal?variant=52008751431961"target="_blank" rel="noopener">Cockpit AX160</a>, que é de perfil de alumínio e muito modular, e o <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-4-0-horizontal?variant=52004294852889"target="_blank" rel="noopener">Cockpit 4.0</a>, que é de aço tubular mais tradicional. Mas nenhum dos dois estava disponível no momento da compra. E aí eu encontrei o <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-formula-fx1-preto-e-verde?variant=51700876509465"target="_blank" rel="noopener">Formula FX1 preto e verde</a> da Extreme Racing — cores Petronas, estilo F1.</p>
<p>O FX1 é muito diferente dos cockpits tradicionais. A estrutura inteira é de tubos de aço grossos, soldados. Quando eu digo que a coisa não balança, eu quero dizer que não balança nada. Zero wobble. É uma diferença brutal em relação ao suporte na frente do sofá. A posição de pilotagem é reclinada, tipo F1 — seus pés ficam na mesma altura ou mais altos que seu quadril. Parece que vai ser desconfortável, mas não é. Dá pra ficar horas ali sem reclamar. Vem com banco acolchoado e ajustável, suporte pra monitor com articulação, suporte pra pedais com inclinação ajustável, e suporte pra volante com altura regulável.</p>
<p>Tive que esperar cerca de 1 mês pela entrega. No meio tempo, como quem acompanha meu blog sabe, eu mergulhei numa maratona de 16 horas por dia testando os novos agentes de IA da Anthropic e OpenAI — veja as tags <a href="/tags/vibe-coding/">#vibecoding</a> e <a href="/tags/ai/">#agents</a> pra ver tudo que eu fiz. Depois de uns 30 dias nessa maratona insana, minha lombar cedeu e eu comecei a desenvolver o que parece ser hérnia de disco. Tive que ir ao médico e tomar anti-inflamatórios pesados.</p>
<p>E exatamente nessa semana, o cockpit decidiu chegar.</p>
<h3>A montagem<span class="hx:absolute hx:-mt-20" id="a-montagem"></span>
    <a href="#a-montagem" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu estava com dor absurda, mas montei o cockpit assim mesmo. Levou o dia inteiro pra desembalar e montar as peças de aço pesadas com a lombar gritando, mas eu fiz.</p>
<p>O vídeo de montagem oficial que eu segui:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/WnocqqhmZas"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/first-pieces-mounted.jpg" alt="Primeiras peças montadas — a base e a estrutura do banco"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mostly-mounted.jpg" alt="Estrutura quase completa — banco, suporte do volante, pedais"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mostly-mounted-from-front.jpg" alt="Vista frontal da estrutura quase pronta"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/almost-mounted,-with-monitor.jpg" alt="Montagem quase completa com o suporte do monitor"  loading="lazy" /></p>
<h3>O volante McLaren GT3 V2<span class="hx:absolute hx:-mt-20" id="o-volante-mclaren-gt3-v2"></span>
    <a href="#o-volante-mclaren-gt3-v2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Depois de montar o cockpit, eu decidi que o volante padrão que vem com o kit CSL GT não era suficiente. Fiz o upgrade pro <a href="https://www.racingwheelbrasil.com.br/produtos/volante-fanatec-csl-elite-mclaren-gt3-v2-pc-xbox-ps4-ps5-ready/"target="_blank" rel="noopener">Fanatec CSL Elite McLaren GT3 V2</a> (~R$ 4.990). É uma réplica em escala real do volante do McLaren GT3, com fibra de carbono, display OLED, e compatível com PC, Xbox, PS4 e PS5.</p>
<p>O que eu mais gosto nele: tem as borboletas de câmbio normais atrás (shift up/down), mas também tem dois paddles analógicos adicionais que podem ser configurados em quatro modos diferentes. No modo B, que é o que eu uso, o paddle esquerdo funciona como handbrake analógico e o direito como embreagem analógica. Isso é perfeito pra rally — eu consigo puxar o freio de mão no meio da curva sem tirar a mão do volante. Tem também dois toggles de 2 posições, dois rotary de 12 posições, 7 botões padrão com caps intercambiáveis, e o FunkySwitch de 7 direções da Fanatec. É um controle completo de corrida.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mclaren-wheel.jpg" alt="O volante McLaren GT3 V2 montado no cockpit — fibra de carbono e display OLED"  loading="lazy" /></p>
<h2>O setup final<span class="hx:absolute hx:-mt-20" id="o-setup-final"></span>
    <a href="#o-setup-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O cockpit ficou num cantinho do meu quarto, entre as estantes de mangá (dá pra ver Akira, Initial D, e uns 500 outros volumes atrás). Montei o mini-PC e o PS5 na estrutura lateral do cockpit, junto com a eGPU e a RTX 4090. Tudo fica permanentemente conectado. É isso que faz a diferença: eu não preciso mais montar nem desmontar nada. Sento, ligo, e estou dirigindo em 30 segundos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fully-mounted-from-side.jpg" alt="Vista lateral do cockpit completo — entre as estantes de mangá"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/all-consoles-from-front.jpg" alt="Os consoles montados na estrutura: PS5 com Darkplates, Minisforum UX790 Pro, eGPU com RTX 4090"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/close-up-of-the-consoles-from-front.jpg" alt="Close-up dos consoles e cabeamento"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/sitting-angle.jpg" alt="A perspectiva do piloto — é assim que eu vejo quando estou sentado"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/monitor-on-view-from-front.jpg" alt="PS5 mostrando a lista de jogos — Gran Turismo 7 no topo"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/another-fully-mounted-shot.jpg" alt="O cockpit completo visto de trás"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/from-front-gran-turismo-h264.mp4" type="video/mp4">
  </video>
  <em>Gran Turismo 7 rodando no cockpit final</em>
</div>
<h2>O sistema de áudio<span class="hx:absolute hx:-mt-20" id="o-sistema-de-áudio"></span>
    <a href="#o-sistema-de-%c3%a1udio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra completar o setup eu precisava de áudio dedicado. Eu não queria usar o áudio do monitor (péssimo) nem ficar sempre de headset. A solução foi montar um sistema de áudio separado com extração de áudio do HDMI.</p>
<p>A peça central é um <a href="https://www.amazon.com.br/dp/B00MNGIP2Y"target="_blank" rel="noopener">switcher HDMI 2.1 da OREI</a> com extração de áudio. Ele tem 2 entradas e 1 saída HDMI, suporta 4K a 120Hz (48 Gbps de bandwidth), e extrai o áudio via saída óptica TOSLINK e P2 3.5mm. Eu conecto a saída HDMI da RTX 4090 numa entrada e o PS5 na outra. O vídeo vai pro monitor. O áudio sai pela óptica.</p>
<p>O áudio óptico vai pra um <a href="https://www.mercadolivre.com.br/amplificador-de-potncia-aiyima-d03-bluetooth-50-150-watts-cor-preto/p/MLB46172770"target="_blank" rel="noopener">amplificador Aiyima D03</a>, um amp compacto 2.1 canais com 150W por canal, DAC integrado (chip PCM1808), e Bluetooth 5.0 com aptX HD. Ele tem entrada óptica, coaxial, USB, RCA e Bluetooth. Tem até saída dedicada pra subwoofer quando eu resolver adicionar um. Usa chip amplificador TAS5624 da Texas Instruments e tem controle de graves e agudos pelo controle remoto. Pra um setup de cockpit onde você está a 1 metro das caixas, 150W é mais que suficiente.</p>
<p>Na prática, eu deixo o amplificador em 50% e o volume do Windows em 50%, e isso já fica alto pra caramba. Ou seja: não é um sisteminha &ldquo;quebra-galho&rdquo;. Está equipado pra tocar realmente alto se eu quiser.</p>
<p>As caixas são <a href="https://edifier.com.br/caixa-de-som-passiva-p12-madeira-edifier.html"target="_blank" rel="noopener">Edifier P12</a>, passivas, com woofer de 4 polegadas e tweeter de 19mm. Resposta de frequência de 55Hz a 20kHz, impedância de 6 ohms, potência de 20W RMS cada. O gabinete de MDF com acabamento em madeira tem um porte-reflex traseiro que ajuda nos graves. Pra caixas passivas desse tamanho, elas entregam bem. O mid-range sai limpo e os agudos não distorcem mesmo no volume alto.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/audio-equipment.jpg" alt="Os equipamentos de áudio — caixa do Aiyima D03 e do Edifier P12"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/audio-speakers-next-to-other-stuff.jpg" alt="As Edifier P12 posicionadas nas estantes ao lado do cockpit, junto com a coleção de Akira e miniaturas de carros"  loading="lazy" /></p>
<p>A lógica do setup é: HDMI switcher faz o chaveamento entre PS5 e PC, extrai o áudio pra óptica, o amp converte e amplifica, e as caixas passivas entregam o som. Tudo sem precisar tocar no monitor ou trocar cabos. Aperto um botão no switcher e troco de console.</p>
<p>Quando eu quero jogar sem incomodar ninguém, conecto meu <a href="https://www.mercadolivre.com.br/fones-de-ouvido-meze-audio-109-pro-com-fio-de-madeira-com-en/p/MLB42456685"target="_blank" rel="noopener">Meze 109 Pro</a> direto na saída P2 do switcher HDMI. O Meze 109 Pro é um headphone aberto com drivers dinâmicos de 50mm, impedância de 40 ohms, sensibilidade de 112 dB SPL/1mW, e resposta de 5Hz a 30kHz. Os ear cups são de madeira de nogueira com acabamento artesanal. É um headphone audiófilo que funciona perfeitamente sem amplificador dedicado graças à impedância baixa. O som é quente, com graves cheios e mids ricos. Dá pra ouvir cada detalhe do ronco dos motores.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/meze-headset.jpg" alt="Meze 109 Pro — madeira de nogueira, som audiófilo"  loading="lazy" /></p>
<p>Ainda não decidi sobre subwoofer, mas vai ser meu próximo upgrade. Um sub dedicado vai adicionar aquele peso nos graves que faz você sentir o motor no peito.</p>
<h2>O veredito<span class="hx:absolute hx:-mt-20" id="o-veredito"></span>
    <a href="#o-veredito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sofa com suporte funciona. A mesa do PC com suporte funciona. Mas nenhum dos dois chega perto de um cockpit dedicado. A estrutura de aço do FX1 não mexe nem um milímetro, mesmo com o direct drive do Fanatec dando torque máximo. A posição F1 reclinada é confortável pra sessões de horas. Os pedais com load cell ficam firmes na base. O monitor fica exatamente na altura e distância certas. E o melhor de tudo: está sempre pronto. Eu não preciso montar, desmontar, passar cabo, nada. Sento e dirijo.</p>
<p>Pra quem está na dúvida se vale a pena investir num cockpit dedicado vs. ficar com suporte na mesa ou no sofá: vale. Se você já tem um volante direct drive, o cockpit é a peça que falta. Eu passei anos achando que &ldquo;tá bom assim&rdquo; com o suporte no sofá. Não tava. A diferença na dirigibilidade é outra coisa. E pro meu caso — introvertido, single-player only, simcade — não podia ter montado antes. Pra falar a verdade, eu acho que finalmente cheguei no meu setup de simulador perfeito pro meu gosto.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/monitor-on-view.jpg" alt="O monitor ligado com a vista do piloto"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/video-testing-h264.mp4" type="video/mp4">
  </video>
  <em>Eu dirigindo no cockpit final</em>
</div>
<h2>Lista de compras: quanto custou tudo<span class="hx:absolute hx:-mt-20" id="lista-de-compras-quanto-custou-tudo"></span>
    <a href="#lista-de-compras-quanto-custou-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui está a lista consolidada de tudo que compõe meu setup atual, com preços aproximados (alguns comprei em dólar, converti pra real na cotação da época):</p>
<table>
  <thead>
      <tr>
          <th>Item</th>
          <th style="text-align: right">Preço Estimado (R$)</th>
          <th>Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://loja.cockpitextremeracing.com.br/products/cockpit-formula-fx1-preto-e-verde?variant=51700876509465"target="_blank" rel="noopener">Cockpit Formula FX1 Preto e Verde</a></td>
          <td style="text-align: right">~6.290</td>
          <td>Extreme Racing</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/sim-racing-bundles/crd-9020007-8nm-us/gran-turismo-dd-pro-8nm-qr2l-us"target="_blank" rel="noopener">Fanatec Gran Turismo DD Pro 8Nm (motor + volante + pedais + Boost Kit)</a></td>
          <td style="text-align: right">~9.590</td>
          <td>Fanatec / Racing Wheel Brasil</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/pedals/csl_p_lc/csl_pedals_lc"target="_blank" rel="noopener">Fanatec CSL Pedals LC (com Load Cell)</a></td>
          <td style="text-align: right">~1.500</td>
          <td>Fanatec</td>
      </tr>
      <tr>
          <td><a href="https://www.racingwheelbrasil.com.br/produtos/volante-fanatec-csl-elite-mclaren-gt3-v2-pc-xbox-ps4-ps5-ready/"target="_blank" rel="noopener">Fanatec CSL Elite McLaren GT3 V2</a></td>
          <td style="text-align: right">~4.990</td>
          <td>Racing Wheel Brasil</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/add-ons/crd-9040002-ww/clubsport-shifter-sq"target="_blank" rel="noopener">Fanatec ClubSport Shifter SQ V1.5</a></td>
          <td style="text-align: right">~2.500</td>
          <td>Fanatec</td>
      </tr>
      <tr>
          <td><a href="https://www.minisforum.com/products/UX790-Pro.html"target="_blank" rel="noopener">Minisforum UX790 Pro</a></td>
          <td style="text-align: right">~5.000</td>
          <td>Minisforum</td>
      </tr>
      <tr>
          <td><a href="https://www.minisforum.com/products/minisforum-deg1-egpu-dock"target="_blank" rel="noopener">Minisforum DEG1 eGPU Dock</a> + RTX 4090</td>
          <td style="text-align: right">~12.000</td>
          <td>Minisforum / comprado separado</td>
      </tr>
      <tr>
          <td>PlayStation 5 + <a href="https://dbrand.com/shop/limited-edition/ps5"target="_blank" rel="noopener">dbrand Darkplates</a></td>
          <td style="text-align: right">~4.500</td>
          <td>Sony / dbrand</td>
      </tr>
      <tr>
          <td><a href="https://www.samsung.com/br/monitors/gaming/odyssey-oled-g8-g81sf-32-inch-240hz-oled-uhd-ls32fg810snxzd/"target="_blank" rel="noopener">Samsung Odyssey OLED G8 32&quot;</a></td>
          <td style="text-align: right">~2.500</td>
          <td>Samsung</td>
      </tr>
      <tr>
          <td><a href="https://www.amazon.com.br/dp/B00MNGIP2Y"target="_blank" rel="noopener">OREI BK-21A HDMI 2.1 Switcher 2x1 com extração de áudio</a></td>
          <td style="text-align: right">~450</td>
          <td>Amazon</td>
      </tr>
      <tr>
          <td><a href="https://www.mercadolivre.com.br/amplificador-de-potncia-aiyima-d03-bluetooth-50-150-watts-cor-preto/p/MLB46172770"target="_blank" rel="noopener">Amplificador Aiyima D03</a></td>
          <td style="text-align: right">~900</td>
          <td>Mercado Livre</td>
      </tr>
      <tr>
          <td><a href="https://edifier.com.br/caixa-de-som-passiva-p12-madeira-edifier.html"target="_blank" rel="noopener">Edifier P12 (par)</a></td>
          <td style="text-align: right">~799</td>
          <td>Edifier</td>
      </tr>
      <tr>
          <td><a href="https://www.mercadolivre.com.br/fones-de-ouvido-meze-audio-109-pro-com-fio-de-madeira-com-en/p/MLB42456685"target="_blank" rel="noopener">Meze 109 Pro</a></td>
          <td style="text-align: right">~5.390</td>
          <td>Mercado Livre / Heinrich Audio</td>
      </tr>
      <tr>
          <td>Cabos (HDMI 2.1, óptico, P2, energia)</td>
          <td style="text-align: right">~300</td>
          <td>Diversos</td>
      </tr>
      <tr>
          <td><strong>TOTAL ESTIMADO</strong></td>
          <td style="text-align: right"><strong>~56.709</strong></td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>Sim, quase R$ 57 mil é bastante dinheiro. Eu trabalhei feito um condenado por décadas. Agora que consegui me aposentar honestamente, minha família está bem amparada, não tenho dívidas, e eu posso finalmente me dar algo que eu sempre quis quando era moleque mas não tinha como bancar. Quando eu sentava naqueles cabinets de OutRun e Daytona USA no fliperama, eu sonhava em ter algo assim em casa. Levou 30 e poucos anos, mas cheguei lá.</p>
<p>E se você somar os anos de gambiarras, suportes que não funcionavam, cabos HDMI de 15 metros, impressões 3D, placas de aço usinadas, e a frustração de montar e desmontar tudo — o cockpit dedicado economiza sanidade. Diferente de um PC que deprecia rápido, um cockpit de aço dura décadas.</p>
]]></content:encoded><category>off-topic</category><category>gaming</category><category>sim-racing</category><category>cockpit</category><category>fanatec</category><category>gran-turismo</category><category>initial-d</category></item><item><title>O código fonte do Claude Code vazou. O que achamos dentro.</title><link>https://www.akitaonrails.com/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/</guid><pubDate>Tue, 31 Mar 2026 17:00:00 GMT</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;Atualizado em 2 de abril de 2026: se você já tinha lido este texto ontem, vale pular direto para a &lt;a href="#update-2026-04-02"&gt;nova seção de atualização&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hoje de manhã (31 de março de 2026), o pesquisador de segurança &lt;a href="https://x.com/Fried_rice"target="_blank" rel="noopener"&gt;Chaofan Shou&lt;/a&gt; descobriu que o código fonte inteiro do Claude Code, a CLI oficial da Anthropic pra coding com IA, estava disponível pra qualquer pessoa no registry público do npm. 512 mil linhas de TypeScript. 1.900 arquivos. Tudo exposto num arquivo de source map de 59.8MB incluído acidentalmente na versão 2.1.88 do pacote &lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p>Atualizado em 2 de abril de 2026: se você já tinha lido este texto ontem, vale pular direto para a <a href="#update-2026-04-02">nova seção de atualização</a>.</p>

</blockquote>
<p>Hoje de manhã (31 de março de 2026), o pesquisador de segurança <a href="https://x.com/Fried_rice"target="_blank" rel="noopener">Chaofan Shou</a> descobriu que o código fonte inteiro do Claude Code, a CLI oficial da Anthropic pra coding com IA, estava disponível pra qualquer pessoa no registry público do npm. 512 mil linhas de TypeScript. 1.900 arquivos. Tudo exposto num arquivo de source map de 59.8MB incluído acidentalmente na versão 2.1.88 do pacote <code>@anthropic-ai/claude-code</code>.</p>
<p>Em poucas horas o código já estava espelhado no GitHub, analisado por milhares de desenvolvedores, e a Anthropic soltou uma nota dizendo que foi &ldquo;erro humano no empacotamento de release, não uma brecha de segurança&rdquo;. O que é tecnicamente verdade mas ignora que o resultado é o mesmo.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/leak-tweet.png" alt="Tweet anunciando o vazamento"  loading="lazy" /></p>
<p>Eu uso Claude Code todo dia. Alguns dos artigos que você lê aqui eu escrevi com ele. Então resolvi olhar o que tem dentro. Inclusive comecei este texto no próprio Claude Code, mas meu plano Max acabou antes de eu terminar. O resto eu fechei no Codex.</p>
<h2>Como aconteceu o vazamento<span class="hx:absolute hx:-mt-20" id="como-aconteceu-o-vazamento"></span>
    <a href="#como-aconteceu-o-vazamento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Claude Code é empacotado com o <a href="https://bun.sh/"target="_blank" rel="noopener">Bun</a>, o runtime JavaScript que a Anthropic adquiriu no final de 2024. Quando você builda com Bun, source maps são gerados por padrão. Esses arquivos <code>.map</code> contêm o código fonte original completo, não só mapeamentos. Cada arquivo, cada comentário, cada constante interna, cada system prompt.</p>
<p>A teoria inicial era que um <a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">bug conhecido no Bun</a> teria causado o vazamento: mesmo com <code>development: false</code>, source maps continuavam sendo servidos e incluídos nos bundles. Mas o próprio <a href="https://github.com/oven-sh/bun/issues/28001#issuecomment-4164447815"target="_blank" rel="noopener">Jarred Sumner</a>, criador do Bun, desmentiu: &ldquo;This has nothing to do with claude code. This is with Bun&rsquo;s frontend development server. Claude Code is not a frontend app. It is a TUI. It doesn&rsquo;t use Bun.serve() to compile a single-file executable.&rdquo; Ou seja, o bug do Bun afeta o dev server de frontend, não o processo de build que gerou o pacote npm do Claude Code.</p>
<p>O que de fato aconteceu é mais simples: alguém na Anthropic esqueceu de adicionar <code>*.map</code> ao <code>.npmignore</code> ou não configurou o bundler pra pular geração de source maps em builds de produção. E pior: segundo o <a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register</a>, o source map não só apontava pros arquivos originais, como referenciava um ZIP hospedado num bucket Cloudflare R2 da própria Anthropic. O npm serviu feliz pra qualquer pessoa que rodasse <code>npm pack</code>, e o resto virou trabalho de espelho.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/claude-files.png" alt="Arquivos fonte expostos no pacote npm"  loading="lazy" /></p>
<p>A ironia é que o código contém um sistema inteiro chamado &ldquo;Undercover Mode&rdquo; feito especificamente pra evitar que informações internas da Anthropic vazem em commits e PRs. Eles construíram um subsistema pra impedir o AI de revelar codinomes internos, e aí o source map expôs tudo.</p>
<h2>O que tem dentro: as features escondidas<span class="hx:absolute hx:-mt-20" id="o-que-tem-dentro-as-features-escondidas"></span>
    <a href="#o-que-tem-dentro-as-features-escondidas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O código fonte revela 44 feature flags cobrindo funcionalidades prontas mas ainda não lançadas. Não é vaporware. É código real escondido atrás de flags que compilam pra <code>false</code> nos builds externos. Vou destacar as mais interessantes.</p>
<h3>KAIROS: Claude que nunca para<span class="hx:absolute hx:-mt-20" id="kairos-claude-que-nunca-para"></span>
    <a href="#kairos-claude-que-nunca-para" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dentro do diretório <code>assistant/</code>, existe um modo chamado KAIROS, um assistente persistente que não espera você digitar. Ele observa, registra e age proativamente sobre coisas que percebe. Mantém arquivos de log diários append-only, recebe prompts <code>&lt;tick&gt;</code> em intervalos regulares pra decidir se deve agir ou ficar quieto, e tem um budget de 15 segundos: qualquer ação proativa que bloquearia o workflow do usuário por mais de 15 segundos é adiada.</p>
<p>Ferramentas exclusivas do KAIROS: <code>SendUserFile</code> (envia arquivos pro usuário), <code>PushNotification</code> (notificações push), <code>SubscribePR</code> (monitora pull requests). Nada disso existe no build público.</p>
<h3>BUDDY: um Tamagotchi no terminal<span class="hx:absolute hx:-mt-20" id="buddy-um-tamagotchi-no-terminal"></span>
    <a href="#buddy-um-tamagotchi-no-terminal" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Não estou inventando. O Claude Code tem um sistema completo de pet companion estilo Tamagotchi chamado &ldquo;Buddy&rdquo;. Um sistema gacha determinístico com 18 espécies, raridade, variantes shiny, stats gerados proceduralmente, e uma &ldquo;alma&rdquo; escrita pelo Claude no primeiro hatch.</p>
<p>A espécie é determinada por um PRNG Mulberry32 seedado pelo hash do userId. Mesmo usuário sempre recebe o mesmo buddy. Tem 5 stats (DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK), 6 estilos de olhos, 8 opções de chapéu, e sprites renderizados como ASCII art de 5 linhas com animações. O código referencia 1-7 de abril de 2026 como janela de teaser, com lançamento completo pra maio de 2026.</p>
<h3>ULTRAPLAN: 30 minutos de planejamento remoto<span class="hx:absolute hx:-mt-20" id="ultraplan-30-minutos-de-planejamento-remoto"></span>
    <a href="#ultraplan-30-minutos-de-planejamento-remoto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O ULTRAPLAN offloads tarefas complexas de planejamento pra uma sessão remota rodando Opus 4.6, dá até 30 minutos pra pensar, e permite que você aprove o resultado pelo browser. O terminal mostra polling a cada 3 segundos, e quando aprovado, um valor sentinela <code>__ULTRAPLAN_TELEPORT_LOCAL__</code> &ldquo;teletransporta&rdquo; o resultado de volta pro terminal local.</p>
<h3>Multi-Agent: &ldquo;Coordinator Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="multi-agent-coordinator-mode"></span>
    <a href="#multi-agent-coordinator-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O sistema de orquestração multi-agente no diretório <code>coordinator/</code> transforma o Claude Code de um agente único num coordenador que spawna, dirige e gerencia múltiplos workers em paralelo. Research em paralelo, síntese pelo coordenador, implementação pelos workers, verificação pelos workers. O prompt ensina paralelismo explicitamente e proíbe delegação preguiçosa: &ldquo;Do NOT say &lsquo;based on your findings&rsquo; - read the actual findings and specify exactly what to do.&rdquo;</p>
<p>E tem mais. O leak também mostra teammates in-process com <code>AsyncLocalStorage</code> pra isolar contexto, workers em processos separados via tmux/iTerm2 panes, sincronização de memória entre agentes, e flags já prontas para <code>BRIDGE_MODE</code>, <code>VOICE_MODE</code>, <code>WORKFLOW_SCRIPTS</code>, <code>AFK mode</code>, <code>advisor-tool</code> e <code>history snipping</code>. Isso não garante lançamento, mas sugere um roadmap bem mais adiantado do que a versão pública deixa transparecer.</p>
<h2>A arquitetura de memória<span class="hx:absolute hx:-mt-20" id="a-arquitetura-de-memória"></span>
    <a href="#a-arquitetura-de-mem%c3%b3ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema de memória me chamou atenção. Não é um &ldquo;guarde tudo e recupere&rdquo;. É uma arquitetura de três camadas:</p>
<p><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-code-memory-architecture.jpg" alt="Resumo da arquitetura de memória do Claude Code"  loading="lazy" /></a></p>
<p>O <code>MEMORY.md</code> é um índice leve de ponteiros (~150 caracteres por linha) que fica permanentemente carregado no contexto. Não guarda dados, guarda localizações. O conhecimento real está distribuído em &ldquo;topic files&rdquo; buscados sob demanda. Transcrições brutas nunca são lidas inteiras de volta no contexto, apenas pesquisadas com grep pra identificadores específicos.</p>
<p>E isso vem com uma disciplina importante: o sistema escreve primeiro no arquivo de tópico e só depois atualiza o índice. O <code>MEMORY.md</code> não vira depósito de fatos. Continua sendo só mapa. Se você deixa o índice virar storage, ele polui o contexto permanente e degrada o sistema inteiro.</p>
<p>O sistema &ldquo;Dream&rdquo; (<code>services/autoDream/</code>) é um motor de consolidação de memória que roda como subagent em background. O nome é intencional. É o Claude sonhando.</p>
<p>O sonho tem um sistema de trigger com três portas: 24 horas desde o último sonho, pelo menos 5 sessões desde o último sonho, e aquisição de um lock de consolidação (impede sonhos concorrentes). As três precisam passar.</p>
<p>Quando roda, segue quatro fases: Orient (ls no diretório de memória, lê o índice), Gather (busca sinais novos em logs, memórias desatualizadas, transcrições), Consolidate (escreve ou atualiza topic files, converte datas relativas pra absolutas, deleta fatos contraditos), e Prune (mantém o índice abaixo de 200 linhas e ~25KB).</p>
<p>Os tipos de memória são quatro: <code>user</code> (perfil do usuário), <code>feedback</code> (correções e confirmações), <code>project</code> (contexto sobre o trabalho em andamento), <code>reference</code> (ponteiros pra sistemas externos). A taxonomia exclui explicitamente coisas deriváveis do código (patterns, arquitetura, git history, file structure).</p>
<p>O subagent de sonho recebe bash read-only. Pode olhar o projeto mas não pode modificar nada. É puramente uma passada de consolidação.</p>
<p>E tem outro detalhe que eu achei elegante: memória não é tratada como verdade. É tratada como pista. O sistema assume que memória pode estar velha, errada ou contraditória, então o modelo ainda precisa verificar antes de confiar. Isso é o oposto da fantasia de &ldquo;joga tudo num banco vetorial e deixa a mágica acontecer&rdquo;.</p>
<h2>O &ldquo;Undercover Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="o-undercover-mode"></span>
    <a href="#o-undercover-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Funcionários da Anthropic (identificados por <code>USER_TYPE === 'ant'</code>) usam o Claude Code em repositórios públicos e open source. O Undercover Mode (<code>utils/undercover.ts</code>) impede que o AI revele informações internas acidentalmente em commits e PRs.</p>
<p>Quando ativo, injeta no system prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>## UNDERCOVER MODE - CRITICAL

You are operating UNDERCOVER in a PUBLIC/OPEN-SOURCE repository. Your commit
messages, PR titles, and PR bodies MUST NOT contain ANY Anthropic-internal
information. Do not blow your cover.

NEVER include in commit messages or PR descriptions:
- Internal model codenames (animal names like Capybara, Tengu, etc.)
- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8)
- Internal repo or project names
- Internal tooling, Slack channels, or short links
- The phrase &#34;Claude Code&#34; or any mention that you are an AI
- Co-Authored-By lines or any other attribution</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não tem como desligar. Se o sistema não tem certeza que está num repositório interno, fica em modo undercover. Isso confirma uma coisa meio desconfortável: a Anthropic usa Claude Code pra contribuir em open source, e o agente é instruído a esconder que é IA.</p>
<p>Os codinomes internos são nomes de animais: Tengu (codinome do projeto Claude Code), Fennec (Opus), Capybara, Numbat (em teste). O &ldquo;Fast Mode&rdquo; é internamente chamado de &ldquo;Penguin Mode&rdquo; com endpoint <code>claude_code_penguin_mode</code> e kill-switch <code>tengu_penguins_off</code>.</p>
<h2>As partes mais paranoicas<span class="hx:absolute hx:-mt-20" id="as-partes-mais-paranoicas"></span>
    <a href="#as-partes-mais-paranoicas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma parte da análise que eu quase deixei passar porque estava olhando mais pras features escondidas. Mas talvez o mais revelador sobre a mentalidade da Anthropic esteja nos mecanismos de defesa contra cópia e abuso.</p>
<p>Segundo a análise do <a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim</a>, existe um modo de anti-distillation que pode pedir ao servidor pra injetar ferramentas falsas no prompt do sistema. A ideia é envenenar tráfego gravado por quem estiver tentando destilar o comportamento do Claude Code pra treinar concorrente. Tem também um segundo mecanismo de sumarização de texto de conectores, assinado criptograficamente, pra que parte do tráfego observável não corresponda ao raciocínio bruto original. Não é proteção perfeita. É mais uma camada de atrito. Mas mostra que a empresa está pensando explicitamente em cópia por observação, não só em segurança tradicional.</p>
<p>E tem a parte mais agressiva: client attestation. Cada request inclui um header de billing com um placeholder <code>cch=00000</code>, e o runtime nativo do Bun substitui isso por um hash calculado abaixo da camada JavaScript. Em outras palavras, não basta parecer Claude Code. O binário tenta provar que é Claude Code. Isso ajuda a explicar por que a briga com ferramentas terceiras como OpenCode ficou tão sensível: não era só questão comercial ou jurídica. Tinha enforcement técnico embutido no transporte.</p>
<h3>Atualização: o DRM morreu em menos de 24 horas<span class="hx:absolute hx:-mt-20" id="atualização-o-drm-morreu-em-menos-de-24-horas"></span>
    <a href="#atualiza%c3%a7%c3%a3o-o-drm-morreu-em-menos-de-24-horas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Lembram que eu mencionei o client attestation como &ldquo;a parte mais agressiva&rdquo;? Pois é. Durou menos de um dia.</p>
<p>Pra entender o contexto: a Anthropic vinha travando uma guerra contra ferramentas terceiras desde janeiro de 2026. Primeiro veio o bloqueio server-side de tokens OAuth vindos de clientes não-oficiais. Depois, em março, o maintainer do <a href="https://github.com/anomalyco/opencode"target="_blank" rel="noopener">OpenCode</a> mergeou um <a href="https://github.com/anomalyco/opencode/issues/7456"target="_blank" rel="noopener">PR</a> removendo toda autenticação Claude do projeto. O commit message tinha duas palavras: &ldquo;anthropic legal requests.&rdquo; O <a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register reportou</a> que a Anthropic atualizou seus termos de serviço pra deixar explícito que tokens OAuth de assinaturas Pro/Max só podem ser usados no Claude Code oficial e no Claude.ai. Quem pagava $100-200/mês pelo Max e queria usar a ferramenta de sua escolha ficou na mão.</p>
<p>O mecanismo técnico por trás do bloqueio era justamente o <code>cch=</code>. Com o código vazado dá pra ver que o sistema tem duas partes. A primeira é um sufixo de versão: o campo <code>cc_version</code> inclui 3 caracteres hex derivados da primeira mensagem do usuário via SHA-256, usando um salt de 12 caracteres embutido no JavaScript. A segunda é o body hash propriamente dito: o corpo inteiro do request (mensagens, ferramentas, metadata, modelo, config de thinking, tudo) é serializado como JSON compacto com o placeholder <code>cch=00000</code>, e então hasheado com <a href="https://github.com/Cyan4973/xxHash"target="_blank" rel="noopener">xxHash64</a> usando um seed fixo. O resultado é mascarado com <code>0xFFFFF</code> (20 bits) e formatado como 5 caracteres hex lowercase. O placeholder é substituído pelo hash calculado antes do request sair do processo.</p>
<p>O detalhe que faz a diferença: essa substituição acontece dentro do runtime nativo do Bun, escrito em Zig, abaixo da camada JavaScript. O Bun literalmente muta a string JavaScript in-place, sobrescrevendo os bytes <code>00000</code> no buffer da string com o hash computado. Se você rodasse o mesmo bundle em Node ou num Bun stock, o placeholder iria pro servidor como está e o request seria rejeitado.</p>
<p>E aí veio o vazamento. Com o source code exposto, o <a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG</a> (Jay Guthrie) anunciou na noite do mesmo dia: &ldquo;Yesterday I said Anthropic&rsquo;s compiled Zig cch= hash was banning 3rd-party Claude clients. Tonight, the DRM is dead. We extracted the algorithm from the binary. It&rsquo;s not advanced cryptography. It&rsquo;s a static xxHash64 seed.&rdquo;</p>
<p>O seed é <code>0x6E52736AC806831E</code>. O <a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">algoritmo completo</a>, como explicou numa sequência de tweets, cabe em poucas linhas de TypeScript:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">xxhash</span> <span class="kr">from</span> <span class="s2">&#34;xxhash-wasm&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">h64Raw</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">xxhash</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span> <span class="c1">// com cch=00000 no placeholder
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">h64Raw</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span> <span class="mh">0x6E52736AC</span><span class="nx">n</span> <span class="o">|</span> <span class="p">(</span><span class="mh">0x806831E</span><span class="nx">n</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">cch</span> <span class="o">=</span> <span class="p">(</span><span class="nx">hash</span> <span class="o">&amp;</span> <span class="mh">0xFFFFF</span><span class="nx">n</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">).</span><span class="nx">padStart</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s2">&#34;0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// substituir cch=00000 por cch={valor calculado}
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <a href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn</a> celebrou: &ldquo;we cracked it. the cch= signing system in claude code is fully reverse engineered.&rdquo; E já colocou o bypass no <a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">free-code</a>, um fork do Claude Code com telemetria removida, guardrails de system prompt stripados, e todas as 54 feature flags experimentais desbloqueadas.</p>
<p><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/free-code-screenshot.png" alt="Screenshot do free-code rodando com features experimentais"  loading="lazy" /></a></p>
<p>O ponto técnico que importa: xxHash64 não é criptografia. É um hash de checksum projetado pra velocidade, não pra segurança. O seed é estático, embutido no binário. Muda a cada versão do Claude Code, mas dentro de uma versão é o mesmo pra todo mundo. A &ldquo;segurança&rdquo; dependia inteiramente de ninguém conseguir extrair o seed do binário Zig compilado. Com o source code vazado, essa obscuridade evaporou em horas.</p>
<p>Agora qualquer client terceiro — OpenCode, Claw-Code, o que for — pode interceptar o <code>fetch()</code>, hashear o body com o seed correto, e passar pela validação do servidor como se fosse o Claude Code oficial. A barreira que a Anthropic construiu pra proteger seu modelo de negócio de $2.5 bilhões de ARR era, no fim das contas, security by obscurity num hash não-criptográfico.</p>
<p>O terceiro detalhe é pequeno mas diz muito sobre produto real em produção: o sistema detecta frustração de usuário com regex. Sim, regex. Palavrão, insulto, &ldquo;this sucks&rdquo;, esse tipo de coisa. É engraçado ver uma empresa de LLM fazendo sentiment analysis na base do <code>wtf|ffs|shit</code>, mas também é o tipo de solução pragmática que alguém coloca quando precisa de resposta barata e imediata, não de elegância conceitual.</p>
<h2>O que o código revela sobre como você usa o Claude Code<span class="hx:absolute hx:-mt-20" id="o-que-o-código-revela-sobre-como-você-usa-o-claude-code"></span>
    <a href="#o-que-o-c%c3%b3digo-revela-sobre-como-voc%c3%aa-usa-o-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> compilou uma thread com sete achados técnicos do código que qualquer usuário deveria saber:</p>
<p>O Claude Code tem um cap de 2.000 linhas por leitura de arquivo. Quando você pede pra ler um arquivo maior, ele trunca silenciosamente. Resultados de ferramentas são cortados em 50.000 caracteres. O sistema de compressão de context window descarta mensagens antigas pra caber mais contexto novo. E existe uma diferença entre o nível de acesso de funcionários Anthropic (<code>USER_TYPE === 'ant'</code>) e o acesso público: ferramentas internas como <code>ConfigTool</code> e <code>TungstenTool</code> são invisíveis no build externo.</p>
<p>A descoberta mais útil da thread é como funcionários Anthropic contornam as limitações que os usuários externos enfrentam. O código revela que <code>USER_TYPE === 'ant'</code> desbloqueia ferramentas internas, beta headers exclusivos (<code>cli-internal-2026-02-09</code>), acesso a staging (<code>claude-ai.staging.ant.dev</code>), e um <code>ConfigTool</code> que permite alterar configurações em runtime. Builds externos compilam tudo isso pra <code>false</code> via dead code elimination.</p>
<p>Mas o ponto que interessa é: o CLAUDE.md que você coloca na raiz do seu projeto é lido inteiro pelo Claude Code e injetado no system prompt. É literalmente o lugar onde você controla como o agente se comporta. O <a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> publicou um override completo com 10 regras mecânicas, e depois subiu o arquivo inteiro num repositório separado: <a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md</a>.</p>
<p><a href="https://github.com/iamfakeguru/claude-md/blob/main/CLAUDE.md"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-md-production-grade-agent-directives.png" alt="Screenshot do CLAUDE.md publicado pelo fakeguru"  loading="lazy" /></a></p>
<p>Eu não vou colar o bloco inteiro aqui. O que importa é o conteúdo: ele força verificação pós-edição (<code>tsc</code> e <code>eslint</code> antes de declarar sucesso), impõe releitura de arquivos antes de editar, exige leitura em chunks para arquivos grandes, assume truncamento silencioso de resultados muito longos, e manda quebrar trabalho maior em fases ou subagentes paralelos. Em outras palavras: ele transforma em regra explícita tudo que os usuários externos estavam apanhando para descobrir empiricamente.</p>
<p>Essas não são instruções mágicas. São guardrails. A diferença é que agora sabemos quais limites o sistema realmente tem e podemos escrever um CLAUDE.md que trabalha a favor deles, não contra eles.</p>
<h2>Bugs de cache que custam caro<span class="hx:absolute hx:-mt-20" id="bugs-de-cache-que-custam-caro"></span>
    <a href="#bugs-de-cache-que-custam-caro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne</a> (Alex Volkov) reportou bugs de invalidação de cache que fazem tokens não-cacheados custarem 10-20x mais que os cacheados. São dois bugs: um de substituição de string no Bun que afeta a CLI standalone (workaround: usar <code>npx @anthropic-ai/claude-code</code> em vez do binário instalado), e outro na flag <code>--resume</code> que quebra o cache sem workaround conhecido. Mais de 500 usuários reportaram problemas similares de exaustão de quota. Se você sentiu que o Claude Code estava gastando tokens mais rápido que o esperado nos últimos dias, provavelmente não era impressão.</p>
<h2>&ldquo;Spaghetti de staff engineer&rdquo;<span class="hx:absolute hx:-mt-20" id="spaghetti-de-staff-engineer"></span>
    <a href="#spaghetti-de-staff-engineer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A análise do código revelou problemas reais. Um comentário no próprio fonte admite: &ldquo;1.279 sessões tiveram 50+ falhas consecutivas (até 3.272) numa única sessão, desperdiçando ~250K chamadas de API por dia globalmente.&rdquo; O fix foram três linhas: limitar falhas consecutivas a três antes de desabilitar compactação.</p>
<p>O arquivo <code>print.ts</code> tem 5.594 linhas com uma única função de 3.167 linhas contendo doze níveis de nesting. O <code>main.tsx</code> tem 803.924 bytes num único arquivo. O <code>interactiveHelpers.tsx</code> tem 57.424 bytes. São arquivos que nenhum humano consegue revisar com confiança.</p>
<p>A reação mais viral veio do <a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze</a>: ele pediu pro GPT-5.4 avaliar o codebase e a nota foi 6.5/10. A descrição: &ldquo;This is not junior spaghetti. This is staff-engineer spaghetti: performance-aware, feature-flagged, telemetry-instrumented, surgically optimized spaghetti.&rdquo; Ou seja, não é código ruim de inexperiência. É código ruim de pressão pra entregar rápido sem pagar o custo de organizar depois.</p>
<p>O <a href="https://x.com/thekitze/status/2038986445839622405"target="_blank" rel="noopener">@thekitze</a> também elaborou em outra thread sobre como o código evidencia falta de práticas básicas de engenharia. E é aqui que eu me sinto vindicado.</p>
<p>Eu venho repetindo em vários posts sobre <a href="/tags/vibe-coding/">vibe coding</a> que velocidade sem disciplina produz exatamente isso. Os princípios que eu defendo, incrementos pequenos, testes a cada passo, revisão antes de commitar, refactoring contínuo, CI que rejeita complexidade ciclomática alta, são os mesmos princípios do Extreme Programming que funcionam desde os anos 2000. A Anthropic aparentemente não seguiu nenhum deles no próprio produto.</p>
<p>Uma função de 3.167 linhas com 12 níveis de nesting não é algo que aparece da noite pro dia. É acúmulo. É o resultado de dezenas de adições onde ninguém parou pra refatorar porque &ldquo;tá funcionando, não mexe&rdquo;. É o anti-pattern clássico de vibe coding sem disciplina: gerar código com IA, ver que compila, fazer commit, repetir. Sem review rigoroso. Sem limites de complexidade no CI. Sem a regra básica de que se uma função passa de 50 linhas, ela precisa ser quebrada.</p>
<p>A ironia é que a Anthropic vende a ferramenta de vibe coding mais popular do mercado e não pratica o que eu chamo de vibe coding responsável. O Claude Code vale $2.5 bilhões de ARR. O código que gera esse faturamento tem qualidade 6.5/10.</p>
<h2>A questão do &ldquo;clean room&rdquo;<span class="hx:absolute hx:-mt-20" id="a-questão-do-clean-room"></span>
    <a href="#a-quest%c3%a3o-do-clean-room" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Com o código fonte inteiro público, surge uma implicação legal e competitiva séria. E aqui eu acho que muita gente começou a usar o termo &ldquo;clean room&rdquo; com uma leveza que não combina com o assunto.</p>
<p>Clean room de verdade não é só &ldquo;reescrevi em outra linguagem&rdquo; nem &ldquo;não dei copy and paste&rdquo;. O modelo clássico é bem mais chato: um grupo estuda o original e produz uma especificação funcional; outro grupo, isolado, implementa a partir dessa especificação sem ver o código original. A ideia é justamente reduzir o risco de contaminação.</p>
<p>O <a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai</a> levantou outro ponto interessante: com ferramentas generativas, alguém poderia tentar um &ldquo;clean room rebuild&rdquo; usando testes, comportamento observável e documentação, sem reaproveitar a implementação original. Em tese, faz sentido. Na prática, o que aparece no calor do vazamento costuma ficar numa zona bem mais cinzenta.</p>
<p>O caso do <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> ilustra isso bem. O projeto se apresenta como rewrite independente e já migrou o foco para Python e Rust, mas o próprio README admite estudo direto do código exposto e fala até em parity audit contra archive local. Então eu não chamaria isso de clean room clássico no sentido mais rigoroso. Eu chamaria de reimplementação inspirada, com tentativa deliberada de se afastar do snapshot vazado.</p>
<p>Isso não quer dizer que toda reimplementação está condenada. Copyright de software não protege ideia abstrata, fluxo genérico de ferramenta, arquitetura em alto nível ou &ldquo;uma CLI que faz X&rdquo;. Protege expressão concreta. Mas justamente por isso a disciplina importa. Quanto mais um projeto quiser sustentar independência, menos ele deveria depender do material vazado como benchmark direto.</p>
<p>Tem um detalhe mais pragmático aí: as cópias literais do source vazado provavelmente vão sumir rápido quando os primeiros DMCA começarem a chegar. Mirror cai fácil. É por isso que uma reimplementação interessa mais do que um espelho bruto. Isso não apaga a discussão jurídica, mas muda bastante o tipo de briga e a chance de continuar no ar.</p>
<p>Foi mais ou menos o que eu mesmo fiz quando <a href="/2026/03/16/reescrevi-o-openclaw-em-rust-funcionou-frankclaw/">reescrevi o OpenClaw em Rust</a>. O ponto não era copiar linha por linha. Era entender o comportamento e reescrever a peça inteira com código meu.</p>
<p>O site satírico <a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh</a> apareceu hoje oferecendo &ldquo;Clean Room as a Service&rdquo; com o tagline &ldquo;Robot-Reconstructed, Zero Attribution&rdquo;. A piada: robôs de IA recriam projetos open source eliminando obrigações de atribuição, com garantias tipo &ldquo;This has never happened because it legally cannot happen. Trust us.&rdquo; e indenização via subsidiária offshore numa jurisdição que não reconhece copyright de software. É sátira, mas é sátira que descreve o que alguém vai tentar fazer de verdade.</p>
<p><a id="update-2026-04-02"></a></p>
<h2>Atualização em 2 de abril de 2026<span class="hx:absolute hx:-mt-20" id="atualização-em-2-de-abril-de-2026"></span>
    <a href="#atualiza%c3%a7%c3%a3o-em-2-de-abril-de-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Como o texto acima abre no calor do dia 31, vale registrar o que aconteceu logo depois. Resolvi adicionar esta atualização depois de ler <a href="https://x.com/k1rallik/status/2039686500619534818"target="_blank" rel="noopener">este tweet do @k1rallik</a>, que resume bem o clima do pós-vazamento, mas mistura fatos confirmáveis com um pouco de épica demais.</p>
<p>Primeiro: a parte do DMCA ficou mais bagunçada do que parecia. A própria notice publicada no repositório <code>github/dmca</code> diz que o GitHub processou a remoção contra a rede inteira de <strong>8.1 mil repositórios</strong>, porque a notificação afirmava que &ldquo;all or most of the forks&rdquo; eram infringing na mesma medida que o repositório principal. No dia seguinte, a Anthropic publicou uma <strong>retratação parcial</strong>: pediu reinstalação de todos os repositórios removidos, exceto o <code>nirholas/claude-code</code> e <strong>96 forks listados individualmente</strong>. Então a tese de que a tentativa inicial foi ampla demais está certa. O retrato final, porém, não é &ldquo;8.100 repositórios ficaram derrubados&rdquo;. O que houve foi um recuo formal depois da remoção em massa.</p>
<p>Segundo: o projeto <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> realmente explodiu. Na hora em que atualizei este post, o GitHub já mostrava <strong>142.829 stars</strong> e <strong>101.510 forks</strong>. Isso por si só já basta pra dizer que a história saiu da categoria &ldquo;fork curioso do vazamento&rdquo; e entrou na categoria &ldquo;efeito colateral competitivo real&rdquo;. O tweet viral que circulou hoje acerta no tamanho do estrago, mas exagera em alguns detalhes. O próprio README do projeto se autodescreve como &ldquo;the fastest repo in history to surpass 50K stars&rdquo; e diz que a marca veio em duas horas. Eu não consegui confirmar esse marco histórico de forma independente, então prefiro tratar isso como alegação do próprio projeto, não como fato fechado.</p>
<p>Terceiro: a parte do Rust também precisa de nuance. Sim, já existe workspace em Rust no branch principal e o <code>Cargo.toml</code> está com versão <code>0.1.0</code>. Mas eu não encontrei release pública no GitHub para sustentar a frase &ldquo;já saiu release 0.1.0&rdquo; como um lançamento formal. O que dá pra afirmar com segurança é outra coisa: o projeto já tem base em Python, já tem workspace em Rust, e já virou alvo de atenção suficiente para continuar existindo mesmo sem o mirror literal do código vazado.</p>
<h2>O que a Anthropic deveria ter feito<span class="hx:absolute hx:-mt-20" id="o-que-a-anthropic-deveria-ter-feito"></span>
    <a href="#o-que-a-anthropic-deveria-ter-feito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A Anthropic respondeu rápido. Tirou o pacote comprometido, soltou uma nota pública, e limpou o que podia. Mas o dano já estava feito. O código foi espelhado antes da remoção. Mirrors no GitHub, análises em blogs, threads no X/Twitter. Não tem como des-publicar algo na internet.</p>
<p>O que me incomoda não é o vazamento em si. Bugs acontecem. O que me incomoda é que isso era evitável com práticas básicas de engenharia:</p>
<ol>
<li>Adicionar <code>*.map</code> ao <code>.npmignore</code>. Uma linha.</li>
<li>Configurar o bundler pra não gerar source maps em builds de produção. Uma flag.</li>
<li>Ter um CI check que rejeita publicação se o pacote contém <code>.map</code>. Um script de 5 linhas.</li>
<li>Ter um pipeline de release com review manual antes de publicar no npm. Processo, não código.</li>
</ol>
<p>Nenhuma dessas é difícil. Todas são o tipo de coisa que se perde quando você está movendo rápido demais e não tem disciplina no processo de release. É exatamente o que eu prego como <a href="/tags/vibe-coding/">vibe coding disciplinado</a>: mover rápido não significa pular os guardrails.</p>
<p>E a segunda falha: a qualidade do código em si. 512 mil linhas com funções de 3 mil linhas e 12 níveis de nesting não é engenharia. É acúmulo. É o que acontece quando você gera código com IA sem review rigoroso, sem refactoring contínuo, sem CI que rejeita complexidade ciclomática alta. A ironia de ser justamente a empresa que vende a ferramenta de vibe coding mais popular do mundo não passa despercebida.</p>
<h2>Fontes<span class="hx:absolute hx:-mt-20" id="fontes"></span>
    <a href="#fontes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/Kuberwastaken/claude-code"target="_blank" rel="noopener">Kuberwastaken/claude-code - Breakdown completo do código vazado</a></li>
<li><a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim - Claude Code Source Leak: fake tools, frustration regexes, undercover mode</a></li>
<li><a href="https://venturebeat.com/technology/claude-codes-source-code-appears-to-have-leaked-heres-what-we-know/"target="_blank" rel="noopener">VentureBeat - Claude Code&rsquo;s source code appears to have leaked</a></li>
<li><a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register - Anthropic accidentally exposes Claude Code source code</a></li>
<li><a href="https://fortune.com/2026/03/31/anthropic-source-code-claude-code-data-leak-second-security-lapse-days-after-accidentally-revealing-mythos/"target="_blank" rel="noopener">Fortune - Anthropic leaks its own AI coding tool&rsquo;s source code</a></li>
<li><a href="https://cybernews.com/security/anthropic-claude-code-source-leak/"target="_blank" rel="noopener">Cybernews - Full source code for Anthropic&rsquo;s Claude Code leaks</a></li>
<li><a href="https://gizmodo.com/source-code-for-anthropics-claude-code-leaks-at-the-exact-wrong-time-2000740379"target="_blank" rel="noopener">Gizmodo - Source Code for Anthropic&rsquo;s Claude Code Leaks at the Exact Wrong Time</a></li>
<li><a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone?s=33"target="_blank" rel="noopener">Anthropic - Anthropic acquires Bun as Claude Code reaches $1B milestone</a></li>
<li><a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">Bun Issue #28001 - Source maps incorrectly served in production</a></li>
<li><a href="https://news.ycombinator.com/item?id=43909409"target="_blank" rel="noopener">Hacker News - Claude&rsquo;s system prompt is over 24k tokens with tools</a></li>
<li><a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh - Clean Room as a Service (sátira)</a></li>
<li><a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru - Thread com 7 achados técnicos do código</a></li>
<li><a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne - Bugs de cache que custam 10-20x mais</a></li>
<li><a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze - &ldquo;Staff-engineer spaghetti&rdquo; 6.5/10</a></li>
<li><a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai - Clean room e implicações legais</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/03/2026-03-31-anthropic.md"target="_blank" rel="noopener">GitHub DMCA - Anthropic takedown notice processada contra a rede de 8.1K repositórios</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/04/2026-04-01-anthropic-retraction.md"target="_blank" rel="noopener">GitHub DMCA - Retratação parcial da Anthropic no dia seguinte</a></li>
<li><a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">ultraworkers/claw-code - Reimplementação em Python e Rust que virou o principal projeto pós-vazamento</a></li>
<li><a href="https://x.com/mem0ai/status/2039041449854124229"target="_blank" rel="noopener">@mem0ai - Análise da arquitetura de memória</a></li>
<li><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener">@himanshustwts - Resumo da arquitetura de memória</a></li>
<li><a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md - Override publicado com o CLAUDE.md completo</a></li>
<li><a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG - &ldquo;the DRM is dead&rdquo; - reverse engineering do cch= hash</a></li>
<li><a href="https://x.com/StraughterG/status/2039344035555344550"target="_blank" rel="noopener">@StraughterG - Seed xxHash64 e código TypeScript do bypass</a></li>
<li><a href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn - &ldquo;we cracked it&rdquo; - confirmação do reverse engineering</a></li>
<li><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">paoloanzn/free-code - Fork do Claude Code com telemetria removida e features desbloqueadas</a></li>
<li><a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">a10k.co - What&rsquo;s cch? Reverse Engineering Claude Code&rsquo;s Request Signing</a></li>
<li><a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register - Anthropic clarifies ban on third-party tool access to Claude</a></li>
<li><a href="https://github.com/anomalyco/opencode/issues/7456"target="_blank" rel="noopener">OpenCode Issue #7456 - Claude Code API credentials removal</a></li>
</ul>
]]></content:encoded><category>ai</category><category>security</category><category>claude-code</category><category>vibe-coding</category><category>open-source</category></item><item><title>Migrando meu Home Server com Claude Code | openSUSE MicroOS</title><link>https://www.akitaonrails.com/2026/03/31/migrando-meu-home-server-com-claude-code/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/31/migrando-meu-home-server-com-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 GMT</pubDate><description>&lt;p&gt;Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (&lt;code&gt;/home/akitaonrails/docker/&lt;/code&gt;, &lt;code&gt;/home/akitaonrails/sonarr/&lt;/code&gt;, &lt;code&gt;/mnt/terachad/&lt;/code&gt;), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.&lt;/p&gt;
&lt;p&gt;Com o &lt;a href="https://www.akitaonrails.com/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;novo Minisforum MS-S1 Max&lt;/a&gt; que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.</p>
<p>Com o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">novo Minisforum MS-S1 Max</a> que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.</p>
<p>O que segue é o relato da migração e o guia pra quem quiser replicar. Se um dia eu precisar reconstruir do zero, esse post é a documentação.</p>
<h2>Escolha do sistema operacional<span class="hx:absolute hx:-mt-20" id="escolha-do-sistema-operacional"></span>
    <a href="#escolha-do-sistema-operacional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Por que não Ubuntu Server de novo<span class="hx:absolute hx:-mt-20" id="por-que-não-ubuntu-server-de-novo"></span>
    <a href="#por-que-n%c3%a3o-ubuntu-server-de-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu usei Ubuntu Server no NUC por praticidade. Mas <code>do-release-upgrade</code> é uma roleta russa. Toda vez que o Ubuntu lança versão nova, a atualização é um risco real de quebrar coisas. Pacotes mudam, configs são sobrescritas, dependências conflitam. Pra um servidor que precisa estar sempre rodando, isso é inaceitável.</p>
<h3>Por que não Arch Linux<span class="hx:absolute hx:-mt-20" id="por-que-não-arch-linux"></span>
    <a href="#por-que-n%c3%a3o-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso Arch no desktop e gosto. Mas Arch é uma rolling release sem nenhuma garantia de estabilidade. Pra um desktop onde eu posso parar e resolver problemas, ótimo. Pra um servidor headless que roda 49 containers Docker e precisa funcionar depois de cada reboot, não.</p>
<h3>Fedora CoreOS vs openSUSE MicroOS<span class="hx:absolute hx:-mt-20" id="fedora-coreos-vs-opensuse-microos"></span>
    <a href="#fedora-coreos-vs-opensuse-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As duas opções modernas pra servidor de containers são Fedora CoreOS e openSUSE MicroOS. As duas são sistemas imutáveis: o root filesystem é read-only, atualizações são atômicas (ou aplicam inteiro ou não aplicam nada), e rollback é instantâneo.</p>
<p>A diferença: Fedora CoreOS usa Ignition (configuração declarativa antes do primeiro boot) e é projetada pra ser provisionada automaticamente. MicroOS usa <code>transactional-update</code> e permite uso interativo normal. Pra um home server onde eu quero SSH e mexer manualmente quando precisar, MicroOS se encaixa melhor.</p>
<h3>O que torna MicroOS diferente<span class="hx:absolute hx:-mt-20" id="o-que-torna-microos-diferente"></span>
    <a href="#o-que-torna-microos-diferente" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O conceito de sistema imutável muda como você opera o servidor:</p>
<p>Toda instalação de pacote ou edição de <code>/etc</code> passa por <code>transactional-update</code>, que cria um snapshot btrfs novo, aplica a mudança nesse snapshot, e no reboot seguinte o sistema boota no snapshot atualizado. Se a mudança quebrar alguma coisa, você faz <code>transactional-update rollback</code> e volta pro snapshot anterior em segundos.</p>
<p>Atualizações são automáticas e diárias. O <code>transactional-update.timer</code> baixa patches, cria snapshot, e o <code>rebootmgr</code> reinicia numa janela configurada (no meu caso, entre 4h e 5h30 da manhã). Se a atualização quebra o boot, o GRUB automaticamente volta pro snapshot anterior.</p>
<p>SELinux vem enforcing por padrão. Isso causou 90% dos problemas durante a migração, mas é a configuração certa pra segurança.</p>
<h2>Setup inicial<span class="hx:absolute hx:-mt-20" id="setup-inicial"></span>
    <a href="#setup-inicial" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Hardware<span class="hx:absolute hx:-mt-20" id="hardware"></span>
    <a href="#hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>AMD Ryzen AI Max+ 395, 128GB LPDDR5X</li>
<li>96GB alocados como VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>NVMe de 2TB (sistema + Docker)</li>
<li>Rede cabeada 2.5Gbps</li>
<li>Synology DS1821+ NAS em 192.168.0.21 (NFS)</li>
</ul>
<h3>Primeiros passos<span class="hx:absolute hx:-mt-20" id="primeiros-passos"></span>
    <a href="#primeiros-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instalação do MicroOS é padrão. Depois:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Criar usuário com UID que bate com o NAS (pra NFS funcionar sem problemas de permissão)</span>
</span></span><span class="line"><span class="cl">useradd -u <span class="m">1026</span> -m akitaonrails
</span></span><span class="line"><span class="cl">passwd akitaonrails
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configurar sudo (dentro de transactional-update shell)</span>
</span></span><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="c1"># dentro: adicionar akitaonrails ao sudoers</span>
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>NFS do Synology<span class="hx:absolute hx:-mt-20" id="nfs-do-synology"></span>
    <a href="#nfs-do-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O NAS Synology exporta <code>/volume1/TERACHAD</code> via NFS. O ponto de montagem no MicroOS é <code>/var/mnt/terachad</code> (não <code>/mnt/</code>, que fica no root imutável).</p>
<p>No <code>/etc/fstab</code> (aplicado via transactional-update):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>192.168.0.21:/volume1/TERACHAD /var/mnt/terachad nfs4 nfsvers=4.1,rsize=262144,wsize=262144,hard,_netdev 0 0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhes que importam: <code>nfsvers=4.1</code> porque 4.2 não funcionou com o Synology. <code>rsize=262144,wsize=262144</code> (256KB buffers) foi a maior melhoria de performance NFS. <code>hard</code> em vez de <code>nofail</code> pra que o mount retente indefinidamente se o NAS desconectar temporariamente.</p>
<h3>GPU / ROCm<span class="hx:absolute hx:-mt-20" id="gpu--rocm"></span>
    <a href="#gpu--rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse passo deu trabalho. O Radeon 8060S do AI Max+ 395 é gfx1151, que o ROCm não suporta oficialmente. Foram necessários três passos, e os três são obrigatórios:</p>
<ol>
<li>BIOS: setar UMA Frame Buffer Size pra 96GB</li>
<li>Kernel: adicionar <code>amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824</code> em <code>/etc/kernel/cmdline</code></li>
<li>Docker: usar <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> em todo container ROCm</li>
</ol>
<p>Sem o passo 2, o ROCm só vê 15.5GB mesmo com a alocação do BIOS. Os números são 96GB / 4KB (page size) = 25.165.824 pages.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824&#34;</span> &gt;&gt; /etc/kernel/cmdline
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo sdbootutil update-all-entries  <span class="c1"># FORA do transactional shell</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Verificação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /sys/class/drm/card1/device/mem_info_vram_total
</span></span><span class="line"><span class="cl"><span class="c1"># 103079215104 (96 * 1024^3)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Docker no MicroOS<span class="hx:absolute hx:-mt-20" id="docker-no-microos"></span>
    <a href="#docker-no-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update --non-interactive pkg install docker
</span></span><span class="line"><span class="cl">sudo reboot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">sudo usermod -aG docker akitaonrails
</span></span><span class="line"><span class="cl"><span class="c1"># logout e login pra o grupo pegar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Instalar docker-compose standalone (o pacote openSUSE não inclui)</span>
</span></span><span class="line"><span class="cl">sudo curl -L <span class="s2">&#34;https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">mkdir -p ~/.docker/cli-plugins
</span></span><span class="line"><span class="cl">ln -s /usr/local/bin/docker-compose ~/.docker/cli-plugins/docker-compose</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>daemon.json<span class="hx:absolute hx:-mt-20" id="daemonjson"></span>
    <a href="#daemonjson" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-level&#34;</span><span class="p">:</span> <span class="s2">&#34;warn&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-driver&#34;</span><span class="p">:</span> <span class="s2">&#34;local&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-opts&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;max-size&#34;</span><span class="p">:</span> <span class="s2">&#34;10m&#34;</span><span class="p">,</span> <span class="nt">&#34;max-file&#34;</span><span class="p">:</span> <span class="s2">&#34;5&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;selinux-enabled&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;live-restore&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;userland-proxy&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec-opts&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;native.cgroupdriver=systemd&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>live-restore: true</code> faz containers sobreviverem restart do daemon Docker. <code>userland-proxy: false</code> usa iptables direto em vez de processos proxy (menos overhead). <code>selinux-enabled: true</code> é obrigatório no MicroOS.</p>
<h2>SELinux e Docker: a maior fonte de problemas<span class="hx:absolute hx:-mt-20" id="selinux-e-docker-a-maior-fonte-de-problemas"></span>
    <a href="#selinux-e-docker-a-maior-fonte-de-problemas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Isso merece uma seção inteira porque foi responsável por 90% dos bugs durante a migração.</p>
<p>No MicroOS com SELinux enforcing, todo container que escreve em diretório bind-mounted do host precisa de tratamento especial. Existem duas abordagens: o sufixo <code>:Z</code> nos volumes e a opção <code>security_opt: label:disable</code>.</p>
<h3>NUNCA use <code>:Z</code>. Use <code>security_opt: label:disable</code>.<span class="hx:absolute hx:-mt-20" id="nunca-use-z-use-security_opt-labeldisable"></span>
    <a href="#nunca-use-z-use-security_opt-labeldisable" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>:Z</code> diz pro Docker relabeling o diretório do host com o contexto SELinux do container. Parece a coisa certa. Na prática:</p>
<ul>
<li>Bancos SQLite quebram. O relabeling muda o contexto do arquivo e o SQLite pode recusar abrir o WAL journal.</li>
<li>Mounts NFS ignoram <code>:Z</code> silenciosamente. O NFS não suporta xattrs do SELinux. O kernel ignora o flag sem erro, mas o container continua sem permissão.</li>
<li>Mounts <code>:ro,Z</code> tentam relabeling mesmo sendo read-only, o que falha em NFS e pode corromper contexto em paths locais.</li>
</ul>
<p>A solução correta pra todo container nesse sistema:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">meuservico</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable    </span><span class="w"> </span><span class="c"># desliga enforcement SELinux pra esse container</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./data:/data     </span><span class="w"> </span><span class="c"># SEM :Z</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yml:/etc/config.yml:ro </span><span class="w"> </span><span class="c"># SEM :Z mesmo em :ro</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>label:disable</code> desliga enforcement de labels SELinux apenas pra aquele container, não pro sistema inteiro. Combinado com o isolamento de rede e processos do Docker, é seguro pra home server.</p>
<h2>A migração dos stacks<span class="hx:absolute hx:-mt-20" id="a-migração-dos-stacks"></span>
    <a href="#a-migra%c3%a7%c3%a3o-dos-stacks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os stacks Docker foram reorganizados em <code>/var/opt/docker/&lt;stack&gt;/docker-compose.yml</code>. No servidor antigo, estavam espalhados em <code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/&lt;servico&gt;/</code>, sem padrão.</p>
<h3>Substituições aplicadas em todos os compose files<span class="hx:absolute hx:-mt-20" id="substituições-aplicadas-em-todos-os-compose-files"></span>
    <a href="#substitui%c3%a7%c3%b5es-aplicadas-em-todos-os-compose-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Antes</th>
          <th>Depois</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/mnt/terachad/</code></td>
          <td><code>/var/mnt/terachad/</code></td>
      </tr>
      <tr>
          <td><code>192.168.0.145</code></td>
          <td><code>192.168.0.90</code></td>
      </tr>
      <tr>
          <td><code>/home/akitaonrails/&lt;servico&gt;/</code></td>
          <td><code>/var/opt/docker/&lt;stack&gt;/&lt;servico&gt;/</code></td>
      </tr>
      <tr>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.14:11434</code></td>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.90:11434</code></td>
      </tr>
  </tbody>
</table>
<h3>Stack de media (Plex, Radarr, Sonarr, etc.)<span class="hx:absolute hx:-mt-20" id="stack-de-media-plex-radarr-sonarr-etc"></span>
    <a href="#stack-de-media-plex-radarr-sonarr-etc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O stack de media é o mais complexo. Plex precisa de IP próprio na LAN (macvlan) pro streaming direto funcionar. O setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create -d macvlan <span class="se">\
</span></span></span><span class="line"><span class="cl">  --subnet<span class="o">=</span>192.168.0.0/24 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gateway<span class="o">=</span>192.168.0.1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o <span class="nv">parent</span><span class="o">=</span>enp97s0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  plex_macvlan</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No compose, o Plex precisa estar em duas redes: a macvlan (pro IP 192.168.0.6) e a bridge default (pra outros containers como Seerr conseguirem se comunicar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.0.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mac_address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;02:42:c0:a8:00:06&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span>{}<span class="w">    </span><span class="c"># obrigatório — sem isso, Seerr não enxerga o Plex</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhe que quase me quebrou: o Plex guarda paths absolutos no banco de dados. Se o volume interno do container mudou de <code>/media</code> pra <code>/data</code>, o Plex não encontra mais nada. Tem que usar exatamente o mesmo mount target do compose antigo.</p>
<h3>Ollama com ROCm<span class="hx:absolute hx:-mt-20" id="ollama-com-rocm"></span>
    <a href="#ollama-com-rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Stack novo, não existia no servidor anterior:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ollama</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ollama/ollama:rocm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/kfd:/dev/kfd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">seccomp:unconfined</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;485&#34;</span><span class="w">   </span><span class="c"># render group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;488&#34;</span><span class="w">   </span><span class="c"># video group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">HSA_OVERRIDE_GFX_VERSION=11.5.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">PYTORCH_ROCM_ARCH=gfx1151</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KEEP_ALIVE=30m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_NUM_PARALLEL=4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_FLASH_ATTENTION=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KV_CACHE_TYPE=q8_0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/var/lib/ollama:/root/.ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;11434:11434&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>OLLAMA_FLASH_ATTENTION=1</code> ativa flash attention. <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> usa KV cache em 8-bit, cortando bandwidth necessária por token pela metade. São otimizações de performance gratuitas.</p>
<h3>Monitoramento (Grafana + Prometheus)<span class="hx:absolute hx:-mt-20" id="monitoramento-grafana--prometheus"></span>
    <a href="#monitoramento-grafana--prometheus" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Grafana usa named volume (<code>grafana_data</code>) que NÃO é incluído em backups normais de filesystem. Foi o motivo pelo qual perdi todos os dashboards na primeira tentativa. A solução é backup explícito do named volume:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># No servidor antigo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data:ro -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/grafana_data.tar.gz -C /data .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Transferir e restaurar no novo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s2">&#34;cd /data &amp;&amp; tar xzf /backup/grafana_data.tar.gz&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mesma coisa pro Portainer (<code>portainer_data</code>). Qualquer volume definido no bloco <code>volumes:</code> do compose sem host path precisa desse tratamento.</p>
<h3>Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="cloudflare-tunnel"></span>
    <a href="#cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> pra acessar todos os serviços fora de casa sem abrir portas no roteador. A migração foi a mais fácil: copiar o arquivo de credenciais JSON do túnel e o <code>config.yml</code>, atualizar os IPs de <code>.145</code> pra <code>.90</code>, e subir o container. O túnel mantém o mesmo ID, não precisa recriar DNS.</p>
<p>Os hostnames ficam em <code>config.yml</code>: portainer, grafana, plex, seerr, qbittorrent, syncthing, radarr, sonarr, bazarr, prowlarr, vault, gitea, kavita, e outros. Tudo acessível via <code>https://&lt;servico&gt;.example.com</code> de qualquer lugar.</p>
<h3>Gitea (registry de imagens)<span class="hx:absolute hx:-mt-20" id="gitea-registry-de-imagens"></span>
    <a href="#gitea-registry-de-imagens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> funciona como registry Docker privado na porta 3007. Os projetos Frank FBI, Frank Mega, Frank Yomik e Mila têm imagens Docker que são buildadas e pushadas pro Gitea. Pra funcionar, o <code>daemon.json</code> do Docker precisa de:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;192.168.0.90:3007&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O SSH do Gitea deu problema na migração: o app.ini antigo tinha <code>SSH_LISTEN_PORT=22</code>, mas o entrypoint do container também inicia sshd na porta 22. Conflito. Solução: <code>GITEA__server__SSH_LISTEN_PORT=2222</code> como variável de ambiente no compose.</p>
<h3>Todos os 49 containers rodando<span class="hx:absolute hx:-mt-20" id="todos-os-49-containers-rodando"></span>
    <a href="#todos-os-49-containers-rodando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O servidor migrado roda 49 containers em 15 stacks. O stack de media sozinho tem 10 containers (<a href="https://www.plex.tv/"target="_blank" rel="noopener">Plex</a>, <a href="https://github.com/Radarr/Radarr"target="_blank" rel="noopener">Radarr</a>, <a href="https://github.com/Sonarr/Sonarr"target="_blank" rel="noopener">Sonarr</a>, <a href="https://github.com/morpheus65535/bazarr"target="_blank" rel="noopener">Bazarr</a>, <a href="https://github.com/Prowlarr/Prowlarr"target="_blank" rel="noopener">Prowlarr</a>, <a href="https://github.com/qbittorrent/qBittorrent"target="_blank" rel="noopener">qBittorrent</a>, <a href="https://github.com/sabnzbd/sabnzbd"target="_blank" rel="noopener">SABnzbd</a>, <a href="https://github.com/Jackett/Jackett"target="_blank" rel="noopener">Jackett</a>, <a href="https://github.com/FlareSolverr/FlareSolverr"target="_blank" rel="noopener">FlareSolverr</a>, <a href="https://github.com/seerr/seerr"target="_blank" rel="noopener">Seerr</a>). Os projetos pessoais (<a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, Mila) somam mais 11. Monitoramento com <a href="https://github.com/grafana/grafana"target="_blank" rel="noopener">Grafana</a>, <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, node-exporter e <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>. Utilitários como <a href="https://github.com/portainer/portainer"target="_blank" rel="noopener">Portainer</a>, <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener">Vaultwarden</a>, <a href="https://github.com/syncthing/syncthing"target="_blank" rel="noopener">Syncthing</a>, <a href="https://github.com/causefx/Organizr"target="_blank" rel="noopener">Organizr</a>, <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>. <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> como registry Docker privado. <a href="https://github.com/immich-app/immich"target="_blank" rel="noopener">Immich</a> como Google Photos self-hosted. <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> pra manga com <a href="https://github.com/Kareadita/Kavita"target="_blank" rel="noopener">Kavita</a> como reader. <a href="https://github.com/ollama/ollama"target="_blank" rel="noopener">Ollama</a> com ROCm. E o <a href="https://github.com/bitcoin/bitcoin"target="_blank" rel="noopener">Bitcoin Core</a>/<a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> indexando a blockchain do NAS.</p>
<h2>Backups: duas camadas<span class="hx:absolute hx:-mt-20" id="backups-duas-camadas"></span>
    <a href="#backups-duas-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Camada 1: snapshots btrfs locais (snapper)<span class="hx:absolute hx:-mt-20" id="camada-1-snapshots-btrfs-locais-snapper"></span>
    <a href="#camada-1-snapshots-btrfs-locais-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>/var</code> vive numa partição btrfs de 3.7TB. O snapper cria snapshots automáticos: 7 diários + 1 semanal. São crash-consistent, não application-consistent (postgres pode ficar levemente inconsistente se tiver write pesado no momento do snapshot).</p>
<p>Pra recuperar um arquivo apagado acidentalmente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo snapper -c var list
</span></span><span class="line"><span class="cl">sudo cp /var/.snapshots/5/snapshot/opt/docker/media/radarr/appdata/config/radarr.db <span class="se">\
</span></span></span><span class="line"><span class="cl">        /var/opt/docker/media/radarr/appdata/config/radarr.db</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra rollback completo de um stack:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo docker compose -p media down
</span></span><span class="line"><span class="cl">sudo snapper -c var undochange 7..0 /var/opt/docker/media
</span></span><span class="line"><span class="cl">sudo docker compose -p media up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Camada 2: restic pro NAS (off-machine)<span class="hx:absolute hx:-mt-20" id="camada-2-restic-pro-nas-off-machine"></span>
    <a href="#camada-2-restic-pro-nas-off-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/restic/restic"target="_blank" rel="noopener">restic</a> roda toda noite às 3h, faz backup incremental pra <code>/var/mnt/terachad/homelab-backups/</code>. Retenção: 7 diários + 4 semanais. Deduplicação por conteúdo, então Plex config (19GB) e Gitea repos (12GB) transferem apenas deltas.</p>
<p>Antes do restic rodar, um <code>pg_dump</code> exporta os bancos postgres (Immich, Kaizoku). Os dumps vão pra <code>/tmp/homelab-db-dumps/</code> e são incluídos no backup.</p>
<p>O que NÃO é incluído no backup (re-downloadável): blockchain Bitcoin (785GB no NAS), imagens Docker (re-pullable), modelos Ollama (re-downloadable), cache do HuggingFace/EasyOCR, transcoding temporário do Plex.</p>
<p>Diretórios grandes e re-downloadáveis foram convertidos em subvolumes btrfs pra que o snapper os ignore: <code>/var/lib/ollama</code> e <code>/var/opt/docker/bitcoin/fulcrum/fulc2_db</code>.</p>
<h2>Tuning de performance<span class="hx:absolute hx:-mt-20" id="tuning-de-performance"></span>
    <a href="#tuning-de-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>btrfs com compressão zstd<span class="hx:absolute hx:-mt-20" id="btrfs-com-compressão-zstd"></span>
    <a href="#btrfs-com-compress%c3%a3o-zstd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Adicionei <code>compress=zstd:1</code> no fstab pra partição <code>/var</code>. O zstd nível 1 tem overhead de CPU quase zero em NVMe e comprime bem metadata Docker, JSON configs e logs. Dados incompressíveis (SQLite, postgres) são ignorados automaticamente pelo btrfs.</p>
<h3>zram swap<span class="hx:absolute hx:-mt-20" id="zram-swap"></span>
    <a href="#zram-swap" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com ~30GB de RAM disponível pro sistema (96GB vão pra VRAM), swap comprimido em memória ajuda. O zram cria um dispositivo de swap de ~15GB (ram/2) com compressão zstd, muito mais rápido que swap em disco.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/zram-generator.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[zram0]</span>
</span></span><span class="line"><span class="cl"><span class="na">zram-size</span> <span class="o">=</span> <span class="s">ram / 2</span>
</span></span><span class="line"><span class="cl"><span class="na">compression-algorithm</span> <span class="o">=</span> <span class="s">zstd</span>
</span></span><span class="line"><span class="cl"><span class="na">swap-priority</span> <span class="o">=</span> <span class="s">100</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>btrfs nodatacow em diretórios de banco de dados<span class="hx:absolute hx:-mt-20" id="btrfs-nodatacow-em-diretórios-de-banco-de-dados"></span>
    <a href="#btrfs-nodatacow-em-diret%c3%b3rios-de-banco-de-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copy-on-write + escrita aleatória de banco de dados = write amplification. Desabilitei CoW nos diretórios que guardam SQLite e postgres:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/gitea/data/gitea.db
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/immich/db/
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/media/radarr/appdata/config/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>CPU em modo performance<span class="hx:absolute hx:-mt-20" id="cpu-em-modo-performance"></span>
    <a href="#cpu-em-modo-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Num servidor headless, não faz sentido economizar energia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> performance <span class="p">|</span> sudo tee /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Persistido via systemd service <code>cpu-epp.service</code>.</p>
<h3>Docker shutdown fix<span class="hx:absolute hx:-mt-20" id="docker-shutdown-fix"></span>
    <a href="#docker-shutdown-fix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Problema que descobri: o Docker vem com <code>KillMode=process</code>, que significa que no shutdown do sistema, o systemd mata só o dockerd e deixa todos os <code>containerd-shim</code> (um por container, ~49 no meu caso) órfãos. O systemd-shutdown precisa caçar eles um a um depois que o journal já parou, causando um hang silencioso de vários minutos.</p>
<p>Fix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker.service.d/shutdown.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">KillMode</span><span class="o">=</span><span class="s">control-group</span>
</span></span><span class="line"><span class="cl"><span class="na">TimeoutStopSec</span><span class="o">=</span><span class="s">30</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Os problemas que encontramos<span class="hx:absolute hx:-mt-20" id="os-problemas-que-encontramos"></span>
    <a href="#os-problemas-que-encontramos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a tabela de problemas reais que tivemos durante a migração. Se você está planejando algo parecido, leia antes de começar:</p>
<table>
  <thead>
      <tr>
          <th>Problema</th>
          <th>Causa</th>
          <th>Solução</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ROCm vê só 15.5GB de VRAM</td>
          <td>Kernel TTM limita pages mesmo com BIOS em 96GB</td>
          <td>Adicionar <code>amdttm.pages_limit=25165824</code> no kernel cmdline</td>
      </tr>
      <tr>
          <td>Todos containers: permission denied em volumes</td>
          <td>SELinux <code>container_t</code> não escreve em paths sem label</td>
          <td><code>security_opt: label:disable</code> em todo serviço</td>
      </tr>
      <tr>
          <td>NFS com <code>:Z</code> falha silenciosamente</td>
          <td>NFS não suporta xattr do SELinux</td>
          <td>Nunca usar <code>:Z</code> em paths NFS</td>
      </tr>
      <tr>
          <td>SQLite quebra com <code>:Z</code></td>
          <td>Relabeling muda contexto, WAL mode falha</td>
          <td>Remover <code>:Z</code>, usar <code>label:disable</code></td>
      </tr>
      <tr>
          <td>Radarr/Sonarr mostraram tela de setup</td>
          <td>Backup em <code>appdata/config/</code> mas compose montava <code>appdata/</code></td>
          <td>Corrigir: <code>appdata/config:/config</code></td>
      </tr>
      <tr>
          <td>Grafana perdeu dashboards</td>
          <td>Named volume não incluído no backup de filesystem</td>
          <td>Backup explícito do named volume</td>
      </tr>
      <tr>
          <td>Plex não encontra mídia</td>
          <td>Path interno mudou de <code>/media</code> pra <code>/data</code></td>
          <td>Restaurar path original no compose</td>
      </tr>
      <tr>
          <td>Seerr não conecta no Plex</td>
          <td>macvlan isolada da bridge network</td>
          <td>Adicionar <code>default: {}</code> nas networks do Plex</td>
      </tr>
      <tr>
          <td>Fulcrum crash: &ldquo;option -b missing&rdquo;</td>
          <td>Env vars não suportadas pela imagem</td>
          <td>Usar flags CLI em <code>command:</code></td>
      </tr>
      <tr>
          <td>bitcoind rejeita RPC</td>
          <td>Bind em <code>::1</code> por padrão</td>
          <td>Adicionar <code>-rpcbind=0.0.0.0 -rpcallowip=172.0.0.0/8</code></td>
      </tr>
      <tr>
          <td>sdbootutil warning no transactional shell</td>
          <td>Deve rodar fora da transação</td>
          <td>Executar <code>sdbootutil update-all-entries</code> no shell normal</td>
      </tr>
      <tr>
          <td>Watchtower permission denied em docker.sock</td>
          <td>SELinux bloqueia acesso ao socket</td>
          <td><code>label:disable</code></td>
      </tr>
      <tr>
          <td>Gitea SSH crash</td>
          <td>Conflito: entrypoint sshd porta 22 + app porta 22</td>
          <td><code>GITEA__server__SSH_LISTEN_PORT=2222</code></td>
      </tr>
      <tr>
          <td>docker-compose não instalado com Docker</td>
          <td>Pacote openSUSE só instala o daemon</td>
          <td>Instalar binário standalone manualmente</td>
      </tr>
  </tbody>
</table>
<h2>O que dizer pro Claude Code antes de começar<span class="hx:absolute hx:-mt-20" id="o-que-dizer-pro-claude-code-antes-de-começar"></span>
    <a href="#o-que-dizer-pro-claude-code-antes-de-come%c3%a7ar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu fosse refazer a migração do zero, daria estas instruções pro Claude Code na primeira mensagem. Na ordem de importância:</p>
<p>Diga que SELinux está enforcing e que ele NÃO deve usar <code>:Z</code> em nenhum volume Docker, e sim <code>security_opt: label:disable</code> em todo serviço. Diga que <code>/var/mnt/terachad/</code> é mount NFS e que <code>:Z</code> nunca deve aparecer em paths NFS. Diga pra sempre olhar o compose original antes de reescrever e só mudar IPs, paths e nomes de container, sem inventar novos layouts de volume. Avise que named volumes precisam de backup explícito (Grafana, Portainer). Explique que Plex roda em macvlan e precisa de <code>default: {}</code> nas networks. Informe que a GPU é gfx1151, não suportada oficialmente, e que precisa de UMA 96GB no BIOS + kernel TTM params + <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code>. E diga que Bitcoin/Fulcrum não processam variáveis de ambiente, tudo vai como argumento no <code>command:</code>.</p>
<p>Essas instruções teriam evitado 80% dos problemas que encontramos.</p>
<h2>Layout final do servidor<span class="hx:absolute hx:-mt-20" id="layout-final-do-servidor"></span>
    <a href="#layout-final-do-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/var/opt/docker/
├── bitcoin/          (bitcoind &#43; fulcrum)
├── cloudflared/      (túnel Cloudflare)
├── frank_fbi/        (análise de fraude por email)
├── frank_mega/       (clone do Mega)
├── frank_yomik/      (tradução de mangás)
├── gitea/            (registry Docker)
├── immich/           (Google Photos self-hosted)
├── kaizoku/          (manga downloader &#43; reader)
├── media/            (Plex &#43; *arr stack)
├── mila/             (bot Discord)
├── monitor/          (Grafana &#43; Prometheus)
├── ollama/           (LLM local com ROCm)
├── rip/              (HandBrake)
└── utils/            (Portainer, Vaultwarden, Syncthing, etc.)

/var/mnt/terachad/    (NFS do Synology)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (overflow de modelos se disco local encher)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Aviso sobre usar IA pra administrar servidor<span class="hx:absolute hx:-mt-20" id="aviso-sobre-usar-ia-pra-administrar-servidor"></span>
    <a href="#aviso-sobre-usar-ia-pra-administrar-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu usei Claude Code pra acelerar a migração. Criou compose files, escreveu scripts de backup, configurou firewall, diagnosticou problemas de SELinux. Funcionou bem pro meu caso: home server, só eu uso, e eu estava revisando cada passo.</p>
<p>Mas tem armadilhas. O Claude não sabe que <code>:Z</code> quebra SQLite a menos que você diga. Ele não sabe que o Fulcrum não aceita env vars a menos que já tenha visto o Dockerfile. Ele vai inventar layouts de volume &ldquo;melhores&rdquo; que quebram o Plex porque o Plex guarda paths absolutos no banco de dados.</p>
<p>Se fosse produção real: não faça isso sem review. Cada compose file que o Claude gerar, leia inteiro antes de aplicar. Cada comando destrutivo (rollback, delete, recreate), confirme manualmente. E tenha backups testados antes de começar. O Claude é bom pra gerar a primeira versão e diagnosticar erros, mas as decisões de arquitetura e as validações de segurança são suas.</p>
<p>Os posts anteriores sobre o home server que podem dar contexto adicional:</p>
<ul>
<li><a href="/2024/04/03/meu-netflix-pessoal-com-docker-compose/">Meu &ldquo;Netflix Pessoal&rdquo; com Docker Compose</a></li>
<li><a href="/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/">Acessando meu Home Server com domínio de verdade</a></li>
<li><a href="/2025/09/10/protegendo-seu-home-server-com-cloudflare-zero-trust/">Protegendo seu Home Server com Cloudflare Zero Trust</a></li>
<li><a href="/2025/09/10/instalando-grafana-no-meu-home-server/">Instalando Grafana no meu Home Server</a></li>
<li><a href="/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Vaultwarden self-hosted</a></li>
</ul>
]]></content:encoded><category>homeserver</category><category>docker</category><category>opensuse</category><category>microos</category><category>claude-code</category><category>AI</category><category>vibe-coding</category></item><item><title>Review: Minisforum MS-S1 Max | AMD AI Max+ 395 com 96GB de VRAM</title><link>https://www.akitaonrails.com/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/</guid><pubDate>Tue, 31 Mar 2026 15:00:00 GMT</pubDate><description>&lt;p&gt;Quem acompanha meus posts sobre &lt;a href="https://www.akitaonrails.com/2024/04/03/meu-netflix-pessoal-com-docker-compose/"&gt;home server&lt;/a&gt; sabe que eu rodava tudo num Intel NUC Core i7 com 32GB de RAM. Funcionava. Mas com o crescimento dos modelos de IA open source, o NUC virou um gargalo. Sem GPU dedicada, qualquer inferência de LLM ia pra CPU e ficava inutilizável.&lt;/p&gt;
&lt;p&gt;Eu comprei um Minisforum MS-S1 Max com o novo chip AMD Ryzen AI Max+ 395 por um motivo específico: esse chip suporta até 128GB de RAM unificada, e eu posso alocar 96GB como VRAM pro iGPU. Isso me dá mais VRAM do que qualquer placa gamer comercial, incluindo a RTX 5090 (32GB). E isso muda o que eu consigo rodar localmente.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Quem acompanha meus posts sobre <a href="/2024/04/03/meu-netflix-pessoal-com-docker-compose/">home server</a> sabe que eu rodava tudo num Intel NUC Core i7 com 32GB de RAM. Funcionava. Mas com o crescimento dos modelos de IA open source, o NUC virou um gargalo. Sem GPU dedicada, qualquer inferência de LLM ia pra CPU e ficava inutilizável.</p>
<p>Eu comprei um Minisforum MS-S1 Max com o novo chip AMD Ryzen AI Max+ 395 por um motivo específico: esse chip suporta até 128GB de RAM unificada, e eu posso alocar 96GB como VRAM pro iGPU. Isso me dá mais VRAM do que qualquer placa gamer comercial, incluindo a RTX 5090 (32GB). E isso muda o que eu consigo rodar localmente.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-desk.jpg" alt="Minisforum MS-S1 Max na mesa"  loading="lazy" /></p>
<h2>Por que trocar o Intel NUC<span class="hx:absolute hx:-mt-20" id="por-que-trocar-o-intel-nuc"></span>
    <a href="#por-que-trocar-o-intel-nuc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O NUC serviu bem como servidor Docker por dois anos. Mas a limitação era clara: sem GPU com VRAM suficiente, eu não conseguia rodar LLMs localmente de forma usável. O <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, meu sistema de tradução automática de mangás, precisava de OCR via CPU (lento) e conectava remotamente no Ollama do meu desktop (AMD 7950X3D + RTX 5090) pra tradução. Funcionava, mas significava que meu desktop precisava estar ligado pro servidor funcionar.</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/FrankYomik/master/docs/sample_translate.png" alt="Frank Yomik - tradução automática de mangás"  loading="lazy" /></p>
<p>Com o Minisforum, o Frank Yomik roda inteiramente no servidor. O worker agora usa ROCm pra OCR com a iGPU, e o Ollama roda local com 96GB de VRAM. Zero dependência do desktop.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-nuc-compare.jpg" alt="Comparação: Intel NUC (esquerda) vs Minisforum MS-S1 Max (direita)"  loading="lazy" /></p>
<p>Na foto dá pra ter uma ideia do tamanho. O NUC é aquele cubinho pequeno na esquerda. O Minisforum é maior, mas ainda é um mini-PC. Cabe na prateleira do rack embaixo do Synology NAS sem problema.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-shelf.jpg" alt="Minisforum instalado na prateleira, ao lado do NAS"  loading="lazy" /></p>
<h2>As specs<span class="hx:absolute hx:-mt-20" id="as-specs"></span>
    <a href="#as-specs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-fastfetch.png" alt="fastfetch do Minisforum"  loading="lazy" /></p>
<p>O chip é o AMD Ryzen AI Max+ 395: 16 cores / 32 threads Zen 5, com iGPU Radeon 8060S integrada e 128GB de LPDDR5X unificada. No BIOS, eu configurei UMA Frame Buffer Size pra 96GB, o que deixa ~30GB de RAM pro sistema operacional e containers. Mais os parâmetros de kernel pra TTM (sem eles, o ROCm só enxerga 15.5GB mesmo com a alocação do BIOS).</p>
<p>O sistema operacional é openSUSE MicroOS (mais sobre isso no <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">próximo post</a>). O consumo de energia da máquina inteira fica abaixo de 100W, o que é absurdo pra quem está acostumado com GPUs dedicadas que puxam 450W+ sozinhas.</p>
<h2>Minisforum vs meu desktop: benchmarks<span class="hx:absolute hx:-mt-20" id="minisforum-vs-meu-desktop-benchmarks"></span>
    <a href="#minisforum-vs-meu-desktop-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu rodei um <a href="https://github.com/akitaonrails/homelab-docs/tree/master/benchmarks"target="_blank" rel="noopener">conjunto de benchmarks</a> comparando o Minisforum com meu desktop (AMD 7950X3D, 96GB DDR5, RTX 5090 32GB GDDR7). Os resultados são claros.</p>
<h3>CPU<span class="hx:absolute hx:-mt-20" id="cpu"></span>
    <a href="#cpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Teste</th>
          <th>7950X3D</th>
          <th>AI Max+ 395</th>
          <th>Vantagem</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prime sieve (single-core)</td>
          <td>0.021s</td>
          <td>0.018s</td>
          <td>Strix Halo +14%</td>
      </tr>
      <tr>
          <td>Float pi (single-core)</td>
          <td>1.335s</td>
          <td>1.706s</td>
          <td>7950X3D +28%</td>
      </tr>
      <tr>
          <td>Multi-core sieve (32 threads)</td>
          <td>0.181s</td>
          <td>0.118s</td>
          <td>Strix Halo +53%</td>
      </tr>
      <tr>
          <td>SHA-256 throughput</td>
          <td>2.714 MB/s</td>
          <td>2.488 MB/s</td>
          <td>7950X3D +9%</td>
      </tr>
      <tr>
          <td>AES-256-CBC throughput</td>
          <td>1.613 MB/s</td>
          <td>1.410 MB/s</td>
          <td>7950X3D +14%</td>
      </tr>
  </tbody>
</table>
<p>Resultados mistos. O AI Max+ 395 é melhor em paralelismo puro (sieve multi-core), provavelmente por conta da latência menor na arquitetura de memória unificada. O 7950X3D ganha em float e crypto por causa dos clocks mais altos e do 3D V-Cache.</p>
<h3>Inferência de LLMs (modelos que cabem nos dois)<span class="hx:absolute hx:-mt-20" id="inferência-de-llms-modelos-que-cabem-nos-dois"></span>
    <a href="#infer%c3%aancia-de-llms-modelos-que-cabem-nos-dois" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui é onde a coisa fica interessante. Pra modelos que cabem nos 32GB da RTX 5090, a comparação é puramente de bandwidth de memória:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Tamanho</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>Vantagem 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>9.1 GB</td>
          <td>155.1</td>
          <td>23.2</td>
          <td>6.7x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>9.3 GB</td>
          <td>138.9</td>
          <td>22.6</td>
          <td>6.1x</td>
      </tr>
      <tr>
          <td>phi4-reasoning</td>
          <td>11.1 GB</td>
          <td>130.2</td>
          <td>19.1</td>
          <td>6.8x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>20.2 GB</td>
          <td>66.9</td>
          <td>10.0</td>
          <td>6.7x</td>
      </tr>
  </tbody>
</table>
<p>A RTX 5090 é ~7x mais rápida. A explicação é simples: GDDR7 tem ~1.792 GB/s de bandwidth. LPDDR5X tem ~256 GB/s. A razão (7x) bate quase exatamente com a diferença de velocidade medida (6.7x). Inferência de LLM é um problema dominado por bandwidth de memória. Quem lê pesos mais rápido, gera tokens mais rápido.</p>
<h3>E o prompt processing?<span class="hx:absolute hx:-mt-20" id="e-o-prompt-processing"></span>
    <a href="#e-o-prompt-processing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>Vantagem 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>~1.933</td>
          <td>~212</td>
          <td>9.1x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>~1.474</td>
          <td>~155</td>
          <td>9.5x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>~767</td>
          <td>~68</td>
          <td>11.3x</td>
      </tr>
  </tbody>
</table>
<p>Prompt processing é ainda pior: 7-11x mais lento. Faz sentido, porque o prompt precisa ser processado inteiro antes de gerar o primeiro token, e é uma operação ainda mais bandwidth-intensiva.</p>
<h3>Onde o Strix Halo ganha: modelos grandes<span class="hx:absolute hx:-mt-20" id="onde-o-strix-halo-ganha-modelos-grandes"></span>
    <a href="#onde-o-strix-halo-ganha-modelos-grandes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agora vem a razão pela qual eu comprei esse PC. Modelos que não cabem na RTX 5090:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Tamanho</th>
          <th>Strix Halo (tok/s)</th>
          <th>Notas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gpt-oss:20b</td>
          <td>13.8 GB MXFP4</td>
          <td>48.9</td>
          <td>MoE, mais rápido que esperado</td>
      </tr>
      <tr>
          <td>qwen3.5:35b</td>
          <td>23.9 GB</td>
          <td>43.2</td>
          <td>MoE, apenas ~4B params ativos</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>51.7 GB</td>
          <td>29.5</td>
          <td>MoE, 50GB+</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81.4 GB Q4_K_M</td>
          <td>19.2</td>
          <td>122B params, MoE</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>59.9 GB</td>
          <td>17.9</td>
          <td>Full precision bf16</td>
      </tr>
      <tr>
          <td>qwen2.5:72b</td>
          <td>47.4 GB Q4_K_M</td>
          <td>4.5</td>
          <td>Dense 72B, bandwidth-limited</td>
      </tr>
  </tbody>
</table>
<p>O qwen3.5:122b com 81GB de pesos rodando a 19 tok/s. Num mini-PC. Isso simplesmente não é possível numa RTX 5090. Na placa da NVIDIA, esse modelo teria que fazer offload de camadas pra RAM do sistema, caindo pra 2-3 tok/s. Na prática, inutilizável.</p>
<p>A diferença entre modelos MoE e densos é brutal. O qwen3.5:35b roda a 43 tok/s porque, apesar de ter 35B de parâmetros totais, só ~4B ficam ativos por token. Um modelo denso de 72B como o qwen2.5:72b precisa ler 40GB+ de pesos por token, e a 256 GB/s de bandwidth, o máximo teórico é ~6.7 tok/s. Os 4.5 medidos representam ~67% de eficiência, que é o esperado pra iGPU (overhead de bus compartilhado e drivers).</p>
<h3>Resumo: quando usar cada máquina<span class="hx:absolute hx:-mt-20" id="resumo-quando-usar-cada-máquina"></span>
    <a href="#resumo-quando-usar-cada-m%c3%a1quina" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Caso de uso</th>
          <th>Melhor máquina</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chat/coding interativo (modelos &lt;32GB)</td>
          <td>RTX 5090 (6-7x mais rápido)</td>
      </tr>
      <tr>
          <td>Modelos grandes (50GB+)</td>
          <td>Strix Halo (única opção)</td>
      </tr>
      <tr>
          <td>Modelos densos 70B+</td>
          <td>Strix Halo (única opção)</td>
      </tr>
      <tr>
          <td>Full-precision bf16</td>
          <td>Strix Halo (única opção)</td>
      </tr>
      <tr>
          <td>Batch processing com contexto longo</td>
          <td>Strix Halo (mais VRAM pra KV cache)</td>
      </tr>
      <tr>
          <td>API serving com baixa latência</td>
          <td>RTX 5090 (sub-150ms TTFT)</td>
      </tr>
  </tbody>
</table>
<h3>Um bug de ROCm que ainda existe<span class="hx:absolute hx:-mt-20" id="um-bug-de-rocm-que-ainda-existe"></span>
    <a href="#um-bug-de-rocm-que-ainda-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nem tudo funciona. Modelos como deepseek-r1:70b, llama3.3:70b e llama4:scout crasham com um bug no ggml (<code>GGML_ASSERT(ggml_nbytes(src0) &lt;= INT_MAX) failed</code>). O tensor de embedding desses modelos excede 2GB e o kernel de cópia do ROCm usa inteiro de 32 bits pro tamanho. No CUDA (NVIDIA) já foi corrigido, mas no ROCm ainda não. Esperando o fix no Ollama 0.20.0+.</p>
<h2>LPDDR5X vs GDDR7: por que essa diferença<span class="hx:absolute hx:-mt-20" id="lpddr5x-vs-gddr7-por-que-essa-diferença"></span>
    <a href="#lpddr5x-vs-gddr7-por-que-essa-diferen%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A pergunta que vem é: por que a LPDDR5X é tão mais lenta?</p>
<p>GDDR7 é memória dedicada de GPU. Ela fica soldada na placa de vídeo, conectada por um barramento largo (384 ou 512 bits na RTX 5090) com clocks altos. O único trabalho dela é alimentar a GPU com dados. LPDDR5X é memória unificada que serve pra tudo: sistema operacional, aplicações, e GPU ao mesmo tempo. O barramento é mais estreito e compartilhado.</p>
<p>Na prática: GDDR7 entrega ~1.792 GB/s dedicados pra GPU. LPDDR5X entrega ~256 GB/s que ainda precisam ser divididos entre CPU e GPU. Inferência de LLM é basicamente &ldquo;leia todos os pesos do modelo da memória, multiplique pelo token atual, gere o próximo token, repita&rdquo;. Quem lê mais rápido, gera mais rápido. Não tem atalho.</p>
<p>A vantagem do Strix Halo não é velocidade. É capacidade. 96GB de VRAM num chip de 100W que custa uma fração de uma GPU profissional. A RTX 5090 é 7x mais rápida, mas trava em 32GB. Modelos que não cabem, não rodam.</p>
<h2>As alternativas: quem mais faz isso?<span class="hx:absolute hx:-mt-20" id="as-alternativas-quem-mais-faz-isso"></span>
    <a href="#as-alternativas-quem-mais-faz-isso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se 96GB não é suficiente ou se você quer mais velocidade, as opções são poucas.</p>
<p>O Framework Desktop usa o mesmo chip AI Max+ 395 com até 128GB de RAM. Mesma plataforma, mesma performance, mas com o diferencial de ser modular e reparável (é o Framework, afinal). Na prática é equivalente ao Minisforum em specs e preço.</p>
<p>Acima disso, a alternativa é o Mac Studio com M3 Ultra. O chip M3 Ultra suporta até 512GB de memória unificada, com bandwidth de ~819 GB/s (mais de 3x o Strix Halo). A Apple fabrica os chips de memória no package, então a latência e a bandwidth são superiores. Você poderia potencialmente alocar ~400GB como VRAM e rodar modelos que não cabem em lugar nenhum fora de servidores com GPUs profissionais.</p>
<p>O NVMe interno da Apple também é outro nível: ~7.4 GB/s de leitura sequencial no M3 Ultra, comparado com ~14 GB/s do Crucial T700 (PCIe 5.0). O T700 é mais rápido em throughput bruto, mas a latência do NVMe da Apple tende a ser menor em I/O aleatório por causa da integração com o SoC.</p>
<table>
  <thead>
      <tr>
          <th>Spec</th>
          <th>Minisforum MS-S1 Max</th>
          <th>Mac Studio M3 Ultra (max)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RAM máxima</td>
          <td>128 GB LPDDR5X</td>
          <td>512 GB unified</td>
      </tr>
      <tr>
          <td>VRAM alocável</td>
          <td>~96 GB</td>
          <td>~400 GB</td>
      </tr>
      <tr>
          <td>Memory bandwidth</td>
          <td>~256 GB/s</td>
          <td>~819 GB/s</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>Zen 5, 16C/32T</td>
          <td>Apple M3 Ultra, 32C</td>
      </tr>
      <tr>
          <td>GPU compute</td>
          <td>ROCm (gfx1151, experimental)</td>
          <td>Metal (mlx, mature)</td>
      </tr>
      <tr>
          <td>Consumo</td>
          <td>~100W</td>
          <td>~135W</td>
      </tr>
      <tr>
          <td>NVMe</td>
          <td>PCIe 5.0 (slot padrão)</td>
          <td>Custom Apple (~7.4 GB/s)</td>
      </tr>
      <tr>
          <td>Preço (EUA)</td>
          <td>~$1.500-2.000</td>
          <td>~$9.999 (512GB config)</td>
      </tr>
      <tr>
          <td>Preço estimado (Brasil)</td>
          <td>~R$ 12.000-15.000</td>
          <td>~R$ 110.000+ (importação)</td>
      </tr>
  </tbody>
</table>
<p>O preço no Brasil é o elefante na sala. A configuração máxima do Mac Studio custa $9.999 nos EUA. Com impostos de importação (~60% + ICMS estadual), passa dos R$ 110.000. O Minisforum com 128GB sai por R$ 12.000-15.000. A diferença de quase 8x no preço compra muita coisa.</p>
<p>Se você precisa de mais de 96GB de VRAM pra modelos realmente enormes (DeepSeek-V3 com 671B parâmetros cabe em ~400GB Q4, por exemplo), o Mac Studio com 512GB é a única opção consumer. A alternativa seria GPUs profissionais NVIDIA A6000 (48GB VRAM, ~$6.000 cada, e você precisaria de várias em NVLink). Pra tudo que cabe em 96GB, o Minisforum faz o trabalho por uma fração do custo.</p>
<h2>E projetos que prometem rodar LLMs grandes em GPUs pequenas?<span class="hx:absolute hx:-mt-20" id="e-projetos-que-prometem-rodar-llms-grandes-em-gpus-pequenas"></span>
    <a href="#e-projetos-que-prometem-rodar-llms-grandes-em-gpus-pequenas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Existe o conceito de &ldquo;layer offloading&rdquo; que projetos como llama.cpp já suportam. A ideia é: se o modelo não cabe inteiro na VRAM, mantém algumas camadas na GPU e o resto na RAM do sistema. A GPU processa as camadas que estão nela, transfere pro CPU processar o resto, e volta.</p>
<p>Na prática, não funciona bem. O gargalo é o PCIe: a velocidade de transferência entre RAM do sistema e VRAM da GPU é de ~32 GB/s (PCIe 5.0 x16). Cada token gerado precisa transferir dados ida e volta. O resultado é que você cai de 150 tok/s (tudo na VRAM) pra 2-8 tok/s (offload parcial). É lento demais pra uso interativo.</p>
<p>A VRAM é a limitação fundamental porque inferência de LLM é memory-bandwidth-bound, não compute-bound. A GPU tem compute sobrando. O que falta é capacidade de ler os pesos do modelo rápido o suficiente. Quando parte dos pesos está em RAM via PCIe, o pipeline inteiro espera pela transferência.</p>
<p>É por isso que memória unificada (como no Strix Halo ou no Apple Silicon) faz diferença. Não tem PCIe no meio. CPU e GPU acessam a mesma memória física. Os 256 GB/s do Strix Halo são lentos comparados com GDDR7, mas são 8x mais rápidos que ficar fazendo offload via PCIe.</p>
<h2>Avanços em otimização de LLMs (até 2026)<span class="hx:absolute hx:-mt-20" id="avanços-em-otimização-de-llms-até-2026"></span>
    <a href="#avan%c3%a7os-em-otimiza%c3%a7%c3%a3o-de-llms-at%c3%a9-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra entender por que alguns modelos rodam tão melhor que outros no Strix Halo, precisa entender o que mudou no ecossistema nos últimos dois anos.</p>
<h3>Mixture of Experts (MoE)<span class="hx:absolute hx:-mt-20" id="mixture-of-experts-moe"></span>
    <a href="#mixture-of-experts-moe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se você roda modelos locais, MoE é o avanço que mais importa. Um modelo MoE tem parâmetros totais altos (ex: 122B no qwen3.5:122b), mas ativa apenas uma fração deles por token (ex: ~4B). Os pesos inativos ficam na VRAM mas não são lidos a cada token, o que reduz drasticamente a bandwidth necessária.</p>
<p>Nos benchmarks do Strix Halo, modelos MoE rodam 3-10x mais rápido que modelos densos do mesmo tamanho. O qwen3.5:35b (MoE, ~4B ativos) roda a 43 tok/s enquanto o qwen2.5:72b (denso, 72B ativos) roda a 4.5 tok/s.</p>
<h3>DeepSeek e a otimização de treinamento<span class="hx:absolute hx:-mt-20" id="deepseek-e-a-otimização-de-treinamento"></span>
    <a href="#deepseek-e-a-otimiza%c3%a7%c3%a3o-de-treinamento" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O DeepSeek V3 (dezembro 2024) mostrou que era possível treinar modelos de 671B parâmetros com custo uma ordem de magnitude menor que o previsto. Eles combinaram MoE com quantização FP8 durante o treinamento (não só na inferência), treinamento multi-estágio com curriculum learning, e várias otimizações de comunicação entre GPUs. O impacto: todo mundo copiou. Qwen, GLM, MiniMax, todos adotaram variações dessa técnica.</p>
<h3>Quantização: de FP16 pra Q4 sem perder muito<span class="hx:absolute hx:-mt-20" id="quantização-de-fp16-pra-q4-sem-perder-muito"></span>
    <a href="#quantiza%c3%a7%c3%a3o-de-fp16-pra-q4-sem-perder-muito" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quantização comprime os pesos do modelo de 16 bits (FP16) pra formatos menores: 8 bits (Q8), 4 bits (Q4), ou até 2 bits. Um modelo de 70B que ocuparia ~140GB em FP16 cabe em ~40GB em Q4_K_M. A perda de qualidade existe, mas nos formatos modernos (GGUF Q4_K_M, AWQ, EXL2) é pequena o suficiente pra uso prático.</p>
<p>O GGUF (formato do llama.cpp) se tornou o padrão pra inferência local. AWQ e GPTQ são alternativas com calibração mais sofisticada, mas o ecossistema convergiu pro GGUF porque ele funciona em CPU, CUDA e ROCm sem recompilação.</p>
<h3>Destilação: modelos menores que sabem mais<span class="hx:absolute hx:-mt-20" id="destilação-modelos-menores-que-sabem-mais"></span>
    <a href="#destila%c3%a7%c3%a3o-modelos-menores-que-sabem-mais" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Destilação é treinar um modelo pequeno usando as respostas de um modelo grande como professor. O Phi-4 da Microsoft (14B) foi treinado com destilação do GPT-4 e compete com modelos de 70B em vários benchmarks. O Qwen3 fez o mesmo: o qwen3:14b é surpreendentemente capaz pro tamanho.</p>
<h3>Flash Attention e KV Cache otimizado<span class="hx:absolute hx:-mt-20" id="flash-attention-e-kv-cache-otimizado"></span>
    <a href="#flash-attention-e-kv-cache-otimizado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Flash Attention (Tri Dao, 2022) mudou como attention é computada: em vez de materializar a matriz inteira de attention na memória, processa em blocos mantendo os dados no SRAM on-chip da GPU, reduzindo consumo de memória de O(n²) pra O(n). Sem isso, contextos de 128K+ tokens seriam impraticáveis. Já passou pelas versões 2 e 3, com otimizações pra FP8 e operações assíncronas no H100. O PagedAttention (vLLM, UC Berkeley) fez o mesmo pro KV cache durante serving: aplica conceitos de memória virtual ao cache, eliminando fragmentação e melhorando throughput 2-4x.</p>
<p>No Ollama, eu configurei <code>OLLAMA_FLASH_ATTENTION=1</code> e <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> no servidor. O primeiro ativa flash attention, o segundo usa KV cache em 8-bit em vez de fp16, cortando a bandwidth necessária pela metade por token. São otimizações que custam zero em hardware e melhoram throughput mensurável.</p>
<h3>O que Qwen, Kimi, MiniMax e GLM estão fazendo<span class="hx:absolute hx:-mt-20" id="o-que-qwen-kimi-minimax-e-glm-estão-fazendo"></span>
    <a href="#o-que-qwen-kimi-minimax-e-glm-est%c3%a3o-fazendo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Qwen (Alibaba) tem sido consistentemente o melhor custo-benefício em modelos open source. O Qwen3:14b é denso e forte; o Qwen3.5:122b é MoE e roda surpreendentemente bem em 96GB. O GLM-4.7 (Zhipu AI) é notável por oferecer versões bf16 full precision que rodam inteiras em 96GB. O MiniMax experimentou com contextos longos (até 4M tokens). O Kimi (Moonshot AI) focou em context windows grandes com arquiteturas lineares.</p>
<h3>O que roda bem em 96GB de VRAM<span class="hx:absolute hx:-mt-20" id="o-que-roda-bem-em-96gb-de-vram"></span>
    <a href="#o-que-roda-bem-em-96gb-de-vram" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com 96GB no Strix Halo, os modelos que funcionam bem pra uso diário:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th>Tamanho</th>
          <th>tok/s</th>
          <th>Uso</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen3.5:35b</td>
          <td>24 GB</td>
          <td>43.2</td>
          <td>Uso geral, excelente</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>52 GB</td>
          <td>29.5</td>
          <td>Código, MoE</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81 GB</td>
          <td>19.2</td>
          <td>Pesado mas usável</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>60 GB</td>
          <td>17.9</td>
          <td>Full precision</td>
      </tr>
      <tr>
          <td>qwen2.5-coder:32b</td>
          <td>20 GB</td>
          <td>10.2</td>
          <td>Código, denso</td>
      </tr>
      <tr>
          <td>deepseek-r1:32b</td>
          <td>20 GB</td>
          <td>7.4</td>
          <td>Raciocínio</td>
      </tr>
  </tbody>
</table>
<p>Modelos densos de 70B+ (deepseek-r1:70b, llama3.3:70b) ainda estão bloqueados pelo bug do ROCm que mencionei. Quando for corrigido, devem rodar a ~4-6 tok/s, usáveis pra batch mas não pra chat interativo.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu comprei o Minisforum pra rodar modelos que não cabem em GPU gamer nenhuma. Pra isso, funciona. Não é rápido. 19 tok/s num modelo de 122B não é a experiência que você tem com Claude ou ChatGPT. Mas é local, é privado, e roda na minha prateleira consumindo menos energia que uma lâmpada antiga.</p>
<p>Pra quem pergunta sobre o Mac Studio: se você tem orçamento, é a melhor máquina pra rodar LLMs locais. 512GB de memória unificada, 819 GB/s de bandwidth, ecossistema Metal/mlx maduro. Dá pra rodar DeepSeek-V3 inteiro em Q4. Mas no Brasil, com importação, passa dos R$ 110 mil. O Minisforum com 128GB por R$ 12-15 mil é a opção realista.</p>
<p>E pra quem acha que dá pra contornar a limitação de VRAM com offloading de camadas: não dá. PCIe é lento demais. O modelo precisa caber inteiro na VRAM pra inferência ser usável. É o motivo pelo qual GPUs gamers com 32GB de GDDR7 ultra-rápida continuam limitadas em tamanho de modelo, e por que a memória unificada do Strix Halo e do Apple Silicon mudou a equação.</p>
<p>No <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">próximo post</a> eu conto como migrei todo o home server pro Minisforum usando Claude Code, os problemas que encontrei, e como o openSUSE MicroOS se comporta como sistema operacional de servidor Docker.</p>
]]></content:encoded><category>hardware</category><category>llm</category><category>homeserver</category><category>amd</category><category>AI</category><category>review</category></item><item><title>Ensinando a questionar notícias | Frank Investigator</title><link>https://www.akitaonrails.com/2026/03/27/ensinando-a-questionar-noticias-frank-investigator/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/27/ensinando-a-questionar-noticias-frank-investigator/</guid><pubDate>Fri, 27 Mar 2026 10:00:00 GMT</pubDate><description>&lt;p&gt;Aviso: o &lt;a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener"&gt;Frank Investigator&lt;/a&gt; é um projeto experimental, em desenvolvimento ativo, e não pretende ser a palavra final sobre nenhuma matéria analisada. Ele não diz o que é verdade ou mentira. O que ele faz é perguntar o que o artigo se recusou a perguntar, identificar padrões retóricos conhecidos, e buscar fontes externas que o autor omitiu. Se você quiser ajudar, contribua no &lt;a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener"&gt;GitHub&lt;/a&gt; ou mande feedback. Se quiser acompanhar os resultados, a newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The Makita Chronicles&lt;/a&gt; vai ter uma seção nova chamada &amp;ldquo;Notícias Duvidosas&amp;rdquo; onde vou publicar o resumo do investigator e o link pro relatório completo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Aviso: o <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">Frank Investigator</a> é um projeto experimental, em desenvolvimento ativo, e não pretende ser a palavra final sobre nenhuma matéria analisada. Ele não diz o que é verdade ou mentira. O que ele faz é perguntar o que o artigo se recusou a perguntar, identificar padrões retóricos conhecidos, e buscar fontes externas que o autor omitiu. Se você quiser ajudar, contribua no <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">GitHub</a> ou mande feedback. Se quiser acompanhar os resultados, a newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The Makita Chronicles</a> vai ter uma seção nova chamada &ldquo;Notícias Duvidosas&rdquo; onde vou publicar o resumo do investigator e o link pro relatório completo.</p>
<p>Dito isso, deixa eu explicar por que eu fiz isso.</p>
<h2>O problema com a mídia brasileira<span class="hx:absolute hx:-mt-20" id="o-problema-com-a-mídia-brasileira"></span>
    <a href="#o-problema-com-a-m%c3%addia-brasileira" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu cansei.</p>
<p>Cansei de abrir o jornal e ter que fazer ginástica mental pra separar informação de narrativa. Cansei de publicações como Folha de São Paulo, UOL, Carta Capital, Brasil 247, O Globo e várias outras que usam manchetes enganosas, omitem contexto de propósito, transpõem evidências de outros países sem nenhuma ressalva, e criam uma aparência de consenso entre veículos que estão todos dizendo a mesma coisa porque seguem a mesma pauta coordenada.</p>
<p>Isso não é teoria da conspiração. É padrão editorial verificável. E o pior: a maioria dos leitores não tem tempo nem ferramentas pra perceber. Você lê o título, lê os dois primeiros parágrafos, e segue com a impressão que o artigo plantou na sua cabeça.</p>
<h2>O que o Frank Investigator faz<span class="hx:absolute hx:-mt-20" id="o-que-o-frank-investigator-faz"></span>
    <a href="#o-que-o-frank-investigator-faz" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Você dá uma URL de artigo de notícia. O sistema busca o artigo com Chromium headless (pra passar por paywalls e cookie walls), extrai o conteúdo filtrando propaganda e sidebar, e decompõe o texto em afirmações verificáveis. Aí começa a análise de verdade: divergência entre manchete e corpo, falácias retóricas (falsa causalidade, apelo a autoridade, espantalho, bait-and-pivot), distorção de fontes, manipulação temporal, citação seletiva, authority laundering. Expande os links citados no artigo pra verificar se as fontes dizem o que o autor afirma. Avalia cada claim com consenso de 3 modelos de IA (Claude Sonnet 4.6, GPT-5.4, Gemini 3.1 Pro) via OpenRouter. Detecta lacunas contextuais, campanha coordenada entre veículos, e mede a proporção entre paixão e evidência. São 15 etapas ao todo.</p>
<p>O princípio central é &ldquo;Verdade Acima do Consenso&rdquo;: uma fonte primária (dado oficial, documento de governo, estudo acadêmico original) veta qualquer quantidade de fontes secundárias repetindo a mesma informação. Dez jornais repetindo a mesma coisa sem fonte primária continuam valendo zero.</p>
<p>Deixa eu mostrar cinco exemplos reais.</p>
<h2>Exemplo 1: O caso Noelia Castillo (BBC)<span class="hx:absolute hx:-mt-20" id="exemplo-1-o-caso-noelia-castillo-bbc"></span>
    <a href="#exemplo-1-o-caso-noelia-castillo-bbc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/noelia-original.png" alt="Artigo original na BBC"  loading="lazy" /></p>
<p>A <a href="https://www.bbc.com/portuguese/articles/clyxedlekleo"target="_blank" rel="noopener">BBC publicou</a> uma matéria com o título &ldquo;Morre Noelia Castillo: a luta de uma jovem de 25 anos na Justiça contra seu pai para receber a eutanásia na Espanha&rdquo;. À primeira vista parece uma história sobre direito à eutanásia e uma disputa familiar. Uma jovem tetraplégica que lutou contra a oposição do pai religioso pra exercer seu direito de morrer.</p>
<p>Só que ao comparar com outros veículos, como a <a href="https://veja.abril.com.br/comportamento/a-decisao-extrema-tomada-por-espabhola-que-ficou-paraplegica-apos-agressao-sexual/"target="_blank" rel="noopener">Veja</a>, aparecem fatos que a BBC omitiu quase completamente. E esses fatos mudam tudo.</p>
<p>Noelia foi retirada da família pelo governo espanhol aos 13 anos e colocada sob custódia do estado. Enquanto estava sob essa custódia, sofreu múltiplos estupros coletivos. A violência sexual resultou em danos psiquiátricos graves e um histórico de saúde mental que já somava 67% de grau de invalidez antes dos eventos de 2022. Quando tentou suicídio em outubro de 2022, jogando-se do quinto andar de um prédio, ficou paraplégica. O grau de invalidez subiu pra 74%.</p>
<p>O pedido de eutanásia foi aprovado pela Comissão de Garantia e Avaliação da Catalunha. A data marcada pro procedimento foi 2 de agosto de 2024, mas ficou suspensa por mais de 600 dias por causa dos recursos judiciais do pai. Cinco instâncias judiciais se pronunciaram. O Tribunal Constitucional descartou violação de direitos fundamentais. O Supremo Tribunal da Espanha recusou o recurso. O Tribunal Europeu de Direitos Humanos rejeitou o pedido de suspensão. Na sexta-feira, 26 de março de 2026, Noelia foi submetida à eutanásia no Hospital Residencial Sant Camil, na comarca catalã de Garraf.</p>
<p>Mas tem um detalhe que a Veja menciona e que é perturbador: Noelia teria manifestado dúvidas antes do procedimento. E o hospital teria acelerado o processo porque os órgãos já estavam comprometidos pra doação.</p>
<p>O <a href="https://investigator.themakitachronicles.com/investigations/e5a27e016c"target="_blank" rel="noopener">relatório do Frank Investigator</a> comparou a cobertura de vários veículos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/noelia-contexto.png" alt="Contexto do evento e fatos omitidos"  loading="lazy" /></p>
<p>A análise cruzada entre os artigos analisados mostrou que alguns veículos, como a BBC, omitiram fatos que alteram a interpretação do caso inteiro. Outros, como a Veja, trouxeram o contexto completo. O episódio de violência sexual coletiva de outubro de 2022, a investigação criminal, o histórico psiquiátrico desde os 13 anos, tudo isso aparece em algumas coberturas mas está completamente ausente em outras. E entre os veículos que omitiram, nenhum tocou na questão ética de que o fundamento físico pro pedido de eutanásia deriva de uma tentativa de suicídio.</p>
<p>O enquadramento convergente entre os veículos é de &ldquo;batalha judicial&rdquo;, &ldquo;morte que pediu&rdquo;, &ldquo;deixar de sofrer&rdquo;, &ldquo;partir em paz&rdquo;, suavizando o caráter definitivo do procedimento e posicionando Noelia como protagonista heroica e o pai como antagonista obstrutivo. O pai, Gerônimo Castillo, e seus Advogados Cristãos são qualificados como &ldquo;ultracatólicos&rdquo; ou &ldquo;ultraconservadores&rdquo; sem fonte independente que sustente essa classificação editorial.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/noelia-coordenada.png" alt="Análise de narrativa coordenada - Noelia"  loading="lazy" /></p>
<p>A coordenação narrativa ficou em 55%. Não parece ser coordenação ativa entre redações, mas alinhamento editorial temático: todo mundo comprou a narrativa de autonomia individual sem questionar. O que eleva o score é a convergência de omissões. Nenhum veículo explica a distinção jurídica entre o TEDH &ldquo;autorizar ativamente&rdquo; a eutanásia e simplesmente recusar medidas cautelares provisórias pedidas pelo pai, que é o que de fato aconteceu. As implicações médicas e bioéticas de aprovar eutanásia em caso decorrente de tentativa de suicídio não aparecem em lugar nenhum. E qualquer voz crítica ao procedimento é automaticamente enquadrada como religiosa ou ideológica, nunca como médica ou jurídica.</p>
<p>É o tipo de caso onde a omissão é a manipulação. Os veículos que omitiram esses fatos não mentiram em nenhum momento. Mas ao enquadrar como &ldquo;disputa familiar sobre direito à eutanásia&rdquo; e omitir a cadeia causal (custódia do estado → estupros coletivos → dano psiquiátrico → tentativa de suicídio → paraplegia → eutanásia), o leitor fica com uma impressão radicalmente diferente da realidade. A comparação entre coberturas é exatamente o tipo de coisa que o investigator faz bem: expor o que cada veículo escolheu mostrar e o que escolheu esconder.</p>
<h2>Exemplo 2: &ldquo;Governo corta imposto de quase mil importados&rdquo; (UOL)<span class="hx:absolute hx:-mt-20" id="exemplo-2-governo-corta-imposto-de-quase-mil-importados-uol"></span>
    <a href="#exemplo-2-governo-corta-imposto-de-quase-mil-importados-uol" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/corte-original.png" alt="Artigo original no UOL"  loading="lazy" /></p>
<p>O <a href="https://economia.uol.com.br/noticias/redacao/2026/03/26/de-remedios-a-lupulo-governo-corta-imposto-de-mil-importados.ghtm"target="_blank" rel="noopener">UOL publicou</a> que o governo cortou impostos de importação de quase mil produtos, de remédios a lúpulo. Manchete positiva, tom de benefício ao consumidor. Vários outros veículos publicaram a mesma coisa com enquadramento parecido: o governo fez algo bom, preços vão cair.</p>
<p>Só que a <a href="https://www.gazetadopovo.com.br/economia/governo-aumenta-imposto-importacao-recua-fake-news/"target="_blank" rel="noopener">Gazeta do Povo</a> conta a outra metade da história. O governo não cortou impostos antigos. O que aconteceu foi: em algum momento anterior a fevereiro de 2025, o governo aumentou tarifas de importação de mais de 1.200 itens, uma medida que geraria R$ 14 bilhões em arrecadação estimada. Depois, sob pressão nas redes sociais e pressão popular público, recuou parcialmente. Zerou tarifas de uns 970 itens de capital, informática e telecomunicações. Reduziu impostos de 120 produtos de informática. E agora chama isso de &ldquo;corte de impostos&rdquo;.</p>
<p>Ou seja: aumentaram, tomaram pressão popular, voltaram atrás em parte, e reembalaram como se fosse uma concessão generosa. A maioria dos mais de 1.200 itens que tiveram tarifas elevadas continua com tarifas mais altas do que antes. Nenhum preço caiu pro consumidor final. Os preços voltaram ao que eram pra alguns produtos, e continuam mais altos pra maioria.</p>
<p>O <a href="https://investigator.themakitachronicles.com/investigations/f35bfe0176"target="_blank" rel="noopener">relatório do Frank Investigator</a> cruzou os artigos e expôs o que foi omitido.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/corte-contexto.png" alt="Contexto e fatos omitidos - corte de impostos"  loading="lazy" /></p>
<p>O que a comparação entre artigos expõe: nenhum dos veículos com enquadramento positivo menciona que a maioria dos 1.200+ itens com tarifas elevadas continua com tarifas mais altas após as duas rodadas de &ldquo;cortes&rdquo;. O impacto fiscal sobre a meta de arrecadação de R$ 14 bilhões prevista com os aumentos originais, ninguém calcula. Vozes da indústria nacional que pode ser prejudicada pela redução tarifária de concorrentes importados, nenhuma. Critérios objetivos do Gecex pra definir &ldquo;oferta insuficiente no mercado interno&rdquo;, não aparecem.</p>
<p>Os dois artigos analisados constroem o mesmo enquadramento positivo pro governo. O paradoxo de que os &ldquo;cortes&rdquo; são reversão parcial de aumentos feitos pelo mesmo governo no ano anterior fica enterrado ou simplesmente ausente.</p>
<p>É o tipo clássico de manipulação por reenquadramento. Ninguém mentiu. Mas &ldquo;governo corta imposto&rdquo; e &ldquo;governo recua de aumento após pressão popular&rdquo; descrevem o mesmo evento com impressões opostas. A escolha editorial de qual versão publicar já é a manipulação.</p>
<h2>Exemplo 3: &ldquo;Globo se desculpa por PowerPoint&rdquo; (Brasil 247)<span class="hx:absolute hx:-mt-20" id="exemplo-3-globo-se-desculpa-por-powerpoint-brasil-247"></span>
    <a href="#exemplo-3-globo-se-desculpa-por-powerpoint-brasil-247" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/globo-original.png" alt="Artigo original no Brasil 247"  loading="lazy" /></p>
<p>Esse caso explodiu nos últimos dias. A GloboNews mostrou um diagrama no programa Estúdio I conectando o presidente Lula, seus ministros e Daniel Vorcaro, dono do Banco Master, que está no centro de fraudes documentadas. A Globo depois se retratou publicamente, chamou o material de &ldquo;errôneo e incompleto&rdquo;, e demitiu uma editora.</p>
<p>O <a href="https://www.brasil247.com/brasil/globo-se-desculpa-por-powerpoint-que-tentou-jogar-o-caso-master-no-colo-de-lula"target="_blank" rel="noopener">Brasil 247</a> publicou uma matéria com o título &ldquo;Globo se desculpa por PowerPoint que tentou jogar o caso Master no colo de Lula&rdquo;.</p>
<p>O <a href="https://investigator.themakitachronicles.com/investigations/752d80653a"target="_blank" rel="noopener">relatório do Frank Investigator</a> expôs o que está acontecendo por baixo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/globo-resumo.png" alt="Resumo da investigação - Globo"  loading="lazy" /></p>
<p>O fato central é real: a Globo pediu desculpas e demitiu alguém. Mas o enquadramento do Brasil 247 vai muito além do que os fatos sustentam. O título diz &ldquo;tentou jogar no colo de Lula&rdquo; atribuindo intenção deliberada onde os documentos apontam pra falha editorial. A retratação da Globo falou em material &ldquo;errôneo e incompleto&rdquo;, não em tentativa de incriminar ninguém.</p>
<p>O que chama atenção nesse caso é a campanha coordenada. O investigator deu 62% de coordenação narrativa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/globo-coordenada.png" alt="Análise de narrativa coordenada - Globo"  loading="lazy" /></p>
<p>Vários veículos editorialmente alinhados ao governo usaram a expressão &ldquo;sem provas&rdquo; de forma convergente pra descrever a associação entre Lula e o caso Master. Todos focaram no erro da Globo como ponto central da narrativa, em vez de investigar as conexões reais. Nenhum veículo mencionou quais outros nomes políticos foram excluídos do PowerPoint original. Nenhum investigou as conexões documentadas de Vorcaro com diferentes esferas do poder. O foco é meta-jornalístico: criticam a emissora em vez de cobrir o escândalo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/globo-retorica.png" alt="Análise retórica - Globo"  loading="lazy" /></p>
<p>As falácias detectadas: linguagem carregada (&ldquo;tentou jogar&rdquo;, &ldquo;sem provas&rdquo; usados pra enquadrar erro editorial como ataque político deliberado), falsa causalidade (a retratação da Globo não prova que as conexões são falsas), cherry-picking (destaca a omissão de nomes ligados ao governo Lula sem contextualizar quais outros nomes foram omitidos), e bait-and-pivot (usa o pedido de desculpas da Globo como gancho pra minimizar o escândalo do Banco Master).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/globo-lacunas.png" alt="Lacunas contextuais - Globo"  loading="lazy" /></p>
<p>E as perguntas que nenhum desses veículos fez: quais nomes de ministros do Supremo e políticos de outros partidos também foram excluídos do PowerPoint? O caso Master tem conexões documentadas com figuras do governo Lula, ou se restringem a governos anteriores? O Brasil 247, que publica esse artigo, tem alinhamento editorial declarado com o governo Lula? Qual foi a reação do Conselho de Ética jornalístico?</p>
<p>Confiança geral: 13%. O artigo não fabrica fatos. Mas seleciona, enquadra e omite de forma a construir uma narrativa que os dados não sustentam.</p>
<h2>Exemplo 4: &ldquo;Por que combustível caro é bom&rdquo; (Folha)<span class="hx:absolute hx:-mt-20" id="exemplo-4-por-que-combustível-caro-é-bom-folha"></span>
    <a href="#exemplo-4-por-que-combust%c3%advel-caro-%c3%a9-bom-folha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/combustivel-original.png" alt="Artigo original na Folha"  loading="lazy" /></p>
<p>O economista Bernardo Guimarães publicou uma <a href="https://www1.folha.uol.com.br/colunas/bernardo-guimaraes/2026/03/por-que-combustivel-caro-e-bom.shtml"target="_blank" rel="noopener">coluna na Folha</a> defendendo que combustível caro é bom pra sociedade porque estimula inovação em energia limpa. Ele cita artigos acadêmicos reais (Popp 2002, NBER) e tem credenciais acadêmicas verificáveis (doutorado em Yale, professor na FGV EESP). Parece sólido.</p>
<p>O <a href="https://investigator.themakitachronicles.com/investigations/e6cd2ac867"target="_blank" rel="noopener">relatório completo do Frank Investigator</a> mostra outro quadro.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/combustivel-summary.png" alt="Resumo da investigação - combustível"  loading="lazy" /></p>
<p>O artigo acerta ao citar estudos reais. Mas omite quem paga a conta. A coluna ignora completamente o impacto distributivo: populações de baixa renda, moradores de áreas periféricas e rurais, que dependem mais de veículos próprios e têm menos acesso a alternativas limpas. Pra um economista, ignorar efeitos distributivos é ou incompetência ou escolha editorial.</p>
<p>Tem um problema pior. As evidências empíricas que ele cita (patentes nos EUA, dados de veículos elétricos na Califórnia entre 2014-2017) são transpostas pro Brasil sem ressalva nenhuma. O Brasil tem uma matriz de etanol e infraestrutura flex-fuel que altera completamente o mecanismo causal. O artigo trata como se o consumidor brasileiro estivesse na mesma situação que o californiano, o que é falso.</p>
<p>E tem o contexto que o artigo menciona de passagem mas não desenvolve: a Guerra no Irã está fazendo os preços de combustível subirem no mundo todo. O Brasil deveria estar numa posição privilegiada por causa do pré-sal e do etanol. Mas décadas de má gestão e corrupção na Petrobras fizeram com que a gente pague o mesmo preço que o resto do mundo. Em vez de questionar isso, a coluna vende a ideia de que &ldquo;pelo menos vai estimular energia limpa&rdquo;. É uma racionalização de um problema que não deveria existir.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/combustivel-lacunas.png" alt="Lacunas contextuais - combustível"  loading="lazy" /></p>
<p>O investigator identificou 5 questões que o artigo se recusou a abordar, com 35% de completude contextual. As falácias detectadas incluem falso dilema (apresenta combustível caro como a única opção viável de política climática), cherry-picking (reconhece inelasticidade de curto prazo mas enfatiza apenas efeitos de longo prazo), e linguagem carregada (descreve alternativas como &ldquo;brincar de plantar uma mudinha&rdquo;).</p>
<p>Confiança geral: 25%. Não é desinformação fabricada. É opinião com seleção de evidências a favor da tese e omissão de contrapontos relevantes.</p>
<h2>Exemplo 5: &ldquo;Não existe cinema forte sem regulamentação do streaming&rdquo; (O Globo)<span class="hx:absolute hx:-mt-20" id="exemplo-5-não-existe-cinema-forte-sem-regulamentação-do-streaming-o-globo"></span>
    <a href="#exemplo-5-n%c3%a3o-existe-cinema-forte-sem-regulamenta%c3%a7%c3%a3o-do-streaming-o-globo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/cinema-original.png" alt="Artigo original no O Globo"  loading="lazy" /></p>
<p>Renata Magalhães, presidente da Academia Brasileira de Cinema, deu uma <a href="https://oglobo.globo.com/blogs/miriam-leitao/post/2026/03/renata-magalhaes-nao-existe-cinema-forte-sem-regulamentacao-do-streaming.ghtml"target="_blank" rel="noopener">entrevista na coluna da Miriam Leitão no O Globo</a> defendendo que regulamentar o streaming é condição necessária pra fortalecer o cinema brasileiro.</p>
<p>O <a href="https://investigator.themakitachronicles.com/investigations/7e4f5605c5"target="_blank" rel="noopener">relatório do Frank Investigator</a> encontrou problemas sérios.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/cinema-resumo.png" alt="Resumo da investigação - cinema"  loading="lazy" /></p>
<p>Primeiro: a afirmação central de que &ldquo;muitos filmes têm baixa audiência&rdquo; aparece sem nenhum dado empírico. Nenhum número, nenhuma série histórica. É afirmação de autoridade pura, sem base analítica.</p>
<p>Segundo: tem uma contradição interna que o artigo não resolve. O texto abre dizendo que o cinema brasileiro está &ldquo;em destaque internacional&rdquo; (prêmios, festivais). E logo em seguida argumenta que a ausência de regulamentação impede o fortalecimento da indústria. Mas se o cinema brasileiro já está ganhando prêmios internacionais sem essa regulamentação, o argumento de que a regulamentação é condição necessária cai por terra. O artigo não endereça essa contradição.</p>
<p>E é aqui que entra o elefante na sala que nenhum desses artigos menciona: as pessoas simplesmente não querem assistir a maioria desses filmes. O cinema brasileiro premiado internacionalmente é feito pra competir em Cannes e no Oscar, não pra lotar sala de cinema no Brasil. Em vez de se perguntar por que o público brasileiro não se interessa, a indústria prefere pedir regulamentação do streaming pra forçar plataformas a financiar e exibir conteúdo que não tem audiência espontânea. É o modelo clássico: usa dinheiro público e regulação pra manter viva uma indústria que não se sustenta no mercado.</p>
<p>A entrevistada é presidente da Academia Brasileira de Cinema. Tem interesse institucional direto na regulamentação. O artigo não apresenta nenhuma voz contrária e não discute os custos pro consumidor: aumento de preço de assinatura, redução de catálogo. Uma fonte só, com conflito de interesse declarado, sem contraponto.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/27/frank-investigator/cinema-coordenada.png" alt="Análise de narrativa coordenada - cinema"  loading="lazy" /></p>
<p>E aqui vem a campanha coordenada, com 55% de coordenação narrativa. Múltiplos veículos reproduzem o mesmo enquadramento emocional: cinema &ldquo;em risco&rdquo;, regulamentação como &ldquo;essencial&rdquo;. Nenhuma das fontes identificadas discute evidências empíricas internacionais comparáveis sobre a eficácia de cotas de conteúdo (a Europa tem experiências com resultados contraditórios). Nenhuma menciona os conflitos de interesse da Academia Brasileira de Cinema. O fato de que as produções brasileiras premiadas internacionalmente foram feitas sem a regulamentação proposta é omitido convergentemente em todos os veículos. Apenas um site isolado (targethd.net) mencionou impactos negativos pro consumidor.</p>
<p>Confiança geral: 9%. O artigo é advocacy editorial legítimo, mas com falhas analíticas que limitam seu valor informativo a quase zero.</p>
<h2>O que o investigator analisa<span class="hx:absolute hx:-mt-20" id="o-que-o-investigator-analisa"></span>
    <a href="#o-que-o-investigator-analisa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Os 5 exemplos acima mostram padrões diferentes, mas os critérios de análise são os mesmos.</p>
<p>O sinal mais forte de manipulação é a omissão. Não é o que o artigo diz que engana, é o que ele deixa de dizer. A análise de lacunas contextuais identifica as perguntas que o artigo deveria ter respondido e não respondeu, e busca contra-evidência pra cada uma. Nos exemplos acima, artigos que omitem a maior parte do contexto relevante não estão informando ninguém. E quando a comparação cruzada entre veículos mostra que uns cobriram os fatos e outros não, como no caso Noelia ou no corte de impostos, fica difícil argumentar que a omissão foi acidental.</p>
<p>Depois vem a detecção de campanha coordenada. Vários jornais cobrirem o mesmo assunto é normal. Todos usarem a mesma linguagem carregada, focarem nos mesmos pontos e omitirem os mesmos contrapontos ao mesmo tempo não é. O sinal mais forte de coordenação não é o que os veículos dizem em comum, mas o que omitem em comum.</p>
<p>Tem também o reenquadramento, que é mais sutil. No caso dos impostos, o governo aumentou tarifas, recuou sob pressão, e os veículos chamaram de &ldquo;corte&rdquo;. Ninguém mentiu tecnicamente, mas a escolha de enquadramento muda completamente a interpretação. Esse tipo de manipulação é mais difícil de detectar porque cada afirmação individual é defensável.</p>
<p>As falácias retóricas pegam construções específicas: falso dilema (&ldquo;ou regula ou o cinema morre&rdquo;), bait-and-pivot (abre com fato positivo e pivota pra narrativa de crise), linguagem carregada (&ldquo;sem provas&rdquo; usado pra atribuir intencionalidade). Cada falácia detectada vem com a citação exata do trecho e a explicação de por que aquela construção é problemática.</p>
<p>E tem o princípio que costura tudo: se 10 veículos repetem a mesma afirmação citando uns aos outros, o consenso de LLMs tem que refletir que a cadeia de evidência é circular, não que a afirmação é bem suportada. Volume de cobertura não é proxy pra verdade.</p>
<h2>Por que você não pode &ldquo;só perguntar pro ChatGPT&rdquo;<span class="hx:absolute hx:-mt-20" id="por-que-você-não-pode-só-perguntar-pro-chatgpt"></span>
    <a href="#por-que-voc%c3%aa-n%c3%a3o-pode-s%c3%b3-perguntar-pro-chatgpt" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A primeira reação de muita gente vai ser: &ldquo;por que eu preciso disso se eu posso colar o artigo no ChatGPT e pedir pra ele analisar?&rdquo;</p>
<p>Tenta. Pega um artigo político e pede pro ChatGPT criticar. Ele vai criticar. Pega o mesmo artigo e pede pra confirmar. Ele vai encontrar argumentos pra confirmar. O LLM não está buscando a verdade. Ele está prevendo qual resposta você mais provavelmente espera ouvir dado o enquadramento da sua pergunta. Se você pede &ldquo;analise os problemas desse artigo&rdquo;, o modelo vai achar problemas. Se você pede &ldquo;esse artigo está correto?&rdquo;, ele vai encontrar méritos. É viés de confirmação automatizado.</p>
<p>Tem outro problema. Os LLMs generalistas foram treinados pra ser agradáveis. A tendência sycophantic (de concordar com o usuário) é documentada em todos os modelos grandes. Se o seu histórico de conversa indica que você é de esquerda, o modelo tende a enquadrar as respostas de uma forma que agrade esse perfil. Se você é de direita, mesma coisa pro outro lado. Ele não está mentindo de propósito. Ele está otimizando pra satisfação do usuário, que é literalmente a métrica pela qual foi treinado via RLHF.</p>
<p>E o pior: LLMs alucinam. Se não têm evidência suficiente pra sustentar a resposta que acham que você quer ouvir, inventam. Fabricam citações e dados plausíveis, atribuem declarações a pessoas que nunca as fizeram. Se você pede pra ele criticar um artigo sobre combustível, ele pode inventar um estudo fictício que &ldquo;prova&rdquo; o contrário do artigo. Parece convincente. Mas não existe.</p>
<p>O Frank Investigator foi construído pra evitar exatamente esses problemas. A primeira decisão de design é que nenhum humano faz pergunta ao LLM. Não existe prompt aberto tipo &ldquo;analise esse artigo&rdquo;. Cada etapa do pipeline tem prompts estruturados que pedem análises específicas: &ldquo;liste as falácias retóricas neste trecho&rdquo;, &ldquo;identifique que informações contextuais estão ausentes&rdquo;, &ldquo;compare a manchete com o corpo do texto&rdquo;. O modelo não sabe se o operador concorda ou discorda do artigo, porque o operador não opina em nenhum momento. Isso elimina o viés de confirmação na raiz.</p>
<p>Pra lidar com alucinação, todo analisador que usa LLM inclui a instrução &ldquo;CRITICAL &ndash; NO HALLUCINATION: Only reference URLs, sources, claims, quotes, and data that are EXPLICITLY present in the input provided to you. Do not invent, guess, or fabricate any URL, source name, statistic, quote, or claim. If you cannot verify something from the provided text, mark it as unverifiable &ndash; never fill in details.&rdquo; Não elimina alucinação completamente, mas reduz muito. E como são 3 modelos de empresas diferentes (Anthropic, OpenAI, Google) respondendo as mesmas perguntas, quando um alucina os outros dois geralmente discordam. O consenso é ponderado por confiança, não por maioria simples. Se dois modelos dão &ldquo;supported&rdquo; com 70% de confiança e um dá &ldquo;mixed&rdquo; com 95%, o &ldquo;mixed&rdquo; pesa mais. Quanto mais distante a discordância, maior a penalização na confiança final. Se um modelo começa a dar respostas inconsistentes, ele é colocado em quarentena e os outros dois continuam.</p>
<p>Mas a salvaguarda que eu considero mais importante é o primary source veto. Se uma fonte primária (dado do IBGE, decisão judicial, estudo original) contradiz uma claim, a confiança é capada em 60% e o veredicto é forçado pra &ldquo;mixed&rdquo;, mesmo que os 3 LLMs digam &ldquo;supported&rdquo;. Dez artigos de jornal repetindo uma afirmação não superam um dado oficial que a contradiz. Na mesma linha, se 5 matérias &ldquo;confirmam&rdquo; uma claim mas todas são do mesmo grupo editorial (Folha/UOL, Globo/G1/Valor), o sistema sabe que são a mesma voz e reduz o peso. Volume não substitui independência.</p>
<p>Nada disso torna o sistema perfeito. Mas é categoricamente diferente de colar texto no ChatGPT e perguntar &ldquo;o que você acha?&rdquo;.</p>
<h2>Os números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>129</td>
      </tr>
      <tr>
          <td>Dias de trabalho ativo</td>
          <td>~9</td>
      </tr>
      <tr>
          <td>Linhas de Ruby (código)</td>
          <td>19.444</td>
      </tr>
      <tr>
          <td>Linhas de teste</td>
          <td>9.190</td>
      </tr>
      <tr>
          <td>Arquivos de teste</td>
          <td>108</td>
      </tr>
      <tr>
          <td>Total de linhas (código)</td>
          <td>24.301</td>
      </tr>
      <tr>
          <td>Services (analisadores + serviços)</td>
          <td>80</td>
      </tr>
      <tr>
          <td>Analisadores de desinformação</td>
          <td>15</td>
      </tr>
      <tr>
          <td>Modelos ActiveRecord</td>
          <td>14</td>
      </tr>
      <tr>
          <td>Background jobs</td>
          <td>19</td>
      </tr>
      <tr>
          <td>Migrações de banco</td>
          <td>31</td>
      </tr>
      <tr>
          <td>Etapas do pipeline</td>
          <td>15</td>
      </tr>
      <tr>
          <td>Modelos de LLM em consenso</td>
          <td>3</td>
      </tr>
      <tr>
          <td>Locales</td>
          <td>2 (en, pt-BR)</td>
      </tr>
  </tbody>
</table>
<p>Stack: Ruby 4.0.1, Rails 8.1.2, SQLite com WAL mode, Solid Queue (jobs dentro do Puma), Solid Cable (WebSockets), Tailwind CSS v4, Chromium headless via Ferrum CDP, deploy com Kamal pro GitHub Container Registry. AGPL-3.0.</p>
<h2>O processo de desenvolvimento<span class="hx:absolute hx:-mt-20" id="o-processo-de-desenvolvimento"></span>
    <a href="#o-processo-de-desenvolvimento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto tem 129 commits em 9 dias de trabalho ativo (11-16 e 25-27 de março). O primeiro dia foi o mais pesado: mais de 60 commits só em 11 de março, começando do zero e chegando num sistema funcional com extração de conteúdo, decomposição de claims, avaliação por LLM, e interface web com live updates via Turbo Streams.</p>
<p>Os commits contam a história. Começou com a fundação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>1e32d5a - Initial Frank Investigator foundation
564eb97 - Add recursive source crawling and RubyLLM scaffold
c8c5357 - Add Brazil source registry and authority connectors
3d30617 - Add U.S. authority profiles, source role modeling, and specialized connectors</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois vieram os analisadores de desinformação, um por um:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>a65fef7 - Add rhetorical fallacy analyzer for detecting narrative manipulation
5dcf99d - Add headline-body divergence detection and headline citation amplification
a113efd - Add smear campaign defense with circular citation and viral volume detection
56d501a - Add media ownership modeling, syndication detection, and independence analysis</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A fase de hardening contra ruído e falsos positivos foi a que mais consumiu tempo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>46e4bc5 - Add pre-fetch defenses: URL filtering, circuit breaker, fetch prioritization
168fea1 - Add post-fetch content gate, claim noise filter, and duplicate content skip
b2843d7 - Add paywall detection, pricing noise filter, and ofertas.* host rejection
b1b230d - Rewrite Chromium fetcher with Ferrum CDP for anti-bot evasion</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois veio a interface, deploy, e os analisadores mais avançados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>c5afcea - Replace custom CSS with Tailwind CSS v4 and rewrite all templates
95a8ec4 - Add Docker deployment with bin/deploy script
ccfacc9 - Add coordinated narrative detection across media outlets
ba3e2f4 - Add 6 new misinformation detection analyzers with cross-analysis
fda984b - Add LLM-generated investigation summary with quality assessment
a4ebe78 - Add contextual gap analysis to detect manipulation through omission</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E nos últimos commits, o consenso de 3 modelos simultâneos e otimização de testes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>5cc9c05 - Enable 3-model LLM consensus: Sonnet 4.6, GPT-4.1 Mini, Gemini 2.5 Pro
cdf5fb5 - Batch 5 content analyzers into single LLM call, add anti-hallucination
28c305e - Add WebMock stubs for LLM and web search — tests run in 1.4s (was 540s)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os testes que rodavam em 9 minutos agora rodam em 1.4 segundo com WebMock stubbing de todas as chamadas de LLM e web search. Isso fez diferença enorme na velocidade de iteração.</p>
<h2>Limitações<span class="hx:absolute hx:-mt-20" id="limitações"></span>
    <a href="#limita%c3%a7%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fact-checking de notícias é um problema difícil. Um artigo sozinho não contém tudo que é necessário pra uma análise completa. O autor escolheu o que incluir e o que omitir, e essa escolha já é o primeiro nível de manipulação. As fontes citadas dentro do artigo foram selecionadas pelo autor pra sustentar a narrativa dele, não pra dar uma visão equilibrada.</p>
<p>O Frank Investigator não diz o que é verdade e o que é mentira. O resultado é um relatório com pontos fortes e fracos, não um selo de &ldquo;verdadeiro&rdquo; ou &ldquo;falso&rdquo;. Mesmo com todas as salvaguardas que descrevi acima, a contra-evidência buscada automaticamente pode não ser a mais relevante. As falácias detectadas podem ter falsos positivos. A análise de campanha coordenada depende do que o web search retorna no momento da busca.</p>
<p>Use os relatórios como ponto de partida pra formar sua própria opinião, não como veredicto final.</p>
<p>O projeto é open source, AGPL-3.0. Se quiser contribuir, testar, reportar bugs ou sugerir melhorias: <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">GitHub</a>. Se quiser acompanhar as análises, a newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The Makita Chronicles</a> vai ter a seção &ldquo;Notícias Duvidosas&rdquo; com resumo e link pro relatório completo de cada investigação.</p>
]]></content:encoded><category>ruby</category><category>rails</category><category>ai</category><category>fact-checking</category><category>open-source</category><category>vibe-coding</category></item><item><title>Reescrevi o OpenClaw em Rust, funcionou? | FrankClaw</title><link>https://www.akitaonrails.com/2026/03/16/reescrevi-o-openclaw-em-rust-funcionou-frankclaw/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/16/reescrevi-o-openclaw-em-rust-funcionou-frankclaw/</guid><pubDate>Mon, 16 Mar 2026 08:00:00 GMT</pubDate><description>&lt;p&gt;Antes de tudo: o FrankClaw ainda está em alpha pesado. Funciona pra tarefas simples, mas não testei workflows complexos. Se você quer ajudar, abre Issues no &lt;a href="https://github.com/akitaonrails/frankclaw"target="_blank" rel="noopener"&gt;GitHub&lt;/a&gt; com o que encontrar. Tem muita coisa pra testar.&lt;/p&gt;
&lt;p&gt;Até &amp;ldquo;funciona&amp;rdquo;, mas esse projeto foi mais pelo exercício. Dito isso, deixa eu contar por que fiz isso.&lt;/p&gt;
&lt;h2&gt;O problema com o OpenClaw&lt;span class="hx:absolute hx:-mt-20" id="o-problema-com-o-openclaw"&gt;&lt;/span&gt;
&lt;a href="#o-problema-com-o-openclaw" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;O &lt;a href="https://github.com/openclaw/openclaw"target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt; é um gateway que conecta canais de mensagem (Telegram, Discord, Slack, WhatsApp, etc.) a provedores de IA (OpenAI, Anthropic, Ollama). Você configura, sobe o servidor, e pode conversar com LLMs direto pelo Telegram ou qualquer outro canal. É um projeto popular e com bastante atividade.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Antes de tudo: o FrankClaw ainda está em alpha pesado. Funciona pra tarefas simples, mas não testei workflows complexos. Se você quer ajudar, abre Issues no <a href="https://github.com/akitaonrails/frankclaw"target="_blank" rel="noopener">GitHub</a> com o que encontrar. Tem muita coisa pra testar.</p>
<p>Até &ldquo;funciona&rdquo;, mas esse projeto foi mais pelo exercício. Dito isso, deixa eu contar por que fiz isso.</p>
<h2>O problema com o OpenClaw<span class="hx:absolute hx:-mt-20" id="o-problema-com-o-openclaw"></span>
    <a href="#o-problema-com-o-openclaw" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://github.com/openclaw/openclaw"target="_blank" rel="noopener">OpenClaw</a> é um gateway que conecta canais de mensagem (Telegram, Discord, Slack, WhatsApp, etc.) a provedores de IA (OpenAI, Anthropic, Ollama). Você configura, sobe o servidor, e pode conversar com LLMs direto pelo Telegram ou qualquer outro canal. É um projeto popular e com bastante atividade.</p>
<p>Atividade demais, na verdade.</p>
<p>Eu fiz um clone depth-1 do repositório e rodei um <code>tokei</code>:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Arquivos TypeScript (sem testes)</td>
          <td>3.794</td>
      </tr>
      <tr>
          <td>Linhas de código TypeScript</td>
          <td>~1.247.000</td>
      </tr>
      <tr>
          <td>Arquivos de teste</td>
          <td>2.799</td>
      </tr>
      <tr>
          <td>Dependências (package.json raiz)</td>
          <td>73</td>
      </tr>
  </tbody>
</table>
<p>Mais de um milhão de linhas de TypeScript. Os 2.799 arquivos de teste parecem bastante em número absoluto, mas proporcionalmente ao tamanho do codebase, a cobertura é baixa. A maior parte do código está em 29 pacotes de um monorepo com 21 extensões.</p>
<p>Fui buscar mais commits pra entender o ritmo de desenvolvimento. Nos 100 commits que consegui puxar, todos caíram em apenas 2 dias (9 e 10 de março). ~50 commits por dia, de 42 contribuidores diferentes. Vibe coding ao extremo.</p>
<p>A conclusão é a que você imagina: volumes enormes de código gerado por IA sendo despejados num repositório a uma velocidade que impossibilita review humano sério. E isso me incomodou o suficiente pra ir investigar mais a fundo.</p>
<h2>A auditoria de segurança<span class="hx:absolute hx:-mt-20" id="a-auditoria-de-segurança"></span>
    <a href="#a-auditoria-de-seguran%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pedi pro Claude fazer uma auditoria de segurança completa no código do OpenClaw. O <a href="https://github.com/akitaonrails/frankclaw/blob/master/docs/OPENCLAW_SECURITY_AUDIT.md"target="_blank" rel="noopener">relatório</a> encontrou:</p>
<table>
  <thead>
      <tr>
          <th>Severidade</th>
          <th>Quantidade</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CRITICAL</td>
          <td>7</td>
      </tr>
      <tr>
          <td>HIGH</td>
          <td>9</td>
      </tr>
      <tr>
          <td>MEDIUM</td>
          <td>12</td>
      </tr>
      <tr>
          <td>LOW</td>
          <td>6</td>
      </tr>
      <tr>
          <td>INFO</td>
          <td>5</td>
      </tr>
  </tbody>
</table>
<p>Sete vulnerabilidades críticas. Deixa eu listar algumas:</p>
<ul>
<li>Timing side-channel na comparação de tokens &ndash; o <code>safeEqualSecret()</code> faz um <code>return</code> antecipado no type-check, permitindo que um atacante distinga tokens malformados de tokens errados medindo latência.</li>
<li><code>eval()</code> no browser tool &ndash; execução arbitrária de JavaScript sem sandbox.</li>
<li>Shell sem allowlist &ndash; qualquer tool pode executar qualquer comando no sistema.</li>
<li>Webhooks do Slack sem verificação de assinatura nenhuma.</li>
<li>Transcripts e config em texto puro no disco, sem criptografia.</li>
<li>Sem rate limiting efetivo &ndash; IPs podem ser spoofados se o operador configura trusted proxies de forma ampla.</li>
</ul>
<p>Essas são só as que o Claude encontrou numa varredura automatizada. Provavelmente tem mais.</p>
<p>Eu não vou rodar isso na minha máquina. Nem dentro de um container Docker. Um gateway que recebe webhooks da internet, executa comandos shell, se conecta a APIs de IA com suas chaves, e guarda histórico de conversas, tudo isso com 7 vulnerabilidades críticas conhecidas? Não, obrigado.</p>
<p>Então eu fiz o que qualquer desenvolvedor racional faria: decidi construir o meu.</p>
<h2>A primeira tentativa: Claude Code<span class="hx:absolute hx:-mt-20" id="a-primeira-tentativa-claude-code"></span>
    <a href="#a-primeira-tentativa-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comecei do jeito mais direto. Clonei o OpenClaw, apontei o Claude Code pro código, e pedi: &ldquo;reescreve isso em Rust.&rdquo;</p>
<p>Não funcionou. O codebase é grande demais. Mais de um milhão de linhas de TypeScript espalhadas em 29 pacotes. O Claude não consegue manter tudo em contexto. O resultado inicial foi muito incompleto: muitos tipos criados mas sem implementação, <code>todo!()</code> por toda parte, boilerplate demais e funcionalidade de menos.</p>
<p>Troquei pro Codex 5.4 pra testar. Mesma coisa: pedi pra analisar e reescrever. Melhorou um pouco em certos aspectos, mas o problema fundamental é o mesmo. Nenhuma IA hoje pega um projeto desse tamanho e reescreve inteiro de uma vez. O contexto não cabe.</p>
<h2>A técnica que funciona<span class="hx:absolute hx:-mt-20" id="a-técnica-que-funciona"></span>
    <a href="#a-t%c3%a9cnica-que-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O que funciona é ir devagar. Um passo de cada vez.</p>
<p>Pede pro Claude (ou Codex) analisar o código original em etapas. Faz um plano longo detalhando cada feature. Depois implementa uma feature por vez em Rust, com testes, faz commit, e repete. É tedioso, mas é o que produz código que compila e funciona de verdade.</p>
<p>O motivo é simples: o código original é tão massivo que nenhum agente de IA (nem vários em paralelo) consegue manter tudo em contexto ao mesmo tempo. Você precisa decidir o que importa, implementar aquilo, validar, e seguir pro próximo.</p>
<p>E precisa decidir o que cortar. O OpenClaw tem 21 extensões de canal: Google Chat, iMessage, IRC, Teams, Matrix, Mattermost, Nostr, Twitch&hellip; Eu não preciso de nenhuma dessas. Mantive os canais mainstream: Web, Telegram, Discord, Slack, Signal, WhatsApp e Email. TTS? Fora. Polls? Fora. WhatsApp Web via Baileys? Fora, uso a Cloud API oficial. São features que adicionam complexidade sem valor proporcional.</p>
<h2>Descobrindo o IronClaw<span class="hx:absolute hx:-mt-20" id="descobrindo-o-ironclaw"></span>
    <a href="#descobrindo-o-ironclaw" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No meio do desenvolvimento, descobri o <a href="https://github.com/nichochar/iron-claw"target="_blank" rel="noopener">IronClaw</a>, que se apresenta como &ldquo;OpenClaw em Rust&rdquo;. Ótimo, pensei. Vou ver o que eles fizeram.</p>
<p>Clonei o repositório e pedi pro Claude escrever um <a href="https://github.com/akitaonrails/frankclaw/blob/master/docs/IRONCLAW_COMPARISON.md"target="_blank" rel="noopener">relatório de comparação</a>. O IronClaw tem coisas boas. Adotei 12 features:</p>
<p>Circuit breaker com retry e backoff exponencial pra resiliência de providers LLM. Detecção de leak de credenciais no output. Cache de respostas LLM com SHA-256 da prompt. Cost tracking com budget guards (aviso em 80%, bloqueio em 100%). Extended thinking pra Claude 3.7+ e o1. MCP client pra tool servers externos. Lifecycle hooks em inbound, tool calls e outbound. Smart model routing que joga queries simples pra modelos mais baratos. Suporte a túneis (cloudflared, ngrok, tailscale). REPL interativo (<code>frankclaw chat</code>). Routines com event triggers além de cron. Job state machine com auto-repair.</p>
<p>Mas o IronClaw depende de PostgreSQL + pgvector, tem sandbox WASM (wasmtime adiciona ~10MB), e é parte do ecossistema NEAR AI. Eu quero um binário único com SQLite embarcado e zero dependências externas.</p>
<h2>O que o FrankClaw traz do OpenClaw<span class="hx:absolute hx:-mt-20" id="o-que-o-frankclaw-traz-do-openclaw"></span>
    <a href="#o-que-o-frankclaw-traz-do-openclaw" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O core do OpenClaw está lá: 7 canais de mensagem, multi-provider com failover, agent runtime, sistema de comandos, skills, subagents, browser automation via CDP, bash tool com allowlist, cron jobs, REPL interativo. Mais as 12 features do IronClaw que listei acima.</p>
<p>Mas vários pedaços precisaram ser reescritos de um jeito que o original não fez. A compactação de contexto, por exemplo, usa sliding window com estimativa de tokens, pruning de mensagens e repair automático de tool pairs que ficam órfãos quando o contexto é cortado. O failover entre providers agora é model-aware: se você pede um modelo Claude e o provider da vez é OpenAI, ele pula automaticamente em vez de dar erro. O canvas renderiza SVG, HTML e Markdown com detecção de conflitos de revisão. São coisas que no OpenClaw ou não existiam ou estavam pela metade.</p>
<p>Depois veio a fase de adicionar o que faltava pra paridade. O FrankClaw hoje tem 30+ LLM tools: <code>web_fetch</code> (SSRF-safe com HTML-to-text), <code>web_search</code> (Brave API), <code>file_read</code>/<code>file_write</code>/<code>file_edit</code> (sandboxed no workspace com proteção contra path traversal), <code>pdf_read</code>, <code>image_describe</code> (via modelos de vision), <code>audio_transcribe</code>, <code>sessions_list</code>/<code>sessions_history</code>/<code>sessions_delete</code>, <code>message_send</code>/<code>message_react</code>, <code>cron_list</code>/<code>cron_add</code>/<code>cron_remove</code>, <code>config_get</code> (auto-redação de secrets), <code>agents_list</code>, <code>memory_get</code>/<code>memory_search</code>, mais os browser tools (<code>browser_open</code>, <code>browser_extract</code>, <code>browser_snapshot</code>, <code>browser_click</code>, <code>browser_type</code>, <code>browser_wait</code>, <code>browser_press</code>, <code>browser_sessions</code>, <code>browser_close</code>, <code>browser_aria</code>).</p>
<p>Algumas adições que não existem no OpenClaw original: um sistema de memória/RAG com SQLite FTS5 e embeddings (OpenAI, Gemini, Voyage) que sincroniza arquivos do workspace automaticamente. Uma API compatível com OpenAI (<code>/v1/chat/completions</code> e <code>/v1/models</code>), então qualquer cliente que fala esse protocolo (Cursor, Continue, Open WebUI) pode usar o FrankClaw como backend sem adaptação. Uma TUI em <code>ratatui</code> pra quem prefere terminal. Aprovação interativa de tools destrutivos antes de executar.</p>
<p>Tem coisas menores que fazem diferença na prática. Você pode configurar múltiplas API keys por provider com round-robin e backoff automático, então se uma key estourar o rate limit, a próxima assume. O model catalog já sabe context windows e custos dos modelos OpenAI e Anthropic sem você ter que configurar. Extração de URLs de mensagens tem blocklist de IPs privados contra SSRF. O sistema de comandos aceita directives inline (<code>/think</code>, <code>/model</code>) além de aliases.</p>
<p>Do lado de operação: ACP (Agent Client Protocol) via JSON-RPC 2.0 sobre NDJSON pra quem quer integrar programaticamente. Sistema de plugins com manifests e lifecycle de enable/disable. i18n com 9 locales via <code>FRANKCLAW_LANG</code>. Workspace identity files (<code>SOUL.md</code>, <code>IDENTITY.md</code>) pra definir a personalidade do bot por projeto. Health monitor nos canais com auto-restart. WebSocket com ping keepalive que sobrevive timeouts de proxy e túnel. <code>frankclaw start/stop/status</code> pra quem quer rodar como daemon com PID tracking. E toda a configuração migrou de JSON pra TOML.</p>
<h2>Hardening: onde está o diferencial<span class="hx:absolute hx:-mt-20" id="hardening-onde-está-o-diferencial"></span>
    <a href="#hardening-onde-est%c3%a1-o-diferencial" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://github.com/akitaonrails/frankclaw/blob/master/docs/OPENCLAW_SECURITY_AUDIT.md"target="_blank" rel="noopener">relatório de auditoria</a> que rodamos no OpenClaw encontrou 7 vulnerabilidades críticas e 9 high. O FrankClaw resolve todas:</p>
<table>
  <thead>
      <tr>
          <th>Área</th>
          <th>OpenClaw</th>
          <th>FrankClaw</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Comparação de tokens</td>
          <td>SHA-256 + timingSafeEqual com early return que leaka timing</td>
          <td>Comparação constant-time byte a byte, sem early returns</td>
      </tr>
      <tr>
          <td>Shell execution</td>
          <td>Sem allowlist obrigatória</td>
          <td>Deny-all por padrão + binary allowlist + rejeição de metacaracteres + sandbox ai-jail opcional</td>
      </tr>
      <tr>
          <td>Browser tool</td>
          <td><code>eval()</code> sem sandbox</td>
          <td>CDP com timeout 15s, SSRF guard, crash recovery, ARIA inspection</td>
      </tr>
      <tr>
          <td>Webhook Slack</td>
          <td>Zero verificação de assinatura</td>
          <td>HMAC-SHA256 com proteção contra replay</td>
      </tr>
      <tr>
          <td>Webhook Discord</td>
          <td>Placeholder hardcoded</td>
          <td>Ed25519 com validação de timestamp</td>
      </tr>
      <tr>
          <td>Criptografia</td>
          <td>Plaintext no disco</td>
          <td>ChaCha20-Poly1305 em sessions e config</td>
      </tr>
      <tr>
          <td>Password hashing</td>
          <td>Sem autenticação por senha</td>
          <td>Argon2id (t=3, m=64MB, p=4)</td>
      </tr>
      <tr>
          <td>Permissões de arquivo</td>
          <td>0o644 (world-readable)</td>
          <td>0o600 (owner-only)</td>
      </tr>
      <tr>
          <td>Prompt injection</td>
          <td>Sanitização básica</td>
          <td>Unicode Cc/Cf stripping + boundary tags + limite de 2MB</td>
      </tr>
      <tr>
          <td>Malware scanning</td>
          <td>Nenhum</td>
          <td>VirusTotal opcional em uploads</td>
      </tr>
      <tr>
          <td>Validação de input</td>
          <td>Sem limites</td>
          <td>IDs de 255 bytes, session keys de 800 bytes, frames WS configuráveis</td>
      </tr>
      <tr>
          <td>SSRF</td>
          <td>Proteção parcial</td>
          <td>Blocklist completa (RFC 1918, loopback, CGNAT, link-local) + DNS rebinding defense</td>
      </tr>
      <tr>
          <td>Tool execution</td>
          <td>Sem confirmação do usuário</td>
          <td>Aprovação interativa pra tools mutating/destructive</td>
      </tr>
  </tbody>
</table>
<p>O FrankClaw compila com <code>#![forbid(unsafe_code)]</code> em todos os 13 crates. Zero blocos unsafe.</p>
<p>E a auditoria não parou no OpenClaw. Fizemos uma <a href="https://github.com/akitaonrails/frankclaw/blob/master/docs/AUDIT_PLAN.md"target="_blank" rel="noopener">auditoria por componente</a> em 14 fases comparando cada parte do FrankClaw contra o original: canais, providers, runtime, tools, sessions, crypto, cron, webhooks. Tudo documentado.</p>
<h2>Deploy: como instalar<span class="hx:absolute hx:-mt-20" id="deploy-como-instalar"></span>
    <a href="#deploy-como-instalar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O FrankClaw roda com Docker Compose. Três containers: gateway, Chromium headless (pra browser tools), e Cloudflare tunnel (pra receber webhooks).</p>
<h3>1. Clonar e configurar<span class="hx:absolute hx:-mt-20" id="1-clonar-e-configurar"></span>
    <a href="#1-clonar-e-configurar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/frankclaw.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> frankclaw
</span></span><span class="line"><span class="cl">cp .env.docker.example .env.docker</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Edite o <code>.env.docker</code> com suas chaves:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Providers de IA (preencha os que usar)</span>
</span></span><span class="line"><span class="cl"><span class="nv">OPENAI_API_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">ANTHROPIC_API_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Canais (preencha os que quiser usar)</span>
</span></span><span class="line"><span class="cl"><span class="nv">TELEGRAM_BOT_TOKEN</span><span class="o">=</span>         <span class="c1"># via @BotFather</span>
</span></span><span class="line"><span class="cl"><span class="nv">WHATSAPP_TOKEN</span><span class="o">=</span>             <span class="c1"># Meta Business Platform</span>
</span></span><span class="line"><span class="cl"><span class="nv">WHATSAPP_PHONE_ID</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">WHATSAPP_VERIFY_TOKEN</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">DISCORD_BOT_TOKEN</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">SLACK_BOT_TOKEN</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">SLACK_APP_TOKEN</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Embedding providers (só se usar memória/RAG)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># GEMINI_API_KEY=</span>
</span></span><span class="line"><span class="cl"><span class="c1"># VOYAGE_API_KEY=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Opcional: criptografia de sessions (recomendado)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Gere com: openssl rand -base64 32</span>
</span></span><span class="line"><span class="cl"><span class="nv">FRANKCLAW_MASTER_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Opcional: scan de malware em uploads</span>
</span></span><span class="line"><span class="cl"><span class="nv">VIRUSTOTAL_API_KEY</span><span class="o">=</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>2. Configurar o gateway<span class="hx:absolute hx:-mt-20" id="2-configurar-o-gateway"></span>
    <a href="#2-configurar-o-gateway" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O arquivo <code>frankclaw.toml</code> define agentes, modelos e canais. Use o wizard ou os exemplos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Gerar config base com canal web</span>
</span></span><span class="line"><span class="cl">cargo run -- onboard --channel web
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ou copiar dos exemplos</span>
</span></span><span class="line"><span class="cl">ls examples/channels/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra cada canal, o CLI tem templates prontos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cargo run -- config-example --channel telegram
</span></span><span class="line"><span class="cl">cargo run -- config-example --channel whatsapp</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>3. Cloudflare Tunnel (pra receber webhooks)<span class="hx:absolute hx:-mt-20" id="3-cloudflare-tunnel-pra-receber-webhooks"></span>
    <a href="#3-cloudflare-tunnel-pra-receber-webhooks" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se você vai usar canais que precisam de webhook (Telegram, Discord, Slack, WhatsApp), precisa de um túnel público. O Docker Compose já inclui o cloudflared:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copie suas credenciais do Cloudflare</span>
</span></span><span class="line"><span class="cl">cp docker/cloudflared/config.yml.example docker/cloudflared/config.yml
</span></span><span class="line"><span class="cl">cp ~/.cloudflared/&lt;tunnel-id&gt;.json docker/cloudflared/credentials.json
</span></span><span class="line"><span class="cl">cp ~/.cloudflared/cert.pem docker/cloudflared/cert.pem
</span></span><span class="line"><span class="cl"><span class="c1"># Edite config.yml com seu hostname</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>4. Subir<span class="hx:absolute hx:-mt-20" id="4-subir"></span>
    <a href="#4-subir" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O gateway sobe na porta 18789 (interna ao Docker). O cloudflared roteia o tráfego externo. O Chromium fica na rede interna pra browser tools.</p>
<p>Pra testar local sem Docker:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cargo run -- chat</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Abre o REPL interativo direto no terminal (agora também tem uma TUI em <code>ratatui</code> com dark mode e tabs). Sem gateway, sem webhook. Bom pra validar que o provider de IA está respondendo antes de configurar canais.</p>
<h3>Validação<span class="hx:absolute hx:-mt-20" id="validação"></span>
    <a href="#valida%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cargo run -- check     <span class="c1"># valida config</span>
</span></span><span class="line"><span class="cl">cargo run -- doctor    <span class="c1"># diagnóstico completo</span>
</span></span><span class="line"><span class="cl">cargo run -- audit     <span class="c1"># auditoria de segurança com severity ratings</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>audit</code> é o que eu mais gosto. Ele verifica se você tem criptografia habilitada, se as permissões de arquivo estão corretas, se os webhooks têm verificação de assinatura, se o bash tool está em deny-all. Sai com exit code non-zero se encontra problemas críticos, então dá pra colocar no CI.</p>
<h2>O processo de desenvolvimento<span class="hx:absolute hx:-mt-20" id="o-processo-de-desenvolvimento"></span>
    <a href="#o-processo-de-desenvolvimento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto tem 178 commits em ~5 dias de trabalho (10-16 de março). Quase 57 mil linhas de Rust em 120 arquivos, organizados em 13 crates.</p>
<p>Os commits contam a história. As primeiras dezenas foram scaffold: estrutura do workspace, tipos básicos, o gateway HTTP/WebSocket, primeira versão dos channel adapters. Código gerado em massa, muita coisa incompleta.</p>
<p>Depois começaram os adapters de canal, um por um:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>236aa1c - Minimal Discord channel adapter
fd67017 - Minimal Slack channel adapter
9f51373 - Minimal Signal channel adapter
1052e47 - WhatsApp channel webhook adapter
035f86e - Email channel adapter (IMAP inbound, SMTP outbound)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando os canais básicos estavam no lugar, veio a integração do IronClaw num commit grande:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>c87ab32 - IronClaw-derived features: circuit breaker, retry, leak detection, cache, cost tracking, extended thinking</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E aí veio o hardening. Essa foi a fase que eu mais tive que intervir manualmente, porque o Claude gera código funcional mas não pensa em vetores de ataque sozinho:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>db34198 - Prompt injection sanitization, external content wrapping, prompt size limit
5719b34 - Optional VirusTotal malware scanning for file uploads
ccd2b2b - Harden input validation across all user-facing entry points
aa918ee - Optional ai-jail sandbox for bash tool
2d7b1df - Security audit command with severity-rated findings
d12cc97 - 3-tier ToolRiskLevel system replacing binary browser mutation flag
21e0c91 - Timing-safe token comparison in WhatsApp, crypto audit tests
e240c1b - Webhook replay prevention with timestamp verification
876a78c - Gateway &amp; media: SSRF redirect validation, filename hardening</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>22 commits de security hardening. Mais 10 de auditoria por componente. Cada achado virou um commit com fix.</p>
<p>Depois vieram as auditorias específicas por canal, cada uma descobrindo edge cases diferentes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>43b085f - Discord audit: HELLO timeout, fatal close codes, message chunking
12c7cff - Telegram audit: caption overflow, parse fallback, edit idempotency
f515062 - WhatsApp audit: message type filtering, send error classification
3c42aff - Slack audit: fatal auth errors, send error classification</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Browser tools precisaram de atenção extra. Um headless Chrome que recebe URLs de um LLM é um vetor de ataque óbvio:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>3217a96 - Browser automation: CDP timeout, SSRF guard, session limits, crash recovery
d98a803 - Gate mutating browser tools behind operator approval
014f56e - Browser screenshot/ARIA tools for accessibility tree inspection</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Nos commits mais recentes, o projeto começou a divergir do OpenClaw:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>5d73c4d - OpenAI-compatible HTTP API (/v1/chat/completions, /v1/models)
d832c36 - Memory/RAG system with SQLite FTS5, embeddings, and file sync
2b05f47 - Interactive tool approval for mutating/destructive tools
49034eb - Web console: dark mode, 8 tabs, focus mode, tool sidebar
9f51a18 - TUI, Gemini/Voyage embeddings, plugin management, ACP protocol
3c0703b - Config migration from JSON to TOML
eb130ad - Channel health monitor with auto-restart and rate limiting
2c3ea7e - Workspace bootstrap files (SOUL.md, IDENTITY.md) to system prompt
9df61bf - Model-aware failover routing, canvas SVG rendering
c7ba108 - WebSocket ping keepalive and auto-reconnect to web console</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A API compatível com OpenAI é a que eu mais uso no dia a dia. Cursor, Continue, Open WebUI, qualquer coisa que fala o protocolo OpenAI consegue usar o FrankClaw como backend sem mexer em nada do lado do cliente.</p>
<h2>Os números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>178</td>
      </tr>
      <tr>
          <td>Dias de trabalho</td>
          <td>~5</td>
      </tr>
      <tr>
          <td>Linhas de Rust</td>
          <td>56.586</td>
      </tr>
      <tr>
          <td>Arquivos Rust</td>
          <td>120</td>
      </tr>
      <tr>
          <td>Crates</td>
          <td>13</td>
      </tr>
      <tr>
          <td>LLM tools</td>
          <td>30+</td>
      </tr>
      <tr>
          <td>Commits de security hardening</td>
          <td>22</td>
      </tr>
      <tr>
          <td>Commits de auditoria</td>
          <td>10</td>
      </tr>
      <tr>
          <td>Canais suportados</td>
          <td>7</td>
      </tr>
      <tr>
          <td>Providers de IA</td>
          <td>9 (OpenAI, Anthropic, Ollama, Google, OpenRouter, Copilot, Groq, Together, DeepSeek)</td>
      </tr>
      <tr>
          <td>Vulnerabilidades críticas do OpenClaw resolvidas</td>
          <td>7/7</td>
      </tr>
      <tr>
          <td>Vulnerabilidades high do OpenClaw resolvidas</td>
          <td>9/9</td>
      </tr>
      <tr>
          <td>Blocos unsafe no código</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
<p>Comparado ao OpenClaw:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>OpenClaw (TS)</th>
          <th>FrankClaw (Rust)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Linhas de código</td>
          <td>~1.247.000</td>
          <td>56.586</td>
      </tr>
      <tr>
          <td>Arquivos fonte</td>
          <td>3.794</td>
          <td>120</td>
      </tr>
      <tr>
          <td>Dependências runtime</td>
          <td>73</td>
          <td>~40 crates</td>
      </tr>
      <tr>
          <td>Canais</td>
          <td>28</td>
          <td>7</td>
      </tr>
      <tr>
          <td>Vulnerabilidades críticas</td>
          <td>7 conhecidas</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
<p>Os números não são diretamente comparáveis. O OpenClaw tem 21 extensões de canal que eu cortei, uma UI web mais completa, e features de nicho que não portei. Mas o core (gateway, canais mainstream, providers, runtime, sessions, tools, memória) está lá com 22x menos código.</p>
<h2>Como ajudar (beta testing)<span class="hx:absolute hx:-mt-20" id="como-ajudar-beta-testing"></span>
    <a href="#como-ajudar-beta-testing" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O FrankClaw funciona pra conversa básica via Web e Telegram (testei ambos). WhatsApp funciona pra mensagens simples. Discord, Slack, Signal e Email estão implementados mas não tiveram teste extensivo. Não fizemos nenhum workflow complicado ainda.</p>
<p>Se você quer testar: clone o repositório, suba com Docker Compose, configure pelo menos um canal (Telegram é o mais fácil) e tente usar normalmente. Mande mensagens, teste tool calls, tente quebrar o sistema. Abra Issues no GitHub com o que encontrar.</p>
<p>O que eu sei que precisa de mais olhos: workflows com tools (browser, bash, MCP), subagent orchestration, failover entre providers, session persistence com criptografia, smart routing entre modelos, scheduled jobs, o sistema de memória/RAG, e a API OpenAI-compatible. Basicamente tudo que vai além de &ldquo;manda mensagem, recebe resposta&rdquo;.</p>
<p>Não precisa ser desenvolvedor Rust. O valor maior está em usar o sistema de formas que eu não pensei e encontrar os edge cases que só aparecem com uso real.</p>
<p>O FrankClaw não substitui o OpenClaw hoje. O OpenClaw tem mais canais, mais features, mais gente trabalhando nele. Mas carrega o peso de mais de um milhão de linhas de TypeScript geradas a 50 commits por dia por dezenas de contribuidores, com vulnerabilidades críticas documentadas. O FrankClaw é a alternativa pra quem olha pra isso e pensa &ldquo;eu não vou rodar esse código na minha máquina&rdquo;.</p>
<p>Mas vou ser honesto: apesar de ter sido divertido construir, eu pessoalmente não sei se preciso disso. O FrankClaw é um gateway genérico, feito pra ser flexível, conectar qualquer canal a qualquer provider, com runtime de agentes, tools, subagents, jobs, hooks. É muita infraestrutura.</p>
<p>O que eu descobri nos últimos meses é que eu consigo construir bots customizados pra tarefas específicas muito mais rápido. Em 1 dia eu tenho um bot funcionando, focado no que eu preciso, sem carregar o peso de um framework genérico. Foi o que eu fiz com o <a href="/2026/02/20/discord-como-admin-panel-bastidores-do-the-m-akita-chronicles/">Marvin</a> no projeto da newsletter, por exemplo. Um bot de Discord feito sob medida, que faz exatamente o que eu preciso e nada mais.</p>
<p>Um gateway genérico como o FrankClaw faz mais sentido pra quem quer uma interface unificada entre vários canais de chat e vários modelos de IA sem programar. Se esse é o seu caso, experimenta. Se você é desenvolvedor e sabe o que quer, talvez um bot customizado te sirva melhor. Fica a seu critério.</p>
<p>O <a href="https://github.com/akitaonrails/frankclaw"target="_blank" rel="noopener">repositório está aqui</a>. AGPL-3.0.</p>
]]></content:encoded><category>rust</category><category>security</category><category>ai</category><category>open-source</category><category>vibe-coding</category></item></channel></rss>