<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Frank-Yomik | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/frank-yomik/</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, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/frank-yomik/index.xml" rel="self" type="application/rss+xml"/><item><title>Solução Completa de Mangá: Frank Manga+, Frank Yomik, extensão Prettify-Manga</title><link>https://akitaonrails.github.io/2026/05/30/manga-plus-shueisha-desktop-frank-manga-plus/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/05/30/manga-plus-shueisha-desktop-frank-manga-plus/</guid><pubDate>Sat, 30 May 2026 09:00:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/logo.png" alt="Logo do Frank Manga&amp;#43;: o nome estilizado em vermelho e branco sobre fundo preto." loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Eu sou assinante do app &lt;strong&gt;MANGA Plus by Shueisha&lt;/strong&gt; na Google Play Store do Android. Pra quem não conhece, é o canal oficial da Shueisha (editora japonesa por trás da Shonen Jump) pra ler os mangás dela legalmente, capítulo lançando semanalmente quase no mesmo dia da publicação no Japão. Por dois dólares por mês dá acesso ao catálogo inteiro, com tradução oficial em inglês.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/logo.png" alt="Logo do Frank Manga&#43;: o nome estilizado em vermelho e branco sobre fundo preto."  loading="lazy" /></p>
<p>Eu sou assinante do app <strong>MANGA Plus by Shueisha</strong> na Google Play Store do Android. Pra quem não conhece, é o canal oficial da Shueisha (editora japonesa por trás da Shonen Jump) pra ler os mangás dela legalmente, capítulo lançando semanalmente quase no mesmo dia da publicação no Japão. Por dois dólares por mês dá acesso ao catálogo inteiro, com tradução oficial em inglês.</p>
<p>Tenho um Samsung ZFold 7, que é uma das poucas exceções de celular que serve bem pra ler mangá e Kindle (já comentei sobre isso e sobre o <a href="/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/">Frank Yomik</a> no ranking final da maratona de IA). O Fold aberto dá uma tela maior que celular normal e ajuda muito. Mas tem dia que meu olho já está cansado de telinha pequena, e o que eu queria mesmo era ler num monitor OLED de 32 polegadas que está bem na minha frente, perfeito pra leituras longas.</p>
<p>O problema é que <strong>MANGA Plus não tem site oficial pra leitura</strong>. A assinatura está presa ao Android. Existe uma versão pra iOS também, mas web não tem. Tablet ajuda, mas nada bate o conforto de uma tela grande e estática quando você quer ler um arco inteiro de One Piece numa tacada só.</p>
<p>Pra contexto, ultimamente coloquei muita coisa em dia: finalmente fechei Chainsaw Man, Dan Da Dan, Jujutsu Kaisen, Kagurabachi, Akane-banashi, Sakamoto Days e, claro, One Piece. Tem muito mais que eu quero acompanhar, e o conforto do monitor grande virou um problema real pra resolver.</p>
<p>O foco começa no <strong>Frank Manga+</strong>, que resolve o buraco da Shueisha no desktop. No fim também falo de dois complementos do meu setup: <a href="#prettify-manga">Prettify Manga Reader</a>, pra transformar sites de fansub e Kindle Web em leitores decentes, e <a href="#frank-yomik">Frank Yomik no navegador</a>, pra furigana/tradução em Kindle Japan e Naver.</p>
<h2>Decompilando o app<span class="hx:absolute hx:-mt-20" id="decompilando-o-app"></span>
    <a href="#decompilando-o-app" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Resolvi atacar. Sentei com o Claude Code e baixamos o xapk oficial do MANGA Plus. Decompilamos pra fonte Java com <a href="https://github.com/skylot/jadx"target="_blank" rel="noopener">jadx</a> e fomos navegando até entender como o app se autentica com a API da Shueisha.</p>
<p>A conclusão é direta: a &ldquo;assinatura&rdquo; do usuário, do ponto de vista do app, é um <strong>deviceSecret</strong> de 32 caracteres hexadecimais guardado num arquivo XML simples dentro dos shared preferences do Android. O app gera esse segredo uma vez quando você loga, e a partir daí toda chamada à API (<code>jumpg-api.tokyo-cdn.com</code>) é assinada com ele. Sem DRM elaborado, sem token rotativo, sem nada disso. É um segredo único e estático por aparelho.</p>
<p>O detalhe é que <strong>extrair esse segredo do app dá trabalho</strong>, porque por padrão você não tem acesso ao <code>/data/data/jp.co.shueisha.mangaplus/shared_prefs/config.xml</code> num Android sem root. Existem três caminhos:</p>
<ol>
<li><strong>Celular rooteado</strong>: roda <code>adb shell su -c &quot;cat /data/data/jp.co.shueisha.mangaplus/shared_prefs/config.xml&quot;</code> e procura o <code>&lt;string name=&quot;secret&quot;&gt;...&lt;/string&gt;</code>.</li>
<li><strong>Emulador com root</strong>: instala o Android Studio, cria um AVD com Google Play x86_64, aplica o <a href="https://gitlab.com/newbit/rootAVD"target="_blank" rel="noopener">rootAVD</a> pra ter root via Magisk, loga na sua conta Google que tem a assinatura, instala o MANGA Plus pela Play Store, e roda o mesmo <code>adb shell</code> de cima.</li>
<li><strong>Captura de rede com mitmproxy</strong> (mais avançado): roda o emulador root, injeta o CA do mitmproxy via Conscrypt APEX, e intercepta o segredo na primeira chamada autenticada do app.</li>
</ol>
<p>Os três caminhos estão documentados em detalhe nos <a href="https://github.com/akitaonrails/frank_mangaplus/blob/main/docs/install.md"target="_blank" rel="noopener">docs/install.md</a> e <a href="https://github.com/akitaonrails/frank_mangaplus/blob/main/docs/debugging.md"target="_blank" rel="noopener">docs/debugging.md</a> do repositório. Não vou repetir aqui passo a passo porque é mais ferramenta do que técnica em si, e quem topa o trabalho vai querer ler o original. O importante é que <strong>funciona</strong>, e que basta fazer <strong>uma vez</strong>: depois de extraído, o segredo serve pro app desktop pelo tempo que durar sua assinatura.</p>
<p>Sim, é chato. Mas é o preço de ter um único ponto de configuração que destrava o resto.</p>
<h2>O app desktop<span class="hx:absolute hx:-mt-20" id="o-app-desktop"></span>
    <a href="#o-app-desktop" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Com o segredo na mão, o resto foi fácil. Montei o <strong>Frank Manga+</strong>, um app nativo em <strong>Rust + Tauri</strong>, com WebView SvelteKit no frontend e cliente Rust com <code>reqwest</code> no backend conversando direto com a API da Shueisha. Pra carregar imagens dos capítulos sem vazar URL nem ter dor de cabeça com CORS, criei um esquema custom <code>mpimg://</code> interceptado pelo Tauri, que baixa, descriptografa (a API entrega as páginas com XOR simples) e devolve pro WebView.</p>
