npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pedrobef/vozz

v0.2.7

Published

TTS ultrarrealista em português do Brasil, 100% offline no navegador. Vozes neurais leves (~86 MB), sem API key, sem servidor, sem custo.

Downloads

1,786

Readme

Voz neural em português do Brasil, direto no navegador.

Sem API key. Sem servidor. Sem custo por caractere.

npm CI licença


npm install @pedrobef/vozz
import { Piper } from "@pedrobef/vozz/piper";

const tts = await Piper.carregar();
const audio = await tts.falar("Olá! Tudo bem com você?");
audio.tocar();

Três linhas. O texto nunca sai do dispositivo do usuário.


Índice


Por que existe

Já havia bibliotecas de TTS neural em JavaScript, mas nenhuma falava português. O motivo é técnico: modelos de voz não recebem letras, recebem fonemas IPA. O fonemizador disponível em JS (phonemizer) só traz inglês compilado:

phonemize("Olá", "pt-br")
// Erro: Invalid language identifier. Should be one of: en, en-us, en-gb...

Sem fonemizador de português, passar texto brasileiro por um motor inglês produz sotaque grotesco:

"Eu sou a Dora"
  fonemizador inglês →  ˌiːjˈuː sˈuː ɐ dˈoːɹə     ✗  "iiu su a dôra"
  vozz               →  ˈeʊ sˈoʊ a dˈoɾæ          ✓

O núcleo do vozz é um conversor grafema→fonema (G2P) de português brasileiro escrito em JavaScript puro — sem WASM, sem binário nativo, sem rede. Ele implementa silabificação, acentuação tônica, nasalização, palatalização de /d/ e /t/, vocalização do /l/ em coda, vibrante múltipla, redução de vogais átonas e sândi entre palavras.

Fidelidade medida: 94,97% contra o espeak-ng pt-br (PER de 5,03% em 319 palavras e frases). O teste roda em npm test.


Os três motores

| | piper ⭐ | index (Kokoro) | sintetizador | | --- | --- | --- | --- | | Qualidade | voz natural pt-BR | natural, multilíngue | robótica | | Download | 18,7 MB | ~86 MB | nada | | Velocidade | ~2× tempo real | ~1× | ~70× | | Navegador | ✅ | ✅ | ✅ | | Workers / edge | importa apenas¹ | ❌ | ✅ executa | | Node / SSR | ✅ | ✅ | ✅ |

¹ O Piper precisa de ~11 MB de WASM + 18,7 MB de modelo, o que estoura o limite de memória do edge. Por design, a síntese neural roda no dispositivo do usuário — onde é gratuita e escala sozinha.

Escolha rápida: quer voz boa em pt-BR? piper. Precisa rodar dentro de um Worker ou não pode baixar nada? sintetizador. Só precisa dos fonemas (lipsync, legendas, visemas)? g2p.


Começando

Voz neural (recomendado)

import { Piper } from "@pedrobef/vozz/piper";

const tts = await Piper.carregar({
  aoProgredir: (p) => {
    if (p.status === "baixando") {
      console.log(`${Math.round(p.progresso * 100)}% — ${p.arquivo}`);
    }
  },
});

const audio = await tts.falar("Bom dia! Hoje são 14h30.", { velocidade: 1.1 });
audio.tocar();

O modelo baixa uma vez e fica na Cache API do navegador. Visitas seguintes carregam instantaneamente.

Streaming — tocar sem esperar o texto inteiro

for await (const { texto, audio } of tts.falarEmFluxo(textoLongo)) {
  console.log("tocando:", texto);
  await audio.tocar();
}

Ler a resposta de um LLM em tempo real

import { DivisorDeTexto } from "@pedrobef/vozz/piper";

const divisor = new DivisorDeTexto();

(async () => {
  for await (const { audio } of tts.falarEmFluxo(divisor)) await audio.tocar();
})();

for await (const token of respostaDoLLM) divisor.empurrar(token);
divisor.fechar();

O divisor acumula tokens e libera frases completas — evita prosódia picotada no meio da sentença.

Sem download nenhum (roda até no edge)

import { Sintetizador } from "@pedrobef/vozz/sintetizador";

const tts = new Sintetizador();          // sem await: não há I/O
const audio = tts.falar("Olá!", { voz: "clara" });
audio.tocar();

Vozes: clara (feminina), bruno (masculina), grave (narração).

Só os fonemas

