Voice Master
Extensão do Chrome que escreve a fala ditada em qualquer campo de texto de qualquer página web, com correção opcional por IA através da chave de API do próprio utilizador.
- Publicado
- Utilizadores
- sobre 2000
- Idiomas da interface
- 17
- Preço
- Grátis
Sobre
O Voice Master injeta um painel arrastável na página atual, executa a Web Speech API do Chrome e insere o texto reconhecido no cursor do último campo com foco: input, textarea, contenteditable ou TinyMCE. Sobre isso assentam um painel de ditado com a sua própria área de texto, atalhos de teclado, 33 idiomas de ditado e uma interface em 17 idiomas.
O difícil é o próprio motor de reconhecimento. O webkitSpeechRecognition em modo contínuo termina as sessões por si após pausas e após alguns minutos de trabalho, e comunica-o apenas através de onend. A extensão mantém viva uma sessão de ditado com um watchdog separado que reanima um motor parado e reinicia um em execução num temporizador, entregando o texto pendente ao campo antes de cada reinício planeado.
A correção opcional por IA envia os fragmentos ditados à OpenAI, Anthropic, DeepSeek ou Gemini com a chave do próprio utilizador. As chamadas saem do service worker, os resultados são inseridos pela ordem da fala, e uma chamada falhada recorre ao texto bruto. As chaves guardadas são cifradas em repouso e não são enviadas ao JavaScript da página.
Voice Master · voz para texto
o que foi resolvido
Manter o reconhecimento vivo
no produtoO que ofereceO ditado longo não pára por si após uma pausa nem após alguns minutos, e nada do que foi dito imediatamente antes de um reinício se perde.
- Problema
- O reconhecimento de fala contínuo do Chrome termina as sessões por si após pausas e após alguns minutos, sem outro sinal fiável além de onend. Um ditado longo pára em silêncio.
- Solução
Uma classe Heartbeat separada consulta o estado do motor a cada 100 ms, reanima um motor parado após um período de tolerância e reinicia um em execução num temporizador, e o host entrega o texto pendente antes de cada reinício planeado.
- Stack
O motor pára por si após uma pausa e o ditado morreO watchdog verifica a cada 100 ms e reinicia o motor após 1.5 s de inatividade confirmada
Heartbeat.tick() pergunta ao host se o reconhecimento está em execução. Quando não está, regista o momento da morte e espera um período de tolerância de 1.5 s antes de chamar onRevive(), para que um ciclo normal de paragem/arranque dentro do Chrome não seja perturbado.
Só um revive está em curso de cada vez. Se onstart não chegar em 3 s, o revive pendente é descartado e começa um novo período de tolerância de 1.5 s antes da tentativa seguinte. A flag recognizing é definida apenas por onstart através de noteEngineUp(), nunca de forma otimista, de modo que um start() falhado (InvalidStateError) não pode fazer o watchdog acreditar que o motor está ativo.
O Chrome termina uma sessão longa por si, num momento que escolheRenovar a cada 270 s: entregar o texto pendente, parar o motor, deixar o watchdog reanimá-lo
O watchdog guarda a hora de início da subsessão ativa atual. Após sessionMaxMs (270000 ms, definido em speech.js) chama refreshSession(), que primeiro entrega a transcrição final acumulada e a última frase provisória ao campo de destino, e só depois pára o motor. end() vê que a paragem não foi manual e devolve sem finalizar; os ticks seguintes reanimam uma sessão nova.
O intervalo é escolhido para cair antes do limite de aproximadamente cinco minutos que o autor observou no Chrome (indicado nos comentários do código), de modo que o reinício acontece num momento que a extensão controla, em vez de um que o Chrome escolhe.
O reinício automático não deve lutar contra o utilizador nem contra um microfone negadoFlags separadas manualStop e fatalError condicionam cada revive; só os erros de microfone e de permissão o param definitivamente
onend dispara para qualquer tipo de paragem, pelo que end() não os consegue distinguir sozinho. O host mantém duas flags: manualStop é definida apenas pela paragem do utilizador (botão, atalho, fecho do painel), fatalError apenas pelos erros audio-capture e not-allowed. O watchdog e reviveRecognition() verificam ambas e ficam inativos quando qualquer uma está definida; todos os outros erros, como no-speech, deixam a sessão para ser reanimada.
Para not-allowed, o handler compara a hora do erro com a hora do clique: um erro em menos de 100 ms significa que a permissão está bloqueada por política, um mais tardio significa que o utilizador negou o pedido, e o painel mostra uma mensagem diferente para cada caso.
Lógica de reinício emaranhada com o tratamento bruto da Web Speech APIUm wrapper fino RecognitionEngine emite callbacks; todo o estado de reinício fica num só host
RecognitionEngine possui apenas o objeto webkitSpeechRecognition (contínuo, resultados provisórios, 5 alternativas) e analisa os resultados em finalText, interimText e hadFinal. start() deixa de propósito InvalidStateError propagar-se para que quem chama decida se volta a tentar.
As flags, a acumulação e as decisões de reinício vivem em speech.js, e a política de tempos vive em Heartbeat. As três partes comunicam por pequenas interfaces de callbacks (isRecognizing, onRevive, onRefresh, onSilence), de modo que o watchdog pode ser afinado sem tocar no código de reconhecimento.
Levar texto para qualquer campo
no produtoO que ofereceAs palavras ditadas caem onde estava o cursor, em qualquer caixa de texto ou editor de um site, e cada frase só é escrita quando está terminada.
- Problema
- O texto reconhecido tem de cair no cursor do campo em que o utilizador estava a escrever, em páginas que a extensão não controla, enquanto o utilizador clica no painel da própria extensão.
- Solução
Um TextInserter segue o último elemento elegível com foco e usa o método de inserção que cada tipo de editor aceita; o painel evita roubar o foco.
- Stack
Os campos diferem: input, textarea, contenteditable, TinyMCEUm insert() escolhe splice, execCommand('insertText') ou mceInsertContent consoante o tipo de destino
Para input e textarea, o valor é emendado entre selectionStart e selectionEnd e o cursor é movido para depois do novo texto, de modo que um fragmento selecionado é substituído pelo ditado. Para contenteditable usa execCommand('insertText'), que também substitui a seleção atual e deixa o editor ver uma edição normal. Para o TinyMCE chama o próprio comando mceInsertContent do editor.
O TinyMCE inicializa tarde, pelo que o hook de foco para ele é anexado após um atraso e, se mais nada tinha ainda foco, o editor ativo recebe foco e passa a ser seguido.
Clicar no painel tira o foco do campo de destinoSeguir o foco na fase de captura, manter o último campo elegível; bloquear o roubo de foco nos controlos
Um listener de foco na fase de captura no document regista cada elemento elegível numa lista com o mais recente no fim, de modo que, quando o utilizador clica no painel, o destino da inserção continua a ser o último campo em que esteve.
O botão de iniciar cancela mousedown e os selects de idioma cancelam mouseup, pelo que premi-los não tira o foco ao campo. O painel arrastável ignora o tratamento de arrastar em inputs, botões e ligações, para que mantenham o foco e o clique normais.
Os resultados provisórios mudam constantemente e não devem ser escritos cedoMostrar o texto provisório numa pré-visualização; entregar a frase só quando o fluxo provisório esvazia
Os resultados finais acumulam-se em finalTranscript. O texto provisório vai para um painel de pré-visualização arrastável separado e é lembrado como prev_text; quando chega uma atualização com a parte provisória vazia, buffer() entrega a frase lembrada ao campo e limpa a pré-visualização.
O span final só é desenhado de novo quando chegou um novo resultado final, o que mantém baixo o trabalho no DOM durante um ditado longo.
Pipeline de correção por IA
no produtoO que ofereceUma IA à escolha do utilizador pode corrigir palavras mal ouvidas pelo contexto. O texto mantém a sua ordem falada e, se a IA falhar, o texto bruto chega na mesma.
- Problema
- Quatro fornecedores de IA com APIs diferentes, chamados a partir de páginas web arbitrárias com a sua própria CSP, enquanto o ditado continua a produzir novos fragmentos que têm de ficar por ordem.
- Solução
Os adaptadores de fornecedores correm no service worker; a página fala com eles através de uma ponte de eventos correlacionados, e uma fila FIFO com timeout e recurso ao texto bruto alimenta o campo.
- Stack
A CSP e o CORS da página bloqueiam chamadas às APIs de IA a partir da páginaTodas as chamadas à API correm no service worker MV3 com host_permissions para os quatro hosts
O corretor do lado da página só constrói o prompt. O pedido viaja através do content script até ao service worker, que é dono do fetch. host_permissions em manifest.json cobrem api.openai.com, api.anthropic.com, api.deepseek.com e generativelanguage.googleapis.com.
O listener onMessage do worker devolve true para manter o canal aberto para a resposta assíncrona.
Os quatro fornecedores diferem na autenticação e nas formas do pedido e da respostaTabela de adaptadores: por fornecedor, um URL, construtor de cabeçalhos, construtor de corpo, parser e leitor de erros
AI_PROVIDERS descreve cada API de forma declarativa: cabeçalho Bearer para a OpenAI e a DeepSeek, x-api-key mais cabeçalhos de versão para a Anthropic, x-goog-api-key para o Gemini, e o caminho até ao texto em cada resposta. aiCorrect() é igual para todos.
Uma chave vazia devolve o código 'no_key' em vez de um erro, o que permite a quem chama recorrer ao texto bruto em silêncio. O Gemini foi acrescentado como quarto fornecedor por uma entrada da tabela mais uma permissão de host no manifesto.
As respostas assíncronas da IA podiam inserir fragmentos fora da ordem faladaFila FIFO em cadeia de promises, timeout de 20 s por fragmento, texto bruto inserido em qualquer falha
LiveImprover.push() acrescenta cada fragmento a uma única cadeia de promises, de modo que o fragmento N+1 só é enviado depois de o fragmento N ter sido inserido. Cada chamada compete com um timeout de 20 s. Em caso de falha ou timeout, o texto original é inserido, pelo que a fala ditada nunca se perde.
Os modelos devolvem texto aparado, pelo que keepSpacing() restaura os espaços iniciais e finais originais do fragmento para impedir que fragmentos consecutivos se colem. Sem chave, a fila comunica o código 'no_key' e a interface fica em silêncio, como se o modo live estivesse desligado.
O mundo da página e o mundo da extensão só podem trocar eventosIDs de pedido e um mapa de promises pendentes transformam eventos de window em chamadas assíncronas
O código do painel corre como script de módulo na página e não tem APIs chrome.*. GptCorrector.improve() atribui um id de sequência, guarda o resolver num Map e despacha stt_ai_correct_request. O content script reencaminha-o para o worker e responde com stt_ai_correct_response com o mesmo id, que resolve exatamente uma promise pendente.
O motor de fala ouve mal palavras; um corretor ortográfico simples deixa-asUm prompt localizado diz ao modelo que o texto é STT bruto e que corrija as palavras pelo contexto
O prompt corr_and_return declara que o texto veio de um reconhecedor de fala imperfeito, pede ao modelo que substitua as palavras mal reconhecidas pelo que o falante provavelmente quis dizer, mantenha o idioma e o sentido, não acrescente nada e devolva apenas o texto. Está traduzido para todos os locales da interface.
No painel de ditado, o texto melhorado substitui o conteúdo da textarea através de execCommand('insertText'), pelo que Ctrl+Z restaura a versão original.
Tratamento das chaves de API
shipped · 2026O que ofereceAs chaves de IA do próprio utilizador são guardadas cifradas, e os sites onde o painel aparece não as podem ler. As definições mantêm-se iguais em todos os separadores abertos.
- Problema
- As chaves de API do utilizador têm de ser guardadas por uma extensão cuja interface corre dentro de páginas não confiáveis, pelo que qualquer script da página poderia ler o que o painel consegue ler.
- Solução
As chaves são cifradas e decifradas no service worker, mantidas em texto simples apenas na memória do content script, e a página recebe apenas flags e indicações mascaradas.
- Stack
As chaves de API não devem estar em texto simples no armazenamento da extensãoAES-GCM com um segredo aleatório de 256 bits por instalação e um IV novo de 12 bytes por valor
sttCryptoKey() gera o segredo uma vez, guarda-o e importa-o como uma CryptoKey não extraível. Os valores cifrados levam um prefixo de versão (stt1:) seguido de IV e texto cifrado em base64.
A cifragem corre no service worker porque este é sempre um contexto seguro com crypto.subtle, enquanto um content script numa página http simples não é. O comentário do código é explícito: isto é proteção em repouso contra leitura casual, não contra quem tem todo o perfil do navegador.
Os scripts da página não devem poder ler as chaves guardadasOs eventos da página levam apenas flags hasKeys e indicações mascaradas; o texto simples fica no content script
sttPublicCfg() é a única forma que passa para a página: fornecedor, flag live, um booleano por fornecedor e uma indicação como os primeiros 3 e os últimos 4 carateres. O formulário de definições nunca recebe a chave guardada; um campo intocado em Guardar significa manter a chave guardada (flag keyTouched).
Os utilizadores existentes tinham chaves em texto simples num formato de armazenamento antigoNa primeira leitura, as chaves legadas e em texto simples são guardadas de novo cifradas sem ação do utilizador
sttGetAi() move a antiga gptApiKey única para a estrutura por fornecedor, cifra qualquer valor sem o prefixo stt1:, escreve o resultado de volta e remove a célula legada. Se o worker falhar a cifragem, o texto simples é mantido para que o utilizador não fique bloqueado, e a migração é repetida na leitura seguinte.
sttDecrypt() devolve uma string vazia perante um valor corrompido ou alheio, pelo que um registo estragado se comporta como 'sem chave' em vez de lançar uma exceção.
As definições têm de se manter atuais em todos os separadores abertosstorage.onChanged volta a difundir a configuração sanitizada para o espelho da página de cada separador
O painel de cada separador mantém um espelho local de fornecedor, flag live e hasKeys. content.js escuta chrome.storage.onChanged para o registo sttAi e empurra para a página uma configuração sanitizada nova, de modo que liveEnabled() nunca atua sobre uma chave que foi eliminada noutro lado.
Eliminar uma chave no popover é local até Guardar; fechar sem guardar mantém a chave guardada.
Funcionar dentro de qualquer página
no produtoO que ofereceO atalho de teclado funciona mesmo em separadores que estavam abertos antes de a extensão carregar, e o painel fala o idioma do utilizador em qualquer site.
- Problema
- O painel vive dentro de páginas de terceiros: os content scripts podem faltar e o código do mundo da página não tem APIs chrome.*.
- Solução
Injeção a pedido com um caminho de recurso e uma ponte de localização para o armazenamento da página.
- Stack
Atalho premido num separador onde o content script não está carregadoTentar sendMessage; em lastError injetar content.js com chrome.scripting e reenviar
start() no service worker envia o toggle para o separador. Se chrome.runtime.lastError mostrar que ninguém está a escutar, injeta content.js e repete a mensagem. O módulo do painel e a folha de estilos são então injetados a pedido, ignorando as páginas chrome://.
O popup verifica da mesma forma com um ping isLoaded antes de decidir se envia a mensagem ao separador ou passa pelo worker.
O código do painel no mundo da página não pode chamar chrome.i18nO content script copia todas as mensagens para o localStorage da página; en fornece a lista de chaves
getLocalizationKeys() lê a lista de chaves do messages.json do locale por defeito, que tem o conjunto completo, e guarda depois chrome.i18n.getMessage(key) para cada uma sob um prefixo stt_. Helper.getI18nMessage() na página lê daí.
Como a lista de chaves vem de en e os valores de chrome.i18n, um locale com chaves em falta recorre ao inglês chave a chave. O locale hi falta atualmente 7 chaves e é coberto desta forma.
Números
do produto- Utilizadores cerca de 2,000 Chrome Web Store.
- Idiomas da interface 17
- Idiomas de ditado 33 63 variantes regionais.