<p>O app abre numa visão de biblioteca com busca:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/library.png" alt="Frank Manga&#43; aberto na aba Library/Search mostrando uma grade de capas de mangá: One Piece, Sakamoto Days, Kagurabachi, Jujutsu Kaisen, Akane-banashi, Chainsaw Man, Dan Da Dan, entre outros."  loading="lazy" /></p>
<p>Clicando num título você vai pra página de detalhe com a lista completa de capítulos e um botão &ldquo;Continue&rdquo; que retoma do último capítulo lido:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/title-detail.png" alt="Página de detalhe do Kagurabachi no Frank Manga&#43;: capa grande à esquerda, sinopse, e lista virtualizada de capítulos numerados à direita com botão Continue no topo."  loading="lazy" /></p>
<p>O leitor abre em página única, com snap por página e as zonas de clique no estilo mangá: clica na <strong>metade esquerda</strong> da página pra avançar e na <strong>direita</strong> pra voltar (sentido de leitura japonês, da direita pra esquerda). Quem prefere teclado tem seta, espaço, <code>j</code>/<code>k</code> e PageUp/PageDown, tudo funciona. Quando você chega no fim de um capítulo, o próximo já vem em prefetch e é anexado no scroll, então não precisa voltar pra lista de capítulos toda hora. E o resume é por capítulo: se você sai no meio, da próxima vez que abrir aquele capítulo cai exatamente na página onde parou. Pra séries gigantes como One Piece (1100+ capítulos), a lista é virtualizada, então abre instantâneo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/reader.png" alt="Leitor do Frank Manga&#43; em página única mostrando página 12 de 19 do capítulo de Kagurabachi: cena dramática em preto e branco do Chihiro empunhando a espada, com indicador de página no topo e barra de progresso no rodapé."  loading="lazy" /></p>
<p>Num monitor de 32 polegadas, página única desperdiça metade da tela. Por isso tem o <strong>modo página dupla</strong>: aperta <code>D</code> (ou clica no ícone de layout no cabeçalho) e o leitor junta as páginas que formam par: a dobra inteira preenche a tela do jeito que o autor desenhou. É como mangá foi feito pra ser lido.</p>
<p>E dá pra ver como isso importa quando compara com encadernação barata. Paperback ruim engole o meio da dobra, exatamente onde a arte deveria se conectar. No digital, quando o leitor junta direito, a cena respira:</p>
<table>
  <thead>
      <tr>
          <th>Frank Manga+ juntando a página dupla</th>
          <th>Paperback barato engolindo a dobra</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/reader-double-page.png" alt="Frank Manga&#43; em modo página dupla num monitor widescreen: uma cena dramática em preto e branco preenche a tela inteira, com uma personagem de maria-chiquinha chorando e os balões “I….” e “…WANT TO GET STRONGER.”."  loading="lazy" /></td>
          <td><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/kikoru-paperback-spread.jpeg" alt="Paperback de Kaiju No. 8 aberto numa página dupla da Kikoru, com a arte sendo engolida e distorcida pela lombada no centro."  loading="lazy" /></td>
      </tr>
  </tbody>
</table>
<p>O <code>D</code> cicla por três layouts: <strong>single</strong> (uma página por vez), <strong>double</strong> (pares sequenciais a partir da página 1) e <strong>double-cover</strong> (a primeira página de cada capítulo sozinha e o resto em pares, que é como mangá impresso encaderna a capa solta antes da primeira dobra). A escolha persiste entre capítulos e sessões, então você configura uma vez e esquece.</p>
<p>Tem também um filtro de leitura noturna. Aperta <code>F</code> (ou clica no ícone de lua no cabeçalho) e os brancos da página esquentam pro sépia. Branco de LCD castiga a vista no escuro, e isso suaviza sem achatar o contraste:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/reader-eye-protection.png" alt="Frank Manga&#43; com o filtro de leitura noturna ativo: uma página dupla em preto e branco ganha um tom âmbar quente, com vários quadros e balões de diálogo em inglês."  loading="lazy" /></p>
<p>O <code>F</code> cicla por quatro níveis: <strong>off → low → med → high → off</strong>. O botão fica âmbar e mostra de um a três pontinhos pro nível ativo, e a escolha fica salva no <code>localStorage</code> igual às outras preferências do leitor. Por baixo é um filtro CSS de <code>sepia + brightness + saturate</code> na pilha inteira de páginas: o sépia empurra a cor pro âmbar mas preserva a faixa de luminância, então preto continua preto e o contraste da arte fica intacto.</p>
<p>Estado de leitura (último capítulo, página corrente) fica no <code>localStorage</code> do WebView. Cache de imagens em <code>~/.cache/mangaplus-reader/</code>. O <code>deviceSecret</code> extraído fica em <code>~/.config/mangaplus-reader/secret</code> (Linux/macOS) ou <code>%APPDATA%\mangaplus-reader\secret</code> (Windows). Dá pra sobrescrever com a variável de ambiente <code>MANGAPLUS_SECRET</code> se preferir.</p>
<h2>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem build pronto pra todas as plataformas principais:</p>
<ul>
<li><strong>Linux</strong>: AppImage, <code>.deb</code>, <code>.rpm</code>, ou via AUR (<code>yay -S mangaplus-reader-bin</code>).</li>
<li><strong>macOS</strong>: <code>.dmg</code> separado pra Apple Silicon (aarch64) e Intel (x64).</li>
<li><strong>Windows</strong>: instalador <code>.exe</code>. Não está assinado, então o Windows SmartScreen vai reclamar — clica em &ldquo;More info → Run anyway&rdquo;.</li>
</ul>
<p>Todos os artefatos saem nos <a href="https://github.com/akitaonrails/frank_mangaplus/releases"target="_blank" rel="noopener">Releases do GitHub</a>.</p>
<h2>Status e contribuições<span class="hx:absolute hx:-mt-20" id="status-e-contribuições"></span>
    <a href="#status-e-contribui%c3%a7%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Isso é <strong>beta</strong>. Funciona pro meu uso (leio várias séries por semana sem dor de cabeça), mas tem features que faltam: filtros de gênero na busca, modo offline real (hoje guarda em cache só o que já foi lido), e o caminho de extração do segredo pra usuários menos técnicos.</p>
<p>Se você assina o MANGA Plus e topa testar, manda issue ou PR. O código é Rust + Svelte, MIT, contribuição bem-vinda: <a href="https://github.com/akitaonrails/frank_mangaplus"target="_blank" rel="noopener">github.com/akitaonrails/frank_mangaplus</a>.</p>
<p>Agora finalmente leio One Piece no monitor grande, sem precisar segurar celular ou ficar de pescoço torto. Já valeu o trabalho.</p>
<p><a id="prettify-manga"></a></p>
<h2>Bônus: Prettify Manga Reader pra sites de fansub<span class="hx:absolute hx:-mt-20" id="bônus-prettify-manga-reader-pra-sites-de-fansub"></span>
    <a href="#b%c3%b4nus-prettify-manga-reader-pra-sites-de-fansub" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Além do app desktop, mantenho uma extensão mais simples: <a href="https://github.com/akitaonrails/prettify-manga"target="_blank" rel="noopener">Prettify Manga Reader</a>. Ela não traduz nada. O objetivo é arrumar a leitura.</p>
