LirafyBlogWendo ↗
Português (Brasil)

Blog

Whisper no celular, sem internet: como o Wendo faz ditado

Sim, o Whisper funciona no celular sem internet. Com o whisper.cpp e um modelo ggml baixado uma vez, o aparelho transcreve a voz sozinho e o áudio não sai dele. É assim que o Wendo faz ditado: ele é o nosso cliente SSH para acompanhar agentes como o Claude Code pelo iOS e pelo Android.

Aqui contamos como isso foi feito: que modelos ele oferece, como baixa cada um, em que formato grava, o que faz com o que o Whisper devolve e por que também existe a opção de usar a API da OpenAI ou da Groq com a sua própria chave.

Whisper offline no celular: por que usamos

Escrever para um agente de código pelo teclado do celular é lento. As instruções para o Claude Code costumam ser frases longas (“veja por que o teste de login está falhando e não mexa na migração”), e digitar isso com o polegar é justamente a parte que dá preguiça. Falar é mais rápido.

O problema é o que você fala. Ao ditar para um agente, você cita arquivos, caminhos, branches e às vezes coisas do cliente. Mandar esse áudio para o servidor de outra empresa para virar texto nem sempre é uma boa ideia, e com sinal ruim nem funciona. Por isso o motor padrão do Wendo é o Whisper local. Em lib/core/settings.dart o valor inicial é speechEngine = 'whisper' com o modelo base, e a tela de ajustes resume isso numa linha: “O Whisper transcreve no celular com whisper.cpp: nenhum áudio é enviado a nenhum servidor”.

Como usar o whisper.cpp no Flutter: whisper_ggml e record

O whisper.cpp é a reimplementação em C/C++ do Whisper, o modelo de reconhecimento de voz da OpenAI, e usa o formato de pesos ggml. Ele roda na CPU sem dependências pesadas, e o repositório traz exemplos oficiais para iOS e Android. No Flutter não é preciso escrever os bindings à mão: há vários pacotes no pub.dev que o encapsulam.

O Wendo usa dois, segundo o pubspec.yaml:

record: ^7.1.2
whisper_ggml: ^2.8.1

O record grava o microfone e o whisper_ggml chama o whisper.cpp.

Tem um detalhe que não aparece de cara. O whisper_ggml converte o áudio de entrada com ffmpeg, e colocar ffmpeg num app mobile só para isso é carregar uma dependência grande. Como o Wendo já grava no formato que o whisper.cpp espera, o pubspec.yaml troca esse pacote por um stub local:

dependency_overrides:
  # O whisper_ggml converte o áudio com ffmpeg; o Wendo já grava WAV de 16 kHz mono (veja o pacote).
  ffmpeg_kit_flutter_new_min:
    path: packages/ffmpeg_kit_stub

O stub (packages/ffmpeg_kit_stub/lib/ffmpeg_kit.dart) implementa só o execute, que sempre “falha”. Diante dessa falha, o whisper_ggml transcreve o arquivo como está. Se você monta Whisper offline no Flutter com esse pacote e já grava em WAV de 16 kHz por conta própria, dá para se livrar do ffmpeg do mesmo jeito.

Ditado com Whisper no celular: grava em WAV de 16 kHz, o whisper.cpp transcreve com um modelo ggml baixado e o texto é limpo; a API da OpenAI ou da Groq é opcional

Quais modelos do Whisper ele oferece e quanto ocupa um modelo

O Wendo não traz nenhum modelo dentro do app. Ele oferece sete em Ajustes › Fala, definidos em WHISPER_MODELS de lib/core/dictation.dart, em dois grupos:

  • Multilíngues: Tiny (75 MB), Base (142 MB), Small (466 MB) e Turbo, que é o large-v3-turbo (o app mostra como 1.6 GB).
  • Só inglês: tiny.en, base.en e small.en, com os mesmos tamanhos das versões multilíngues.

