Transformers.js no Node.js: Implementando Inferência de Modelos Hugging Face
Transformers.js no Node.js: Implementando Inferência de Modelos Hugging Face
Plataformas como a Hugging Face popularizaram os transformers, modelos de linguagem poderosos para processamento de texto. Tradicionalmente, esses modelos eram usados através de bibliotecas Python, mas hoje podemos executá-los diretamente em JavaScript com o Transformers.js. Neste artigo, você aprenderá como criar uma API em Node.js que usa o Transformers.js para inferência de modelos de linguagem do Hugging Face. Abordaremos desde a configuração do projeto até aspectos avançados, como a escolha entre módulos ESM e CommonJS e dicas para otimizar o desempenho e obter inferências mais rápidas.
O que você vai aprender neste tutorial:
- Configurar um projeto Node.js para usar o Transformers.js.
- Carregar e executar modelos de linguagem pré-treinados (por exemplo, para análise de sentimento ou tradução).
- Diferenças entre ESM e CommonJS no Node e como usá-los com o Transformers.js.
- Criar uma API simples (com servidor HTTP ou Express) que responde com o resultado da inferência.
- Boas práticas de performance: inicialização do pipeline, reuso de instâncias e melhorias.
Vamos por partes, começando do básico!
O que é o Transformers.js?
O Transformers.js é uma biblioteca JavaScript que permite rodar modelos 🤗 Hugging Face Transformers dentro de aplicações JavaScript, incluindo o Node.js. Ela traz para o mundo JavaScript a mesma funcionalidade da biblioteca Transformers original do Python. Ou seja, você pode carregar e usar modelos como BERT, GPT, RoBERTa e muitos outros, diretamente no seu código JS.
Um conceito central é o pipeline. Pense no pipeline como uma sequência de etapas pré-definidas: ele já sabe como carregar o modelo apropriado, pré-processar o texto de entrada e tratar a saída para você. Por exemplo, o pipeline de text-classification ou sentiment-analysis prepara tudo para classificar o texto em sentimentos (positivo, negativo, etc). Na prática, usamos algo como:
import { pipeline } from '@huggingface/transformers'; let classificatorio = await pipeline('sentiment-analysis'); let resultado = await classificatorio("Eu adoro programação!"); console.log(resultado);
Aqui, nosso pipeline de análise de sentimento transformou o texto “Eu adoro programação!” em uma resposta indicando sentimento positivo. O Transformers.js torna essa transição de Python para JS muito natural!
Configurando o projeto Node.js
Antes de mais nada, certifique-se de ter instalado Node.js (versão 18 ou superior) e npm (versão 9+). Esses são os requisitos básicos para usar o Transformers.js no servidor.
-
Inicialize o projeto:
Abra um terminal na pasta do seu projeto e rode:npm init -yIsso criará o
package.jsonbásico. -
Instale o Transformers.js:
O nome do pacote na npm para a versão atual é@huggingface/transformers. Rode:npm install @huggingface/transformers(Nas versões mais antigas, o pacote era
@xenova/transformers, mas agora use o@huggingface/transformers, que é o nome oficial desde 2024.) -
Configuração de módulos:
Por padrão, Node.js usa o sistema CommonJS (comrequire()), mas muitos projetos modernos optam por ESM (ECMAScript Modules), usandoimport. Para usar ESM no Node, adicione isto no seupackage.json:{ "type": "module" }Com
"type": "module", arquivos.jsserão tratados como módulos ECMAScript. Sem essa configuração, você fica no modo tradicional CommonJS. A escolha afeta a forma como importamos o Transformers.js, como veremos logo a seguir.
ESM vs. CommonJS: Qual usar?
No Node.js existem dois sistemas de módulos principais:
- ESM (ECMAScript Modules): Usa
importeexport. É o padrão mais moderno, compatível com navegadores e suportado nativamente pelo Node (a partir v13.2.0, mas funciona bem no Node 18+). - CommonJS: Usa
require()emodule.exports. É o formato tradicional do Node.
O Transformers.js em sua versão atual é um módulo ESM. Isso significa que, se você estiver usando CommonJS, precisará fazer um import() dinâmico ao invés de const ... require(). Abaixo damos exemplos práticos de ambas as abordagens:
Usando ESM (moderno)
Se você definiu "type": "module" no package.json, basta escrever:
import { pipeline } from '@huggingface/transformers'; // Exemplo: usando um pipeline de classificação de texto async function exemploPipeline() { const classificatorio = await pipeline('text-classification', 'nlptown/bert-base-multilingual-uncased-sentiment'); const resposta = await classificatorio("Este filme foi fantástico!"); console.log(resposta); } exemploPipeline();
Neste código, usamos await pipeline(...) para criar uma instância do pipeline text-classification com um modelo multilingue. Em seguida, passamos um texto e imprimimos o resultado (tipicamente um rótulo e uma pontuação de confiança).
Usando CommonJS (tradicional)
Se você prefere manter "type": "commonjs" ou não quer mudar o package.json, use import() dinâmico. Por exemplo:
async function exemploPipeline() { // Importa dinamicamente o módulo Transformers.js const { pipeline } = await import('@huggingface/transformers'); // Cria e usa o pipeline normalmente const classificatorio = await pipeline('text-classification', 'nlptown/bert-base-multilingual-uncased-sentiment'); const resposta = await classificatorio("Este filme foi fantástico!"); console.log(resposta); } exemploPipeline();
Aqui usamos await import('...') dentro da função assíncrona para obter o pipeline. Depois é igualzinho: criamos e usamos o pipeline.
Obs.: Queremos sempre usar
awaitao chamarpipeline(...), porque esse método é assíncrono e retorna uma promessa do pipeline carregado.
Criando a API de Inferência em Node.js
Com o ambiente pronto, vamos construir uma API simples que recebe texto e devolve a inferência do modelo. Vamos nos concentrar em um exemplo: análise de sentimento (text-classification).
Pipeline de Classificação de Texto
Primeiro, criamos uma classe helper que carrega o pipeline apenas uma vez (padrão singleton). Isso é importante para evitar recarregamentos desnecessários do modelo a cada requisição, o que consome tempo. Nossa classe armazenará a instância em uma variável estática e só criará o pipeline na primeira chamada:
// Exemplo em ESM (com "type": "module") import { pipeline } from '@huggingface/transformers'; class MeuPipelineClassificacao { static task = 'text-classification'; // Podemos escolher qualquer modelo compatível; este é só um exemplo. static model = 'nlptown/bert-base-multilingual-uncased-sentiment'; static instance = null; static async getInstance() { if (this.instance === null) { // Carrega o pipeline pela primeira vez this.instance = await pipeline(this.task, this.model); } return this.instance; } }
No caso CommonJS, ficaria assim:
class MeuPipelineClassificacao { static task = 'text-classification'; static model = 'nlptown/bert-base-multilingual-uncased-sentiment'; static instance = null; static async getInstance() { if (this.instance === null) { const { pipeline } = await import('@huggingface/transformers'); this.instance = await pipeline(this.task, this.model); } return this.instance; } } // Não se esqueça de exportar ou usar essa classe no seu servidor Node.
Na classe MeuPipelineClassificacao, o método getInstance() é assíncrono e garante que sempre teremos APENAS UMA instância do pipeline carregada em memória. Isso é ótimo para performance, pois evita carregar o modelo repetidamente.
Servidor HTTP Básico
Vamos usar o módulo nativo http do Node.js para um servidor simples, sem dependências extras. Nosso servidor escutará requisições GET em /classify?text=... e retornará o resultado da inferência em JSON:
// Usando ESM import http from 'http'; import url from 'url'; import querystring from 'querystring'; // (Certifique-se de importar a classe MeuPipelineClassificacao definida antes) server.on('request', async (req, res) => { const parsedUrl = url.parse(req.url); const path = parsedUrl.pathname; const query = querystring.parse(parsedUrl.query); res.setHeader('Content-Type', 'application/json'); if (path === '/classify' && query.text) { try { const pipeline = await MeuPipelineClassificacao.getInstance(); const output = await pipeline(query.text); res.writeHead(200); res.end(JSON.stringify(output)); } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: String(err) })); } } else { res.writeHead(400); res.end(JSON.stringify({ error: 'Uso: /classify?text=seu_texto_aqui' })); } }); // Começa o servidor const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer(server.on); server.listen(port, hostname, () => { console.log(`Servidor rodando em http://${hostname}:${port}/`); });
Esse servidor é bem simples: ele extrai o parâmetro text da query string e passa para nosso pipeline de classificação. O resultado (por exemplo, [{ label: 'POSITIVE', score: 0.99 }]) é retornado no corpo da resposta. Se algo der errado, retornamos um erro em JSON.
Obs.: Você pode usar Express ou outro framework se preferir. A lógica principal (carregar o pipeline e chamar o modelo) continua a mesma, apenas mudaria a forma de definir rotas.
Usando Express (opcional)
Para projetos mais complexos, muitos desenvolvedores usam Express. O código equivalente em Express poderia ser:
import express from 'express'; import { MeuPipelineClassificacao } from './MeuPipelineClassificacao.js'; const app = express(); app.get('/classify', async (req, res) => { const text = req.query.text; if (!text) { return res.status(400).json({ error: 'Texto não informado.' }); } try { const pipeline = await MeuPipelineClassificacao.getInstance(); const output = await pipeline(text); res.json(output); } catch (err) { res.status(500).json({ error: String(err) }); } }); app.listen(3000, () => { console.log('API rodando na porta 3000.'); });
Ambas abordagens (http básico e Express) ilustram o mesmo ponto: a integração com Transformers.js é bem direta uma vez que o pipeline foi carregado.
Boas práticas e otimizações de desempenho
Modelos de linguagem são poderosos, mas pesados. Alguns cuidados podem melhorar a velocidade de inferência e a experiência:
-
Cache do Pipeline: Como vimos, carregue o modelo apenas uma vez. Use o padrão singleton ou carregue na inicialização do servidor, em vez de criar um pipeline novo a cada requisição. Isso economiza o tempo de download e carregamento do modelo, que pode levar segundos.
-
Pré-aquecimento (warm-up): Se possível, invoque o pipeline uma vez no servidor na inicialização (talvez com um input dummy) para "aquecê-lo". Assim, o primeiro usuário não sofre a latência do load. Exemplão:
// Após definir MeuPipelineClassificacao MeuPipelineClassificacao.getInstance().then(() => { console.log('Pipeline carregado e pronto para uso!'); }); -
Escolha de modelo adequado: Modelos menores geralmente inferem mais rápido. Se não precisar de extrema precisão, opte por versões distilled ou quantizadas (muitos modelos no Hugging Face têm variantes "distilled" ou "quantized"). Por exemplo, em vez de
bert-base-uncased, usedistilbert-base-uncasedpara ganhar velocidade com pouca perda de qualidade. -
Execução assíncrona: Em Node.js, aproveite que o pipeline é assíncrono para não bloquear o loop principal. Nosso servidor já usa
await, mas em projetos mais pesados você pode rodar vários requests em paralelo (middleware, filas de tarefa, etc.), dependendo das necessidades. -
Recursos de hardware: O Transformers.js roda por padrão usando WebAssembly (WASM), que utiliza CPU. Se você tiver suporte a WebGPU (por exemplo, em navegadores ou Node experimental com suporte a GPU), é possível acelerar MUITO (até 100x, conforme os desenvolvedores do Transformers.js). No Node 20+ existe suporte experimental a WebGPU; você poderia experimentar passar
{ device: 'webgpu' }no pipeline:// Exemplo hipotético (precisa de suporte WebGPU no Node): const pipeline = await pipeline('text-generation', 'gpt2', { device: 'webgpu' });Porém, essa configuração pode não funcionar em todos os ambientes Node. Se não for o caso, foque nas dicas acima.
-
Ambiente de execução: Certifique-se de que o Node.js tem memória suficiente. Modelos maiores podem exigir centenas de MB de RAM. Se encontrar erros de memória, use versões menores dos modelos ou ajuste os recursos do servidor.
-
Tarefas em lote: Se for processar múltiplos textos de uma vez, você pode enviar um array de textos ao pipeline (muitos pipelines aceitam listas), processando-os em lote de forma mais eficiente do que requisições separadas.
-
Redução de saída: Por fim, reduza o que você envia na resposta. Por exemplo, se só interessa o label de maior confiança, filtre no servidor ao invés de enviar toda a estrutura. Isso não acelera a inferência, mas diminui o tempo de rede e processamento adicional.
Em resumo: um bom desempenho vem de evitar cargas repetidas, usar o modelo certo e, quando possível, habilitar aceleração de hardware.
Conclusão
Executar modelos Hugging Face no Node.js ficou muito mais fácil graças ao Transformers.js. Neste artigo, vimos passo a passo como configurar seu projeto e criar uma API de inferência em Node. Os pontos-chave são:
- Instalação e Importação: Instalamos
@huggingface/transformerse aprendemos como importar corretamente em ESM ou CommonJS no Node. - Uso de Pipeline: Criamos pipelines de text-classification, que abstraem o modelo, o pré-processamento (tokenização) e o pós-processamento.
- Servidor de Inferência: Montamos uma API simples que recebe texto e retorna o resultado da inferência em JSON, facilmente adaptável para diferentes tarefas (ex.: tradução, geração de texto, etc.)
- Desempenho: Abordamos boas práticas, como reutilizar a instância do pipeline e considerar modelos otimizados para diminuir a latência.
No futuro, você pode estender este exemplo para outras tarefas: Geração de texto, tradução, reconhecimento de entidade, e assim por diante. O Hugging Face Hub possui centenas de modelos compatíveis; basta trocar o identificador no pipeline. Além disso, fique atento às atualizações do Transformers.js, que apenas tornam a biblioteca mais rápida e poderosa (como o suporte a WebGPU no v3+).
Agora é sua vez: experimente criar sua própria API, teste diferentes modelos e veja o Transformer.js transformando suas aplicações JavaScript!
Inscrever agora para a próxima turma do DevClub?
Que tal não perder esta oportunidade e já se inscrever agora para a próxima turma do DevClub?