import { fonemizar } from "@pedrobef/vozz/g2p";

fonemizar("Olá, tudo bem?");        // "olˈa, tˈudʊ bˈeɪŋ?"
fonemizar("Custa R$ 25,50 às 9h");  // números e moeda já expandidos

Zero dependências, ~36 kB. Útil para lipsync, visemas e legendas fonéticas.

Salvar e exportar

const audio = await tts.falar("Olá!");

audio.duracao;              // segundos
audio.paraWav();            // ArrayBuffer (WAV PCM 16-bit)
audio.paraBlob();           // Blob audio/wav
audio.paraURL();            // object URL para <audio src>
await audio.salvar("ola.wav");  // baixa no navegador, grava em disco no Node

Normalização automática

O texto é preparado antes de virar fala:

| Entrada | Falado como | | --- | --- | | R$ 1.234,56 | mil duzentos e trinta e quatro reais e cinquenta e seis centavos | | 14h30 | quatorze horas e trinta minutos | | 07/09/2025 | sete de setembro de dois mil e vinte e cinco | | 23°C | vinte e três graus celsius | | 15% | quinze por cento | | | primeiro | | Dr. Silva | doutor Silva | | IBGE | i-bê-gê-é |

Desligar: tts.falar(texto, { normalizar: false }).

Pronúncia customizada

await tts.falar("Rodamos em Kubernetes.", {
  lexico: { kubernetes: "kubeɾnˈetʃis" },
});

Para descobrir o IPA de qualquer texto: Piper.fonemizar("...").


Receitas por framework

Next.js (Vercel)

O Piper só existe no cliente:

"use client";
import { useEffect, useRef, useState } from "react";

export default function BotaoFalar() {
  const tts = useRef<any>(null);
  const [pronto, setPronto] = useState(false);

  useEffect(() => {
    (async () => {
      const { Piper } = await import("@pedrobef/vozz/piper");
      tts.current = await Piper.carregar();
      setPronto(true);
    })();
  }, []);

  return (
    <button disabled={!pronto} onClick={async () => (await tts.current.falar("Olá!")).tocar()}>
      {pronto ? "Falar" : "Carregando..."}
    </button>
  );
}

Vite / React / Astro / SvelteKit

import { Piper } from "@pedrobef/vozz/piper";
const tts = await Piper.carregar();
(await tts.falar("Olá!")).tocar();

Sem optimizeDeps, sem config, sem instalar onnxruntime-web.

Cloudflare Pages

Site estático comum — a síntese roda no navegador, então não há backend. Faça o deploy normalmente; nenhuma configuração especial é necessária.

Cloudflare Workers / Pages Functions

No edge funcionam o G2P e o sintetizador (JS puro, sem dependências):

import { Sintetizador } from "@pedrobef/vozz/sintetizador";

export default {
  fetch(request) {
    const texto = new URL(request.url).searchParams.get("t") ?? "Olá!";
    const audio = new Sintetizador().falar(texto);
    return new Response(audio.paraWav(), {
      headers: { "content-type": "audio/wav" },
    });
  },
};

Node (gerar arquivos em lote)

npm i @pedrobef/vozz onnxruntime-node
import { Piper } from "@pedrobef/vozz/piper";
const tts = await Piper.carregar();
await (await tts.falar("Olá!")).salvar("saida.wav");

Deno / Bun

import { fonemizar } from "npm:@pedrobef/vozz/g2p";

Mais exemplos em exemplos/README.md.


API

| Símbolo | Descrição | | --- | --- | | Piper.carregar(opcoes?) | Baixa o modelo e inicializa. Devolve Piper. | | Piper.fonemizar(texto) | Texto → IPA, sem sintetizar. | | Piper.usarRuntime(ort) | Injeta o runtime ONNX manualmente. | | Piper.limparCache() | Remove o modelo do cache do navegador. | | tts.falar(texto, opcoes?) | Sintetiza tudo. Devolve Audio. | | tts.falarEmFluxo(entrada, o?) | Gera frase a frase (async generator). | | new Sintetizador(opcoes?) | Motor por formantes, síncrono. | | fonemizar(texto, opcoes?) | G2P puro (@pedrobef/vozz/g2p). | | DivisorDeTexto | Divisor incremental para saída de LLM. | | Audio | tocar, parar, salvar, paraWav/Blob/URL, duracao. |

Opções de carregar: dispositivo (auto/wasm/webgpu), cdn, urlModelo, urlConfig, urlRuntime, cache, aoProgredir, threads.