<p>A maioria dos sites de fansub é feia, cheia de banner, e claramente pensada pra celular: uma página embaixo da outra, scroll vertical infinito, boa sorte. Isso é prático pra ler no telefone, mas mangá não foi desenhado como feed de Instagram. Mangá foi pensado pra virar página. Duas páginas abertas ao mesmo tempo. O ritmo de leitura, a virada, o respiro entre quadros, tudo considera esse par.</p>
<p>E vários autores usam isso pra fazer spread de duas páginas. <strong>Kaiju No. 8</strong> faz várias vezes. Se o site joga uma página embaixo da outra, você até lê os balões, mas perde a composição inteira. Você está vendo metade da arte de cada vez e achando que viu tudo.</p>
<p>O normal nesses sites é isso:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/prettify-manga-reader-before.webp" alt="Página típica de fansub antes de ativar a extensão, com a página de mangá cercada pelo layout original do site."  loading="lazy" /></p>
<p>O Prettify abre um overlay escuro, encaixa a arte na tela e junta páginas no centro quando está em <strong>Book</strong> ou <strong>Double</strong>. O spread volta a parecer uma página dupla de verdade:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/prettify-manga-reader-spread.webp" alt="Prettify Manga Reader mostrando um spread de duas páginas corretamente unido no centro, em tela cheia e fundo escuro."  loading="lazy" /></p>
<p>Ele tem três modos: <strong>Single</strong> (uma página por tela), <strong>Double</strong> (pares desde o começo) e <strong>Book</strong> (primeira página sozinha, depois pares, que é o jeito de encadernação). Abre em Book por padrão. Se a imagem já é horizontal, mantém como spread em tela cheia. Tem scroll snap pra cada página parar no lugar certo, atalho de teclado, controles pequenos de mouse, ajuda com <code>?</code>, e um cartão no fim do capítulo tentando achar próximo/anterior quando o site deixa isso claro.</p>
<p>Também tem filtro noturno em três níveis. Aperta <code>N</code> e ele esquenta a página com sepia, brilho e contraste, sem transformar o desenho num cinza morto. Página branca no monitor grande de noite é uma lanterna na cara. Isso resolve.</p>
<p>Pra instalar, vai no <a href="https://github.com/akitaonrails/prettify-manga"target="_blank" rel="noopener">GitHub do Prettify Manga Reader</a>, baixa o zip em <strong>Releases</strong>, descompacta numa pasta permanente, abre <code>chrome://extensions</code> ou <code>brave://extensions</code>, liga <strong>Developer mode</strong>, clica em <strong>Load unpacked</strong> e escolhe a pasta descompactada. Se quiser montar do fonte, clona o repo, roda <code>npm test</code>, depois <code>npm run package</code>, descompacta o zip gerado em <code>dist/</code> e carrega essa pasta.</p>
<p>No Kindle Web Reader, o Prettify faz menos de propósito. Em URLs tipo <code>read.amazon.com/manga/...</code>, <code>read.amazon.co.jp/manga/...</code>, <code>read.amazon.co.uk/manga/...</code> e domínios <code>read.kindle.*</code>, não tenta reconstruir o livro numa overlay própria. O leitor da Amazon já resolve conta, página e DRM; a extensão só injeta uma barrinha discreta no canto inferior direito e atalhos de teclado por cima do reader nativo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/prettify-manga-reader-kindle.webp" alt="Kindle Web Reader com a barra discreta do Prettify Manga Reader no canto inferior direito, mostrando o botão de Night ativo pra suavizar a página de mangá."  loading="lazy" /></p>
<p>Espaço, PageDown e setas avançam página; Shift+Space, PageUp e setas opostas voltam; Home/End vão pro começo ou fim quando o reader nativo aceita, com fallback de scroll; <code>N</code> cicla o filtro noturno. Não guarda credenciais da Amazon, não mexe em sessão, não precisa token no código. É só conforto visual e teclado em cima do Kindle Web que já existe.</p>
<p><a id="frank-yomik"></a></p>
<h2>Bônus 2: Frank Yomik agora também no desktop via extensão<span class="hx:absolute hx:-mt-20" id="bônus-2-frank-yomik-agora-também-no-desktop-via-extensão"></span>
    <a href="#b%c3%b4nus-2-frank-yomik-agora-tamb%c3%a9m-no-desktop-via-extens%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem leu o <a href="/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/">balanço da maratona de IA</a> lembra do <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, meu projeto self-hosted que pega uma página de mangá ou webtoon, detecta os balões com RT-DETR-v2, faz OCR, manda pro Ollama local (<code>qwen3:14b</code>) traduzir, e devolve a página com o texto traduzido no lugar. Já expliquei a história e os detalhes técnicos no post <a href="/2026/03/05/meu-primeiro-fracasso-com-vibe-code-e-como-consertei-frank-yomik/">Meu primeiro fracasso com Vibe Code e como consertei | Frank Yomik</a>. Aqui o ponto é o pedaço novo no desktop.</p>