Esses tamanhos batem com a tabela oficial de modelos do whisper.cpp (models/README.md no repositório da ggml-org), que traz 75 MiB, 142 MiB, 466 MiB e 1,5 GiB para o large-v3-turbo. A diferença do Turbo é só de unidade: 1,5 GiB dá uns 1,6 GB.

O README principal do whisper.cpp também publica a memória aproximada que cada tamanho usa: uns 273 MB o tiny, 388 MB o base e 852 MB o small. Num computador tanto faz. Num celular que também está com uma sessão SSH, um terminal e talvez um navegador abertos, isso pesa.

Qual modelo do Whisper usar no celular

As descrições do próprio app resumem: Tiny é “mais rápido, menos preciso”, Base é “rápido, boa precisão”, Small é “mais lento, ótima precisão” e Turbo, “máxima precisão”. Para ditar para um agente, o base é um bom ponto de partida, e por isso é o padrão.

Os modelos .en só entendem inglês. O Wendo força isso no código, então o ajuste de idioma não os afeta:

// Os modelos “.en” só entendem inglês.
final language = whisperModel.endsWith('.en') ? 'en' : settings.whisperLanguage;

Se você dita sempre em inglês, eles compensam: o repositório do Whisper da OpenAI indica que as versões .en rendem melhor em inglês, principalmente nos tamanhos tiny e base. Se você mistura idiomas, ou fala português com nomes de funções em inglês, use um multilíngue com o idioma fixo. A detecção automática (auto) é o padrão, mas num ditado de três palavras o Whisper tem pouquíssimo áudio para adivinhar o idioma, e fixar o idioma tira essa dúvida.

Meça a latência no seu celular

Não publicamos tempos de transcrição porque eles dependem do celular, do modelo e de quanto tempo você fala. O que o Wendo faz é deixar você medir. Em Ajustes › Fala há um botão de microfone para testar o motor, e embaixo do texto reconhecido aparece uma linha com o motor, o modelo, o idioma, o formato, a duração do áudio e a latência em milissegundos. Ela vem do TranscriptionResult, que guarda durationMs e latencyMs em cada transcrição. Teste base e small com a mesma frase e decida com os seus números.

Como um modelo é baixado

Os modelos saem do repositório do whisper.cpp no Hugging Face, com o nome de arquivo padrão:

const _hf = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
String _fileName(String id) => 'ggml-$id.bin';

Quando você toca num modelo que não tem, o downloadModel baixa primeiro para um arquivo .part na pasta temporária e, quando termina, move para a pasta de documentos do app. Se as duas pastas estão em volumes diferentes e não dá para renomear, ele copia e apaga o temporário. Assim um download interrompido nunca se passa por modelo válido.

Mesmo assim, um modelo pode acabar corrompido. Quando o whisper.cpp responde “failed to load the model”, o Wendo não mostra o erro cru: avisa que o modelo está corrompido e pede que você o apague em Ajustes › Fala e baixe de novo.

Gravar no formato que o whisper.cpp espera

O whisper.cpp trabalha com WAV PCM de 16 bits a 16 kHz e mono; o README dele traz até o comando do ffmpeg para converter para esse formato. O Wendo já grava assim, com o record:

const RecordConfig(
  encoder: AudioEncoder.pcm16bits,
  sampleRate: _sampleRate, // 16000
  numChannels: 1,
  androidConfig: AndroidRecordConfig(audioSource: AndroidAudioSource.voiceRecognition),
)

No Android ele pede a fonte VOICE_RECOGNITION, pensada para reconhecimento de voz. Os pedaços de PCM se acumulam na memória enquanto você fala. Quando você para, o Wendo espera 200 ms pelo último pedaço e descarta gravações de menos de 0,3 segundo, que costumam ser toques sem querer. Depois coloca na frente um cabeçalho RIFF de 44 bytes (encodeWav em lib/components/settings/byok.dart), grava dictation.wav no cache, transcreve e apaga o arquivo num finally, tenha dado certo ou não.