Opções de falar: velocidade (0.5–2), ruido (expressividade), ruidoW, lexico, normalizar, maxFonemas (tamanho do bloco; padrão 360).

Para textos longos prefira falarEmFluxo: ele devolve cada trecho assim que fica pronto, em vez de esperar o texto inteiro.

Tipos TypeScript inclusos.


Problemas comuns

Corrigido na 0.2.2. Atualize:

npm i @pedrobef/vozz@latest

O runtime agora é importado do CDN por URL absoluta, que o navegador resolve sozinho — nenhum bundler precisa resolvê-lo. Não é preciso instalar onnxruntime-web nem mexer em optimizeDeps.

Se você usa uma CSP restritiva, veja "CSP bloqueando o CDN" abaixo.

[vozz] Falha ao criar a sessão de inferência: Can't create a session.
ERROR_CODE: 9, ERROR_MESSAGE: Could not find an implementation for
ConvInteger(10) node with name '/enc_p/encoder/attn_layers.0/conv_q/Conv_quant'

Corrigido na 0.2.5. Se você usa o modelo padrão, basta atualizar:

npm i @pedrobef/vozz@latest

Se aponta para um modelo próprio (urlModelo / cdn), continue lendo.

Por que acontece. O ONNX Runtime implementa o operador ConvInteger somente para pesos uint8 (inteiro sem sinal). Um modelo quantizado com QuantType.QInt8 (com sinal) carrega o grafo normalmente, mas nenhum kernel casa com os tipos — e a sessão falha em todos os backends, WASM inclusive. Não é problema de WebGPU nem de navegador.

Como corrigir. Re-quantize a partir do modelo em fp32 usando QUInt8:

from onnxruntime.quantization import quantize_dynamic, QuantType

quantize_dynamic(
    "pt_BR-faber-medium.onnx",        # modelo fp32 original
    "pt_BR-faber-medium-uint8.onnx",  # saída
    weight_type=QuantType.QUInt8,     # ← a correção; NÃO use QInt8
)

O arquivo de saída tem o mesmo tamanho (18,7 MB) e a mesma qualidade de áudio. Os avisos Inference failed or unsupported type to quantize durante a conversão são normais e podem ser ignorados.

O fp32 original das vozes Piper está em rhasspy/piper-voices.

Como saber se o seu modelo tem o problema:

import onnx
from onnx import TensorProto

m = onnx.load("seu-modelo.onnx")
tipos = {i.name: i.data_type for i in m.graph.initializer}
for n in m.graph.node:
    if n.op_type == "ConvInteger":
        for e in n.input:
            if e in tipos:
                print(e, "INT8 (quebra)" if tipos[e] == TensorProto.INT8 else "UINT8 (ok)")
        break

Depois de re-quantizar, aponte o pacote para o novo arquivo:

await Piper.carregar({ urlModelo: "/modelo/pt_BR-faber-medium-uint8.onnx" });

Bug da versão 0.2.5, corrigido na 0.2.6:

npm i @pedrobef/vozz@latest

Era um ReferenceError meu: ao remover um bloco de código, a declaração de uma variável saiu junto, mas os usos ficaram. O erro aparecia dentro de Piper.carregar(), logo após o download do modelo.

O CI agora roda eslint com a regra no-undef antes de publicar, e há um teste que percorre carregar() inteiro com um runtime falso — os dois detectam esse tipo de falha.

Se a mensagem cita ConvInteger, veja o item acima — é quantização, não backend.

Caso contrário, force o WASM, que implementa todos os operadores:

await Piper.carregar({ dispositivo: "wasm" });

WebGPU só acelera modelos em fp32/fp16; com modelos quantizados ele repassa os nós ao WASM automaticamente.

Importe o Piper apenas em código de cliente (dentro de useEffect, onMount, ou await import() sob if (typeof window !== "undefined")).

No Next.js, se ainda ocorrer, marque como externo:

// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (isServer) config.externals = [...(config.externals ?? []), "onnxruntime-web"];
    return config;
  },
};

Se a sua Content-Security-Policy restringe origens, libere o jsDelivr:

script-src  'self' https://cdn.jsdelivr.net;
connect-src 'self' https://cdn.jsdelivr.net;
worker-src  'self' blob:;

Ou hospede tudo no seu domínio:

await Piper.carregar({
  cdn: "/modelo",                          // seu .onnx e .json
  urlRuntime: "/vendor/ort.min.mjs",       // build ESM do onnxruntime-web
});

Ele fica na Cache API do navegador. Se estiver rebaixando sempre:

  • Modo anônimo descarta o cache ao fechar a aba — é esperado.
  • Sem HTTPS: a Cache API exige contexto seguro (https:// ou localhost).
  • Cota cheia: o pacote ignora falhas de cache em silêncio e continua funcionando; libere espaço no navegador.

Para forçar o redownload: await Piper.limparCache().

Melhorado na 0.2.7. Atualize:

npm i @pedrobef/vozz@latest

O custo do modelo é linear: uma sentença muito longa virava uma única inferência que segurava a thread principal por vários segundos — a página não repintava e o áudio pronto não tocava. Medido aqui: 8,0 s de bloqueio numa frase de 479 fonemas.

Agora a síntese é fatiada em blocos (~360 fonemas), cortando em vírgulas e pausas naturais, e a thread é liberada entre eles. Mesmo texto: maior bloqueio caiu para 5,9 s e o primeiro áudio saiu 1,4× mais rápido, com o tempo total inalterado.

Para textos realmente longos, use streaming. Assim o usuário ouve a primeira frase enquanto o resto é gerado:

for await (const { audio } of tts.falarEmFluxo(textoGrande)) {
  await audio.tocar();
}

Ajustes finos:

// blocos menores = interface mais fluida, mais chamadas
await tts.falar(texto, { maxFonemas: 200 });

// mais threads (exige COOP/COEP; veja "A síntese está lenta")
await Piper.carregar({ threads: 2 });

Se ainda travar, o gargalo provavelmente é a thread principal do navegador. A solução definitiva é rodar a síntese num Web Worker — o pacote funciona dentro de um sem alteração.

Ative WASM multi-thread com estes cabeçalhos (SharedArrayBuffer):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

No Cloudflare Pages, crie um arquivo _headers na raiz do deploy:

/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp

Outras opções: use falarEmFluxo para tocar a primeira frase enquanto o resto gera; ou Sintetizador, que é ~70× tempo real (voz robótica).

Atenção: COEP quebra recursos de terceiros sem CORS. Se algo parar de carregar, teste sem esses cabeçalhos primeiro.

Em Node não há import por URL. Instale o pacote nativo:

npm i onnxruntime-node

Importar /piper no Worker é seguro (0.2.1+), mas a síntese neural não roda no edge — falta memória para o WASM. Use o sintetizador:

import { Sintetizador } from "@pedrobef/vozz/sintetizador";

Padrão recomendado: o Worker devolve os fonemas (/g2p, rápido e leve) e o navegador faz a síntese neural.

Nomes próprios e estrangeirismos podem escapar do léxico. Corrija com IPA:

await tts.falar("Trabalho na Zendesk.", {
  lexico: { zendesk: "zẽndˈɛski" },
});

Use Piper.fonemizar("palavra") para ver o que o G2P está gerando.

Navegadores móveis exigem que a reprodução venha de um gesto do usuário. Chame tocar() dentro do handler de clique — não em useEffect nem em setTimeout:

botao.onclick = async () => (await tts.falar("Olá!")).tocar();

Quanto pesa

| Item | Tamanho | Quando | | --- | --- | --- | | código do vozz | ~52 kB | sempre | | /g2p sozinho | ~36 kB | se usar só os fonemas | | runtime ONNX (JS) | ~435 kB | só com o Piper | | runtime ONNX (WASM) | ~11 MB | só com o Piper | | modelo de voz | 18,7 MB | só com o Piper, uma vez |

Tudo fica em cache. Se o peso for proibitivo, vozz/sintetizador gera voz sem baixar nada (~72 kB, roda no edge) — mais robótica, mas inteligível.


Desenvolvimento

npm test          # 39 testes, incluindo a fidelidade do G2P
npm run qualidade # relatório acústico do sintetizador
npm run eval      # PER contra o espeak-ng
npm run pages     # demo local em http://localhost:8080

Créditos e licença

Código em Apache-2.0. Uso comercial liberado.

  • Modelo de voz: Piper (VITS), voz pt_BR-faber-medium, quantizada em int8 — MIT.
  • Motor alternativo: Kokoro-82M de hexgrad — Apache-2.0.
  • Inferência: onnxruntime-web — MIT.

Os pesos não vão dentro do pacote: são baixados sob demanda.