<p>Originalmente tinha só cliente Flutter pra Android e Linux, que abria Kindle Japan e Naver Webtoon dentro de um WebView. Acontece que rodar app Flutter dentro de WebView no desktop pra ler Kindle é meio canhão pra mosca. No desktop você já tem Chromium aberto. O que faltava era uma extensão de navegador.</p>
<p>Nas últimas semanas adicionei isso. Extensão Manifest V3 pra Chromium/Brave/Edge, conversando com o mesmo servidor self-hosted do Yomik. Roda em <a href="https://read.amazon.co.jp"target="_blank" rel="noopener">read.amazon.co.jp</a>, <a href="https://read.kindle.co.jp"target="_blank" rel="noopener">read.kindle.co.jp</a>, <a href="https://comic.naver.com"target="_blank" rel="noopener">comic.naver.com</a> e na versão mobile do Naver. A página fica visualmente idêntica ao original, sem botão injetado nem HUD na cara do leitor. Toda configuração mora no popup:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/30/manga-plus/yomik-extension-popup.png" alt="Popup do Frank Yomik aberto sobre uma página do Kindle Japan mostrando Initial D em japonês: campos pra API base URL, auth token, checkboxes pra Kindle reader e Naver Webtoon, dropdown de manga pipeline em Furigana, target language English, e bloco de webtoon prefetch."  loading="lazy" /></p>
<p>O content script pega a imagem da página atual e manda pro servidor. O bearer token fica no service worker, então não vaza pro JS da página. Quando a tradução volta, a imagem é trocada na hora no lugar da original. Tem cache local pra não retraduzir página já vista, botão de &ldquo;force reprocess&rdquo; pra quando precisa reportar bug, e autosave de configuração. Detalhes no <a href="https://github.com/akitaonrails/FrankYomik/tree/main/extension"target="_blank" rel="noopener">README da extensão</a>, e o zip pra Load Unpacked sai nos Releases do repo.</p>
<p>Na prática, hoje o monitor de 32&quot; virou minha mesa de leitura. Frank Manga+ abre o catálogo oficial da Shueisha em inglês (One Piece, Chainsaw Man, e o que mais a Shonen Jump publicar). Quando é fansub ou Kindle Web e só quero conforto visual, o Prettify resolve reader, teclado e filtro noturno. Quando quero ler coisa japonesa sem tradução oficial, abro Kindle Japan numa aba e o Yomik manda furigana ou tradução LLM em cima da página. Pra webtoon coreano da Naver, mesma história, mesma extensão.</p>
<p>A parte boa é que cada projeto resolve um pedaço diferente e eles se completam. Frank Manga+ existe porque a Shueisha não tem site. Prettify deixa readers de navegador menos sofridos. O Yomik existe porque o resto do mangá que eu leio não tem tradução oficial decente em tempo hábil. Pra quem é doente de mangá igual eu, é o setup que eu queria há anos.</p>
]]></content:encoded><category>frank-yomik</category><category>frank</category><category>llms</category><category>inteligencia-artificial</category></item><item><title>Meu primeiro fracasso com Vibe Code e como Consertei | Frank Yomik</title><link>https://akitaonrails.github.io/2026/03/05/meu-primeiro-fracasso-com-vibe-code-e-como-consertei-frank-yomik/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/05/meu-primeiro-fracasso-com-vibe-code-e-como-consertei-frank-yomik/</guid><pubDate>Thu, 05 Mar 2026 08:00:00 -0300</pubDate><description>&lt;p&gt;Antes que alguém pergunte, todo o código está &lt;a href="https://github.com/akitaonrails/FrankYomik/"target="_blank" rel="noopener"&gt;neste repositório&lt;/a&gt;. E tem binários pré-compilados do client na &lt;a href="https://github.com/akitaonrails/FrankYomik/releases"target="_blank" rel="noopener"&gt;página de releases&lt;/a&gt;. Mas só o app não adianta porque precisa do componente servidor, que roda numa máquina sua (local ou cloud) com GPU de pelo menos 16GB de VRAM, e que você precisa configurar - eu não vou manter um server público, só um pessoal pro meu uso privado.&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_07-52-11.png" alt="kindle library" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Eu tenho uma coleção enorme de mangás comprados na Amazon.co.jp que leio pelo &lt;a href="read.amazon.co.jp"&gt;Kindle web&lt;/a&gt;. Mangás shounen normalmente têm furigana — aquele texto pequeno em hiragana ao lado dos kanjis — que me ajudam a ler, porque, apesar de ter estudado japonês, nunca fui treinado formalmente. Mas mangás para público adulto (seinen, não pornô) geralmente vêm sem furigana. São kanjis puros e minha leitura deles fica muito lenta.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Antes que alguém pergunte, todo o código está <a href="https://github.com/akitaonrails/FrankYomik/"target="_blank" rel="noopener">neste repositório</a>. E tem binários pré-compilados do client na <a href="https://github.com/akitaonrails/FrankYomik/releases"target="_blank" rel="noopener">página de releases</a>. Mas só o app não adianta porque precisa do componente servidor, que roda numa máquina sua (local ou cloud) com GPU de pelo menos 16GB de VRAM, e que você precisa configurar - eu não vou manter um server público, só um pessoal pro meu uso privado.</p>
<p>&ndash;</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_07-52-11.png" alt="kindle library"  loading="lazy" /></p>
<p>Eu tenho uma coleção enorme de mangás comprados na Amazon.co.jp que leio pelo <a href="read.amazon.co.jp">Kindle web</a>. Mangás shounen normalmente têm furigana — aquele texto pequeno em hiragana ao lado dos kanjis — que me ajudam a ler, porque, apesar de ter estudado japonês, nunca fui treinado formalmente. Mas mangás para público adulto (seinen, não pornô) geralmente vêm sem furigana. São kanjis puros e minha leitura deles fica muito lenta.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_07-53-26.png" alt="furigana"  loading="lazy" /></p>
<p>Faz anos que quero uma ferramenta que resolva isso. A ideia é simples: detectar os balões de fala numa página de mangá, extrair o texto com OCR, e ou adicionar furigana nos kanjis, ou traduzir direto pro inglês e renderizar de volta no balão. Parece fácil, certo?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_08-07-43.png" alt="no-furigana"  loading="lazy" />
*(kanji sem legenda/furigana)</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_08-11-21.png" alt="com furigana"  loading="lazy" />
*(com furigana injetado em tempo real)</p>
<p>Pois é, parecia. E por saber que parecia fácil, mas que não seria, eu nunca tive paciência pra fazer. Eu sei fazer os 80% iniciais de qualquer projeto. O problema são os 20% finais — aquela fase de experimentação, tweaking, ajuste fino, tratar edge cases — que consomem mais tempo que todo o resto junto. E num projeto de visão computacional, esses 20% são especialmente traiçoeiros.</p>
<p>Mas aí chegou a era do vibe coding. E eu pensei: talvez agora os 20% sejam viáveis. Iniciei o projeto no dia 24 de fevereiro de 2026, às 23:10 da noite. E este se tornou um exemplo de como é muito fácil fazer grandes volumes de código <strong>inútil</strong>.</p>
<h2>A Ideia Original: OpenCV e Heurísticas<span class="hx:absolute hx:-mt-20" id="a-ideia-original-opencv-e-heurísticas"></span>
    <a href="#a-ideia-original-opencv-e-heur%c3%adsticas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Meu plano original - o conceito que imaginei por anos - era: usar <a href="https://opencv.org/"target="_blank" rel="noopener">OpenCV</a> - que é uma biblioteca famosa e antiga de visão de computador - pra detectar os balões de fala. Balões de mangá são tipicamente áreas brancas com contorno preto dentro de painéis. Em teoria, basta fazer threshold da imagem pra pegar regiões brancas, encontrar contornos, filtrar por tamanho e formato, e pronto.</p>
<p>Em 24 horas eu já tinha uma prova de conceito funcional: detecção de balões, OCR com manga-ocr (modelo treinado especificamente em texto japonês de mangá), furigana com MeCab pra análise morfológica, tradução com Ollama rodando Qwen3:14b local, e renderização do texto de volta no balão. O commit inicial (<code>9169d73</code>) do dia 24/fev já fazia tudo isso.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen6-debug.jpg" alt="shounen6 debug"  loading="lazy" /></p>
<p>No dia seguinte, 25 de fevereiro, eu já estava estendendo o pipeline com pipeline de tradução de webtoons coreanos. 27 commits nesse dia. Tudo parecia estar fluindo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/057-debug.jpg" alt="webtoon debug"  loading="lazy" /></p>
<p>E aí começou o inferno.</p>
<h2>O Inferno dos Falsos Positivos<span class="hx:absolute hx:-mt-20" id="o-inferno-dos-falsos-positivos"></span>
    <a href="#o-inferno-dos-falsos-positivos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O problema com detecção de balões via OpenCV é que o manga não é um documento padronizado. Cada artista tem traço próprio, a qualidade de scan varia enormemente entre eras de impressão, e páginas coloridas precisam de parâmetros completamente diferentes de páginas P&amp;B. E a coisa que mais se parece com um balão de fala branco numa página de mangá é&hellip; <strong>um rosto</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-05_09-15-17.png" alt="false positive face"  loading="lazy" /></p>