O modelo fica carregado entre um ditado e outro (keepModelLoaded: true), então só o primeiro paga o custo de carregar. Se você troca de modelo ou apaga o que está em uso, o Wendo libera antes o contexto do whisper.

Limpar o que o Whisper devolve

O Whisper nem sempre devolve texto ditado. Quando não ouve voz, ele escreve marcações como [BLANK_AUDIO] ou frases entre parênteses do tipo “(silêncio)”. Num editor de texto isso não incomoda; num terminal vai parar no prompt do agente. O _stripNonSpeech remove o que está entre colchetes em maiúsculas e descarta o resultado inteiro se ele for só um parêntese ou asteriscos.

O texto limpo vai para o Editor ou para o terminal. Por padrão ele é escrito sem apertar Enter, para você revisar antes de mandar, a não ser que ative o Envio automático. Cada ditado fica num histórico local (em Ajustes › Fala › Histórico de ditado), de onde você pode reenviá-lo.

Substituições de palavras

Nenhum modelo pequeno acerta “tmux”, “kubectl” ou o nome do seu projeto. Para isso existem as substituições de palavras: pares “o que o Whisper entende” → “o que você quis dizer”. A implementação cabe em poucas linhas:

// Substituições de palavras: em ordem, sem diferenciar maiúsculas, só as ativas.
String applyReplacements(String text) {
  var out = text;
  for (final replacement in getSettings().dictationReplacements) {
    if (!replacement.enabled || replacement.from.isEmpty) continue;
    out = out.replaceAllMapped(RegExp(RegExp.escape(replacement.from), caseSensitive: false), (_) => replacement.to);
  }
  return out;
}

Elas são aplicadas em ordem e sem diferenciar maiúsculas de minúsculas, podem ser desativadas sem apagar e, se o destino estiver vazio, a palavra é removida. Funcionam igual com o Whisper local e com a nuvem. Para os nomes próprios do seu trabalho, uma substituição sai mais barata do que passar para um modelo maior.

A alternativa: traga sua própria chave (OpenAI ou Groq)

O motor BYOK (“bring your own key”) envia o mesmo WAV para um provedor na nuvem com a sua chave de API. O Wendo aceita dois, definidos em BYOK_PROVIDERS:

  • OpenAI, no /v1/audio/transcriptions, com gpt-4o-mini-transcribe como modelo padrão.
  • Groq, no endpoint compatível com a OpenAI, com whisper-large-v3-turbo como padrão.

O campo do modelo é editável, então dá para colocar outro que o seu provedor aceite. A chave fica guardada nas Chaves do dispositivo, uma por provedor, e o app diz na mesma tela que ela “só é enviada” para o provedor escolhido. A requisição é um multipart com file, model, response_format: json e, se você não deixou a detecção automática, language.

As duas opções existem porque resolvem coisas diferentes. O Whisper local é privado, funciona sem sinal e não custa nada por ditado, em troca de ocupar espaço e memória e de depender da potência do celular. A nuvem não ocupa nada no celular e dá acesso a modelos grandes que não cabem nele, mas o áudio sai do aparelho, você precisa de conexão e paga o provedor. A Groq serve o mesmo large-v3-turbo que o Wendo oferece localmente, com a diferença de que roda nos servidores dela e não no seu celular. Já a OpenAI acrescenta os modelos de transcrição próprios.

Nossa recomendação: comece com o base local e algumas substituições. Se você dita muito, o celular sofre com os modelos grandes e não se importa que o áudio saia do aparelho, passe para o BYOK.

Testar

O ditado é uma parte do Wendo, que ainda não está nas lojas. Ele é ativado pelo microfone da barra do terminal ou do Editor, ou segurando o botão do teclado, que é o gesto padrão. Se você ainda não preparou o computador para falar com um agente pelo celular, comece por como usar o Claude Code pelo celular, e para manter a sessão viva enquanto dita, o tmux mínimo para agentes de código.