<p>Rostos de personagens são áreas claras, relativamente arredondadas, com contorno escuro. Exatamente como balões de fala. E não importa quantos filtros você empilhe, sempre aparece um caso onde o rosto de um personagem com cabelo azul passa por todos os seus filtros, ou onde um balão legítimo é rejeitado porque tem formato incomum.</p>
<p>Olha como ficou meu detector de balões no pico da complexidade — 551 linhas com 7 camadas de filtros de falso-positivo (versão resumida):</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"># --- False positive filters ---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Edge density</span>
</span></span><span class="line"><span class="cl"><span class="n">edge_pixels</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">countNonZero</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">bitwise_and</span><span class="p">(</span><span class="n">edges</span><span class="p">,</span> <span class="n">edges</span><span class="p">,</span> <span class="n">mask</span><span class="o">=</span><span class="n">mask</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">edge_density</span> <span class="o">=</span> <span class="n">edge_pixels</span> <span class="o">/</span> <span class="n">area</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">edge_density</span> <span class="o">&gt;</span> <span class="n">max_edge_density</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Bright pixel ratio</span>
</span></span><span class="line"><span class="cl"><span class="n">bright_pixels</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">countNonZero</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">bitwise_and</span><span class="p">(</span><span class="n">bright_thresh</span><span class="p">,</span> <span class="n">mask</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">bright_ratio</span> <span class="o">=</span> <span class="n">bright_pixels</span> <span class="o">/</span> <span class="n">area</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">bright_ratio</span> <span class="o">&lt;</span> <span class="n">min_bright_ratio</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Mid-tone ratio</span>
</span></span><span class="line"><span class="cl"><span class="n">mid_mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">gray</span><span class="p">,</span> <span class="mi">80</span><span class="p">,</span> <span class="mi">220</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">mid_pixels</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">countNonZero</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">bitwise_and</span><span class="p">(</span><span class="n">mid_mask</span><span class="p">,</span> <span class="n">mask</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">mid_ratio</span> <span class="o">=</span> <span class="n">mid_pixels</span> <span class="o">/</span> <span class="n">area</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">mid_ratio</span> <span class="o">&gt;</span> <span class="n">max_mid_ratio</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Contour circularity</span>
</span></span><span class="line"><span class="cl"><span class="n">circularity</span> <span class="o">=</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">np</span><span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">area</span> <span class="o">/</span> <span class="p">(</span><span class="n">perimeter</span> <span class="o">*</span> <span class="n">perimeter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">circularity</span> <span class="o">&lt;</span> <span class="mf">0.10</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. Border darkness</span>
</span></span><span class="line"><span class="cl"><span class="n">border_mean</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">mean</span><span class="p">(</span><span class="n">gray</span><span class="p">,</span> <span class="n">mask</span><span class="o">=</span><span class="n">border_only</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">border_mean</span> <span class="o">&gt;</span> <span class="mi">160</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 6. Background uniformity (white_std)</span>
</span></span><span class="line"><span class="cl"><span class="n">white_pixels</span> <span class="o">=</span> <span class="n">gray</span><span class="p">[(</span><span class="n">mask</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">gray</span> <span class="o">&gt;</span> <span class="mi">200</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">std</span><span class="p">(</span><span class="n">white_pixels</span><span class="p">))</span> <span class="o">&gt;</span> <span class="mi">15</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 7. Dark content analysis (text strokes)</span>
</span></span><span class="line"><span class="cl"><span class="n">very_dark</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">sum</span><span class="p">((</span><span class="n">inner_mask</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">gray</span> <span class="o">&lt;</span> <span class="mi">60</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">dark_ratio_60</span> <span class="o">=</span> <span class="n">very_dark</span> <span class="o">/</span> <span class="n">inner_area</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">dark_ratio_60</span> <span class="o">&lt;</span> <span class="n">min_dark_ratio</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</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 um desses filtros foi adicionado em resposta a um falso-positivo específico. Edge density separava balões (que têm traços esparsos de texto) de rostos (que têm cabelo, olhos, nariz criando bordas densas). Bright pixel ratio verificava se a região era realmente branca. Circularity descartava formas muito irregulares. E por aí vai.</p>
<p>Mas o pior é que esses filtros interagiam entre si de formas imprevisíveis. Olha esse commit do dia 26/fev (<code>70c814a</code>):</p>
<blockquote>
  <p>&ldquo;Revert rect_dark, mid_ratio, and early-split changes that caused face FPs&rdquo;</p>

</blockquote>
<p>Eu tinha tentado relaxar dois thresholds — rect_dark de 0.10 pra 0.11, mid_ratio de 0.15 pra 0.16 — pra recuperar balões que estavam sendo perdidos. Resultado: rostos e regiões de corpo começaram a passar como falsos positivos em mangá do Adachi. Tive que reverter tudo.</p>
<p>Esse é o padrão que se repetiu durante dias: recuperar um balão perdido significava abrir porta pra falsos-positivos. Corrigir um falso-positivo significava perder um balão legítimo. Era um whack-a-mole infinito.</p>
<h2>Os Paliativos: CLAHE, Detecção por Borda, Watershed<span class="hx:absolute hx:-mt-20" id="os-paliativos-clahe-detecção-por-borda-watershed"></span>
    <a href="#os-paliativos-clahe-detec%c3%a7%c3%a3o-por-borda-watershed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando os 7 filtros básicos não foram suficientes, comecei a empilhar passes adicionais.</p>
<p>Commit <code>294e785</code> (26/fev): adicionei CLAHE (<em>Contrast Limited Adaptive Histogram Equalization</em>) como segundo passe de detecção. Balões com brilho médio, perto do threshold de 200, estavam sendo perdidos. O CLAHE equalizava o contraste e revelava esses balões borderline.</p>
<p>Mas o CLAHE também fazia rostos passarem pelos filtros porque inflava artificialmente o brilho de pele. Então precisei adicionar uma função inteira de validação contra a imagem original:</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="k">def</span> <span class="nf">_validate_on_original</span><span class="p">(</span><span class="n">candidate</span><span class="p">,</span> <span class="n">gray_orig</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Check if a CLAHE-detected candidate looks bubble-like on original.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">roi</span> <span class="o">=</span> <span class="n">gray_orig</span><span class="p">[</span><span class="n">y1</span><span class="p">:</span><span class="n">y2</span><span class="p">,</span> <span class="n">x1</span><span class="p">:</span><span class="n">x2</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">mean_brightness</span> <span class="o">=</span> <span class="n">roi</span><span class="o">.</span><span class="n">mean</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Already bright enough for pass 1 — rejected for good reason</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">mean_brightness</span> <span class="o">&gt;</span> <span class="mi">215</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Must have text strokes (dark pixels)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dark_ratio</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">roi</span> <span class="o">&lt;</span> <span class="mi">60</span><span class="p">)</span> <span class="o">/</span> <span class="n">roi</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">dark_ratio</span> <span class="o">&lt;</span> <span class="mf">0.07</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># White pixel variance: text creates high std, face skin is uniform</span>
</span></span><span class="line"><span class="cl">    <span class="n">white_pixels</span> <span class="o">=</span> <span class="n">roi</span><span class="p">[</span><span class="n">roi</span> <span class="o">&gt;</span> <span class="mi">200</span><span class="p">]</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">white_pixels</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">50</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">std</span><span class="p">(</span><span class="n">white_pixels</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">9</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">True</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>Commit <code>5dddb31</code> (1/mar): adicionei um terceiro passe inteiro de detecção baseado em bordas (<em>edge-based segmentation</em>). Pra pegar balões onde o interior branco se fundia com o fundo branco da página. Dilatava as bordas Canny, invertia, fazia AND com regiões brilhantes, e procurava contornos no resultado.</p>
<p>Commit <code>b695295</code> (26/fev): adicionei recuperação de balões pequenos via gradiente morfológico + validação por OCR. Se o OCR confirmava que tinha texto japonês válido na região, provavelmente era um balão real.</p>
<p>Cada paliativo adicionava 50-100 linhas de código e mais uma camada de complexidade. E cada um tinha seus próprios edge cases e falsos-positivos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen-debug.jpg" alt="shounen debug"  loading="lazy" /></p>
<h2>A Conta Final da v0.1<span class="hx:absolute hx:-mt-20" id="a-conta-final-da-v01"></span>
    <a href="#a-conta-final-da-v01" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No dia 1 de março, marquei a tag v0.1. Nesse ponto eu tinha:</p>
<ul>
<li><strong>90 commits</strong> em 6 dias de desenvolvimento (24/fev a 1/mar)</li>
<li><strong>551 linhas</strong> só no <code>bubble_detector.py</code></li>
<li>7 camadas de filtro de falso-positivo, cada uma com thresholds empíricos</li>
<li>Dois passes de detecção completos (original + CLAHE)</li>
<li>Um terceiro passe baseado em bordas</li>
<li>Validação cruzada contra imagem original</li>
<li>Separação de watershed pra balões sobrepostos</li>
<li>Perfis separados de threshold pra páginas coloridas vs preto-e-branco</li>
<li>20+ magic numbers ajustados empiricamente contra uma amostra limitada de páginas</li>
<li>Testes de regressão travando cada falso-positivo específico (face de menina com cabelo azul, moldura de janela, tira fina horizontal, chão de concreto&hellip;)</li>
</ul>
<p>E mesmo com tudo isso, ainda não era confiável. Cada novo mangá que eu testava revelava algum pequeno caso que quebrava o detector. Era um monolito extremamente frágil.</p>
<blockquote>
  <p><strong>SINTOMA:</strong> você faz correção pra corrigir a correção que corrigia outra correção, e quando mexe num pedaço, quebra outro sem querer: significa que o código é <strong>brittle</strong>, frágil, uma casa de cartas que vai cair a qualquer momento. Esse é o ponto pra desistir e repensar!</p>

</blockquote>
<h2>A Decisão: Pesquisar Alternativas<span class="hx:absolute hx:-mt-20" id="a-decisão-pesquisar-alternativas"></span>
    <a href="#a-decis%c3%a3o-pesquisar-alternativas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Na v0.1 eu parei e fiz a pergunta que deveria ter feito no começo: será que já existe alguém que treinou um modelo de ML pra fazer exatamente isso?</p>
<p>Pedi pro Claude pesquisar modelos disponíveis de detecção de balões em quadrinhos. A pesquisa resultou no documento <code>docs/yolo_bubble_detection_plan.md</code>, onde analisamos as alternativas.</p>
<p>A primeira que apareceu foi um <a href="https://medium.com/@beyzaakyildiz/what-is-yolov8-how-to-use-it-b3807d13c5ce"target="_blank" rel="noopener">YOLOv8 Medium do ogkalu</a> (<code>comic-speech-bubble-detector-yolov8m</code>), treinado em ~8.000 imagens de manga, webtoon, manhua e quadrinhos ocidentais. Detecta só uma classe (speech bubble). Mas pesquisando mais, achamos outro modelo do mesmo autor: <code>ogkalu/comic-text-and-bubble-detector</code>, um <a href="https://github.com/lyuwenyu/RT-DETR"target="_blank" rel="noopener">RT-DETR-v2</a> com backbone ResNet-50-vd (42.9M parâmetros), treinado em <strong>~11.000 imagens</strong>, com três classes: <code>bubble</code>, <code>text_bubble</code> e <code>text_free</code>. Ambos Apache 2.0.</p>
<p>Também avaliamos o comic-text-detector (DBNet + YOLOv5, ~13.000 imagens do Manga109), mas esse detectava regiões de texto e não balões. E como dados de treinamento futuro, havia o Roboflow com 4.492 imagens já etiquetadas, e o Manga109 dataset com 147.918 anotações em 21.142 páginas.</p>
<p>O RT-DETR-v2 com 3 classes era o mais promissor porque distinguia balões de fala, texto dentro de balões e texto livre (narração, SFX). Poderia substituir tanto o <code>bubble_detector.py</code> quanto o <code>text_detector.py</code> em uma única passada de inferência.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/026-en.jpg" alt="026 en"  loading="lazy" /></p>
<p>A conclusão do documento de pesquisa era direta:</p>
<blockquote>
  <p>&ldquo;Mesmo sem ajustes finos, esses modelos foram treinados com 8 a 11 mil imagens de diversos quadrinhos. Eles devem lidar com a diversidade de estilos artísticos que nossos filtros manuais têm dificuldade em processar. A cascata heurística de 7 filtros e seus números mágicos seriam completamente eliminados.&rdquo;</p>

</blockquote>
<p>E se não fosse suficiente, incluímos um plano de fine-tuning usando dados pareados (páginas originais em japonês vs fan translations em inglês) que geravam labels automaticamente por diff de imagem. Mas primeiro queríamos testar o baseline.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen2-en.jpg" alt="shounen2 en"  loading="lazy" /></p>
<h2>Trocando Tudo: RT-DETR-v2<span class="hx:absolute hx:-mt-20" id="trocando-tudo-rt-detr-v2"></span>
    <a href="#trocando-tudo-rt-detr-v2" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No dia 4 de março, fiz o commit <code>0df63f2</code>: &ldquo;Replace OpenCV heuristic detection with RT-DETR-v2, add bubble shape masking&rdquo;.</p>
<p>O diff fala por si:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>16 files changed, 732 insertions(&#43;), 1112 deletions(-)</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>1.112 linhas deletadas. Mais linhas removidas do que adicionadas. O detector de balões de 551 linhas foi substituído por 262 linhas — e dessas, a maior parte é extração de máscara de formato (contour mask) a partir do bbox detectado, não detecção em si.</p>
<p>O núcleo da detecção virou 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-python" data-lang="python"><span class="line"><span class="cl"><span class="n">MODEL_ID</span> <span class="o">=</span> <span class="s2">&#34;ogkalu/comic-text-and-bubble-detector&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">DEFAULT_CONFIDENCE</span> <span class="o">=</span> <span class="mf">0.35</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">detect_bubbles</span><span class="p">(</span><span class="n">img_cv</span><span class="p">,</span> <span class="n">confidence</span><span class="o">=</span><span class="n">DEFAULT_CONFIDENCE</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="p">,</span> <span class="n">processor</span><span class="p">,</span> <span class="n">device</span> <span class="o">=</span> <span class="n">_get_model</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">img_pil</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">fromarray</span><span class="p">(</span><span class="n">img_cv</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">::</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>  <span class="c1"># BGR→RGB</span>
</span></span><span class="line"><span class="cl">    <span class="n">inputs</span> <span class="o">=</span> <span class="n">processor</span><span class="p">(</span><span class="n">images</span><span class="o">=</span><span class="n">img_pil</span><span class="p">,</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s2">&#34;pt&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">torch</span><span class="o">.</span><span class="n">no_grad</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">outputs</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="o">**</span><span class="n">inputs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="n">processor</span><span class="o">.</span><span class="n">post_process_object_detection</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">outputs</span><span class="p">,</span> <span class="n">target_sizes</span><span class="o">=</span><span class="n">target_sizes</span><span class="p">,</span> <span class="n">threshold</span><span class="o">=</span><span class="n">confidence</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... map classes, deduplicate, sort</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>Sem thresholds mágicos. Sem perfis separados pra cor vs P&amp;B. Sem CLAHE. Sem detecção por borda. Sem watershed. Sem 7 camadas de filtro. Um modelo que foi treinado em 11.000 imagens diversas de quadrinhos já sabe distinguir balão de rosto melhor do que meus montes de &ldquo;ifs&rdquo;.</p>
<p>O que também sumiu junto:</p>
<ul>
<li>O <code>text_detector.py</code> inteiro (387 linhas) — substituído pela classe <code>text_free</code> do RT-DETR</li>
<li>O sistema de feedback de falsos-positivos — RT-DETR detections são confiáveis o suficiente pra não precisar de marcação manual</li>
<li>Centenas de linhas de testes de regressão testando false-positives específicos</li>
</ul>
<p>No final, nem precisei tentar fazer fine tuning próprio. O modelo que já existe resolvia mais de 99% dos casos, o que pra mim já era excelente.</p>
<p>Quem não entende estatística tem dificuldade de entender isso. Com meu procedimento &ldquo;manual&rdquo; de opencv eu já conseguia pegar 80%, talvez mais. Mas isso é muito pouco. Se em toda página um rosto fica com um balão em cima, isso é péssimo.</p>
<p>Mesmo se com muito esforço (mais quinhentos &ldquo;ifs&rdquo; diferentes) conseguisse chegar em 95%, ainda assim não é suficiente. Chegar em 80% é fácil. os últimos 20% custam exponencialmente mais, e o último 1% pode ser impossível em muitos casos. É assim que as coisas funcionam. Todo mundo pára nos 80%.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen10-debug.jpg" alt="shounen10 debug"  loading="lazy" /></p>
<h2>As Outras Dores: Flutter no Linux<span class="hx:absolute hx:-mt-20" id="as-outras-dores-flutter-no-linux"></span>
    <a href="#as-outras-dores-flutter-no-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Enquanto o backend em Python e Go foi relativamente estável, o cliente Flutter no Linux foi um capítulo à parte de sofrimento.</p>
<p>O Flutter usa WebKitGTK pro WebView no Linux, e esse componente tem particularidades dolorosas. Commit <code>8e5d168</code> (28/fev) conta a história: WebKitGTK não consegue resolver Promise return values de JavaScript assíncrono, gerando PlatformException. Tive que reescrever todos os overlays como IIFEs síncronas com <code>decode().then()</code> e um nudge de <code>opacity: 0.999</code> pra forçar refresh da textura no compositor GPU.</p>
<p>No meu setup com NVIDIA + Wayland, o WebView era inutilizável em resolução full. Commit <code>a36e1eb</code> (27/fev) tentou resolver forçando CPU rendering com <code>WEBKIT_SKIA_ENABLE_CPU_RENDERING=1</code> e desabilitando compositing acelerado. Depois tive que reverter isso (commit <code>8e5d168</code>) e forçar o Mesa da iGPU AMD via <code>__EGL_VENDOR_LIBRARY_FILENAMES</code> pra impedir que o WebKitGTK pegasse a dGPU NVIDIA.</p>
<p>Cada uma dessas descobertas custou horas de debugging de coisas que simplesmente não tinham documentação. No final consegui deixar a versão Linux razoável, mas não é nada espetacular. Não sei se estou perdendo alguma coisa óbvia, mas Flutter no Linux eu achei uma droga, especialmente tendo acabado de fazer um app nativo com Rust/Tauri (muito melhor). Mas como eu queria um app que funcionasse em Linux e Android, não tinha tanta opção.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/078-en.jpg" alt="078 en"  loading="lazy" /></p>
<h2>O Prefetch do Kindle que Morreu<span class="hx:absolute hx:-mt-20" id="o-prefetch-do-kindle-que-morreu"></span>
    <a href="#o-prefetch-do-kindle-que-morreu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma ideia que parecia genial e que deu errado: criar um segundo WebView escondido no Flutter que compartilhava cookies/sessão com o WebView principal e ficava navegando páginas à frente pra pré-processar.</p>
<p>Isso porque o website do Kindle só carrega uma página de cada vez. Ele não carrega tudo de uma vez ou partes, só uma página. Então não dá pra processar páginas pra frente ou pra trás. Quando o servidor ainda era lento pra processar páginas, eu queria deixar tudo já pré-cacheado, pra quem virar a página já tivesse a tradução.</p>
<p>Commit <code>a36e1eb</code> (27/fev) implementou o KindlePrefetchManager inteiro: 406 linhas de Dart, com batched prefetch (3 páginas à frente), eventos GDK confiáveis pra virar página (isTrusted=true), rate limiting com pacing humano, janela com sensitive=FALSE pra não roubar foco.</p>
<p>Nos dias seguintes, vieram os consertos:</p>
<ul>
<li><code>cfe08eb</code>: melhorar confiabilidade do prefetch e matching de overlay</li>
<li><code>4461b5f</code>: harden na seleção de overlay e recovery do Kindle</li>
<li><code>f141b2e</code>: parar de destruir o background webview a cada virada de página</li>
</ul>
<p>E no final (commit <code>2b93e99</code>, 4/mar), deletei tudo. 657 linhas removidas, substituídas por um spinner simples na toolbar.</p>
<p>Com a mudança pro modelo RT-DETR, e paralelizando a tradução dos balões, a tradução ficou &ldquo;quase real-time&rdquo;, em menos de 10 segundos já carregava a tradução, então não é mais um dor tão grande esperar vir a página, e dá pra pedir um de cada vez. O prefetch adicionava complexidade demais pra um ganho marginal, e o modo certo de resolver era simplesmente processar a página sob demanda com feedback visual adequado.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/adult2-furigana.jpg" alt="adult2 furigana"  loading="lazy" /></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><p>No total, o projeto Frank Yomik tem hoje:</p>
<ul>
<li><strong>111 commits</strong> em 9 dias de desenvolvimento (com 2 dias de intervalo no meio)</li>
<li><strong>29.428 linhas</strong> de código em 181 arquivos</li>
<li><strong>~13K linhas</strong> de Python (pipeline de processamento + worker)</li>
<li><strong>~6K linhas</strong> de Dart (cliente Flutter)</li>
<li><strong>~3.4K linhas</strong> de Go (API server)</li>
<li><strong>345 testes unitários</strong> + 34 testes de integração</li>
<li>Suporte a manga japonês (Kindle) e webtoons coreanos (Naver/Webtoons)</li>
</ul>
<p>O pipeline hoje funciona assim:</p>
<ol>
<li>O cliente Flutter (Android ou Linux desktop) abre o site do Kindle ou Webtoons num WebView</li>
<li>Captura a imagem da página (captura do blob de imagem na página do Kindle, fetch do <code>&lt;img&gt;</code> pra webtoons)</li>
<li>Envia pra API Go que enfileira no Redis Streams com dedup por SHA256</li>
<li>Worker Python processa: RT-DETR-v2 detecta balões → manga-ocr ou EasyOCR extrai texto → Ollama qwen3:14b traduz → text_renderer renderiza de volta na imagem</li>
<li>Resultado volta via WebSocket, overlay substitui a imagem original</li>
</ol>
<p>Pra furigana: fugashi (wrapper do MeCab) faz análise morfológica e gera a leitura em hiragana de cada kanji. Troquei de pykakasi pro fugashi porque o pykakasi não considera contexto da frase (「人」 virava にん em vez de ひと).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/adult4-furigana.jpg" alt="adult4 furigana"  loading="lazy" /></p>
<p>Se eu soubesse o que sei agora — que o modelo RT-DETR-v2 já existia e resolvia o problema de detecção com um threshold de confiança — teria eliminado a fase inteira de OpenCV. A parte de OCR, tradução, rendering e Flutter já estava razoavelmente estabilizada. A detecção era o gargalo, e era exatamente a parte que eu poderia ter economizado, se tivesse desistido antes.</p>
<h2>O que eu aprendi errando<span class="hx:absolute hx:-mt-20" id="o-que-eu-aprendi-errando"></span>
    <a href="#o-que-eu-aprendi-errando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu gastei 5 dias polindo um detector de balões em OpenCV que teve 20+ commits de ajuste fino, 7 camadas de filtro, 3 passes de detecção, e que no final foi substituído por um wrapper de 50 linhas ao redor de um modelo pré-treinado.</p>
<p>Foram 1.112 linhas deletadas num único commit. Mas não foi tempo perdido. Aqueles 5 dias me ensinaram exatamente porque heurísticas falham em visão computacional. Eu entendi o problema, a cascata onde mexer num threshold quebra outro, e foi esse entendimento que me fez reconhecer a hora de parar e pesquisar alternativas.</p>
<p>E aqui entra o papel real do vibe coding nessa história.</p>
<blockquote>
  <p><strong>Prompting não substitui pensar.</strong></p>

</blockquote>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen8-en.jpg" alt="shounen8 en"  loading="lazy" /></p>
<p>Eu gerei código muito rápido com Claude, mas o problema não era velocidade de escrita, era a abordagem. Nenhum prompt do mundo transforma 7 camadas de filtro heurístico numa solução robusta. A solução certa era mudar de abordagem completamente.</p>
<p>Mas o vibe coding tornou o fracasso barato. Nos tempos pré-AI, aqueles 5 dias de OpenCV teriam talvez sido 2-3 semanas. O custo de errar seria alto o suficiente pra dificultar a decisão de jogar fora. Com vibe coding, 5 dias foram descartados sem dor porque eu sabia que podia reconstruir rápido. E de fato, a integração do RT-DETR-v2 e reestruturação do projeto inteiro foram feitos em <strong>um único dia</strong> (4/mar, 15 commits).</p>
<p>A pergunta que fica: se eu tivesse feito a pesquisa do <code>yolo_bubble_detection_plan.md</code> no dia 1 em vez do dia 5, provavelmente teria chegado no estado atual em 2 dias. A diferença entre semanas de trabalho e um fim de semana era uma busca no HuggingFace. Pesquisar antes de implementar parece óbvio quando você olha pra trás, mas no calor do momento a tentação de resolver na mão é forte.</p>
<p>O projeto agora é open source. Eu inicialmente não sabia se queria abrir o código, tinha muita gambiarra e aquele detector de 551 linhas que eu tinha vergonha. Mas depois da refatoração, o código ficou limpo o suficiente pra compartilhar. É a versão que eu gostaria de ter construído desde o início, mas que só consegui construir porque errei primeiro.</p>
<p>A dor de cabeça maior foi o modelo de detecção e substituição de balões, mas ainda tem vários outros pontos que não detalhei: notaram que Webtoons são coloridos e os próprios balões tem arte? Precisei usar um modelo de imagem pra fazer <strong>in-painting</strong> e mandar a IA redesenhar o balão antes de colocar o texto em cima.</p>
<p>Outra dor de cabeça que acho que não vou resolver: <strong>coerência da tradução</strong>. Hoje ele traduz cada balão isolado, sem contexto da história antes ou depois. Em japonês, não existe diferença de gênero em palavras. Então o capitão estava falando da Nami mas o balão fala &ldquo;Ele&rdquo; em vez de &ldquo;Ela&rdquo;. Não tem como saber se não ler o texto anterior. Pra isso funcionar melhor, assim como num chat de GPT, precisa adicionar parte do texto anterior, pra saber quando usar o gênero correto ou, pior, quando tem trocadilhos que apareceram volumes atrás e é referenciado no futuro (coisa que um Oda adora fazer). Essas sutilezas todas se perdem se traduzir só um balão de cada vez.</p>
<p>Imagino que é por isso que ainda ninguém fez algo parecido com o que eu fiz, de traduzir em tempo quase real, porque pra realmente ficar bom, o trabalho de tradução seria exponencial pra cada capítulo mais pra frente na história.</p>
<p>Enfim, o nome <strong>Frank Yomik</strong> vem de &ldquo;yomi&rdquo; (読み, leitura em japonês) e &ldquo;ik-da&rdquo; (읽다, ler em coreano). Frank é referência à tradução franca, direta. O app lê nas duas línguas.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/shounen9-en.jpg" alt="shounen9 en"  loading="lazy" /></p>
<p>Pra quem quiser experimentar: o <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">repositório está no GitHub</a>. O server precisa de uma GPU com pelo menos 8GB VRAM pro modelo de detecção + OCR + tradução. O cliente Flutter roda em Android e Linux desktop. E se você, como eu, tem uma pilha de mangás japoneses que gostaria de ler mais fluentemente — agora pode.</p>
]]></content:encoded><category>frank-yomik</category><category>frank</category><category>llms</category><category>inteligencia-artificial</category></item></channel></rss>