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

ngx-transformers

v0.4.0

Published

Run Hugging Face Transformers.js models in Angular - on-device ML with a signals API: text classification, zero-shot classification, embeddings, semantic search, translation, Whisper speech-to-text and small LLMs with streaming, in a Web Worker if you lik

Readme

ngx-transformers

npm version npm downloads CI license: MIT

Run Hugging Face Transformers.js models in Angular - on-device ML with a signals API. Text classification, zero-shot classification, sentence embeddings, semantic search, translation, Whisper speech-to-text and small LLMs with streaming, executed entirely in the browser (in a Web Worker if you like): no server, no API key, works offline once the model is cached.

Documentation · Demo app · Storybook - the demos load real models in your browser.

Why

Transformers.js has a React tutorial and hooks ecosystem - Angular has nothing. This library closes that gap with idiomatic Angular: lazily-loaded pipelines wrapped in signals, DI-friendly configuration, automatic cleanup with the owning component, and a drop-in progress component for the model download.

Install

npm i ngx-transformers @huggingface/transformers

@huggingface/transformers (v4) is a peer dependency. Angular >= 22.

Quick start

import { Component, signal } from '@angular/core';
import { createTextClassifier, ModelProgressComponent } from 'ngx-transformers';

@Component({
  imports: [ModelProgressComponent],
  template: `
    <textarea #box></textarea>
    <button (click)="analyze(box.value)" [disabled]="classifier.busy()">Analyze</button>
    <ngx-model-progress [status]="classifier.status()" [progress]="classifier.progress()" />
    @if (label(); as l) { <strong>{{ l }}</strong> }
  `,
})
export class SentimentComponent {
  readonly classifier = createTextClassifier(); // no download yet - lazy
  readonly label = signal<string | null>(null);

  async analyze(text: string) {
    const [top] = await this.classifier.classify(text); // downloads model on first call
    this.label.set(`${top.label} ${(top.score * 100).toFixed(1)}%`);
  }
}

The model downloads on the first classify() call (with progress reported through the progress signal), is cached by the browser, and is disposed automatically when the component is destroyed.

Semantic search

import { createTextEmbedder } from 'ngx-transformers';

readonly embedder = createTextEmbedder(); // all-MiniLM-L6-v2, ~23 MB q8

const ranked = await this.embedder.rank('how do I make my app faster?', docs);
// [{ text: 'Use trackBy and virtual scrolling...', score: 0.28, index: 2 }, ...]

const score = await this.embedder.similarity('car', 'automobile'); // ~0.8
const vectors = await this.embedder.embed(['one', 'two']); // number[][]

Speech to text (v0.2)

Whisper, fully in the browser - the audio never leaves the device:

import { createMicRecorder, createSpeechRecognizer } from 'ngx-transformers';

readonly whisper = createSpeechRecognizer(); // whisper-tiny.en, ~41 MB q4
readonly mic = createMicRecorder();

// a URL, File/Blob, ArrayBuffer, or 16 kHz Float32Array:
const { text, chunks } = await this.whisper.transcribe(fileOrUrl, { returnTimestamps: true });

// dictation:
async toggle() {
  if (this.mic.recording()) {
    const audio = await this.mic.stop();            // encoded Blob
    const { text } = await this.whisper.transcribe(audio); // decoded + resampled for you
  } else {
    await this.mic.start();                          // asks for mic permission
  }
}

MicRecorder exposes recording, seconds, and error signals for the UI. decodeAudio(blob) is exported separately if you want the 16 kHz mono Float32Array yourself.

Note: the recognizer defaults to dtype: 'q4' - q8 Whisper decoders currently fail on the v4 WASM runtime (transformers.js#1707). Multilingual checkpoints (e.g. onnx-community/whisper-tiny) accept language and task: 'translate' options.

Zero-shot classification

Score labels you name against a text, no fine-tuning - an NLI model judges whether "This example is {label}." follows from the text:

import { createZeroShotClassifier } from 'ngx-transformers';

readonly classifier = createZeroShotClassifier(); // mobilebert-uncased-mnli, ~26 MB q8

const scored = await this.classifier.classify(ticketText, ['billing', 'bug report', 'feature request']);
// [{ label: 'bug report', score: 0.91 }, { label: 'billing', score: 0.06 }, ...] - scores sum to 1

// several labels can apply at once - score each on its own:
const tags = await this.classifier.classify(text, ['food', 'repair', 'politics'], {
  multiLabel: true,
  hypothesisTemplate: 'This text is about {}.',
});

Xenova/distilbert-base-uncased-mnli (~80 MB) is the more accurate drop-in: createZeroShotClassifier({ model: '...' }).

Translation

Marian opus-mt checkpoints, one per language pair (~105 MB q8 each), resolved from the pair and loaded on demand:

import { createTranslator } from 'ngx-transformers';

readonly translator = createTranslator({ from: 'en', to: 'ru' }); // Xenova/opus-mt-en-ru

const russian = await this.translator.translate('The model runs entirely in the browser.');
const german = await this.translator.translate('Good morning.', { to: 'de' }); // opus-mt-en-de, its own download

The translator's status / progress signals follow the model used by the latest call, so one <ngx-model-progress> covers every pair; handleFor({ to: 'de' }) returns the underlying PipelineHandle when you want one per pair. Override the checkpoint for a pair globally with provideTransformers({ translationModels: { 'en-ru': 'my-org/en-ru-tiny' } }).

Multilingual checkpoints (NLLB, M2M100) serve every pair from one model and take the codes per call:

readonly translator = createTranslator({ model: 'Xenova/nllb-200-distilled-600M' });
await this.translator.translate(text, { from: 'eng_Latn', to: 'tgk_Cyrl' }); // src_lang / tgt_lang forwarded

Text generation

A small language model, streamed token by token:

import { createTextGenerator } from 'ngx-transformers';

readonly generator = createTextGenerator(); // SmolLM2-135M-Instruct, ~100 MB q4

const reply = await this.generator.generate(
  [{ role: 'user', content: 'Explain signals in Angular in two sentences.' }],
  { maxNewTokens: 120, onToken: (piece) => console.log(piece) },
);
// generator.output() holds the text generated so far while the model runs

Reactive inference

inferenceResource() runs a handle whenever an input signal changes, as an Angular resource with debounce and latest-wins:

readonly text = signal('');
readonly sentiment = inferenceResource({
  input: () => this.text().trim() || undefined, // undefined: idle
  run: (text) => this.classifier.classify(text),
  debounceMs: 300,
});
// template: sentiment.value(), sentiment.isLoading(), sentiment.error()

Web Worker

One provider moves every pipeline off the main thread; handles and signals work unchanged:

// transformers.worker.ts
/// <reference lib="webworker" />
import { runTransformersWorker } from 'ngx-transformers/worker';
runTransformersWorker();

// app.config.ts
provideTransformersWorker(
  () => new Worker(new URL('./transformers.worker', import.meta.url), { type: 'module' }),
);

ngx-transformers/worker has no Angular dependency, so the worker chunk stays tiny; @huggingface/transformers loads inside the worker only.

Any pipeline

createPipeline() exposes the full Transformers.js task surface with the same signal lifecycle:

import { createPipeline } from 'ngx-transformers';

readonly summarizer = createPipeline<string, { summary_text: string }[]>({
  task: 'summarization',
  model: 'Xenova/distilbart-cnn-6-6',
});

const [out] = await this.summarizer.run(longText);

Global configuration

import { provideTransformers } from 'ngx-transformers';

bootstrapApplication(App, {
  providers: [provideTransformers({ device: 'webgpu', dtype: 'q8' })],
});

Per-pipeline device/dtype/options win over the global config. translationModels maps a "from-to" pair to a checkpoint for createTranslator().

Every handle gets its pipeline from the PIPELINE_FACTORY injection token. Provide your own to add options or logging, route work to a Web Worker, or stub models in tests. createDefaultPipelineFactory() is the default; it lazy-imports @huggingface/transformers on the first pipeline, so wrapping it keeps the initial bundle small:

import { PIPELINE_FACTORY, createDefaultPipelineFactory, type PipelineFactory } from 'ngx-transformers';

const base = createDefaultPipelineFactory();
const logging: PipelineFactory = (task, model, options) => {
  console.log('loading', task, model);
  return base(task, model, options);
};

bootstrapApplication(App, {
  providers: [{ provide: PIPELINE_FACTORY, useValue: logging }],
});

API

Handles

| Export | What it is | |---|---| | createPipeline(request) | Generic PipelineHandle for any Transformers.js task | | createTextClassifier(options?) | TextClassifier - sentiment/classification, classify(text, topK?) | | createTextEmbedder(options?) | TextEmbedder - embed(), similarity(), rank() | | createSpeechRecognizer(options?) | SpeechRecognizer - transcribe(audio, options?) with timestamps | | createZeroShotClassifier(options?) | ZeroShotClassifier - classify(text, labels, { multiLabel?, hypothesisTemplate? }) | | createTranslator(options?) | Translator - translate(text, { from?, to? }), one model per pair, handleFor(pair) | | createTextGenerator(options?) | TextGenerator - generate(prompt, { maxNewTokens?, onToken?, ... }), streamed into output | | inferenceResource({ input, run, debounceMs? }) | An Angular resource that re-runs a handle when an input signal changes | | provideTransformersWorker(createWorker) | Run every pipeline in a Web Worker (ngx-transformers/worker runs the worker side) | | createMicRecorder(deps?) | MicRecorder - mic capture with recording/seconds/error signals | | cosineSimilarity(a, b) / decodeAudio(blob) | Standalone helpers | | PIPELINE_FACTORY / createDefaultPipelineFactory() | Swap or wrap how pipelines are created |

All create* functions must run in an injection context (field initializer, constructor, or runInInjectionContext); handles are destroyed with the surrounding component (the model is released and later calls reject). Call dispose() yourself to free a model early and load it again later.

PipelineHandle signals

| Signal | Type | Meaning | |---|---|---| | status | 'idle' \| 'loading' \| 'ready' \| 'busy' \| 'error' | Lifecycle; error only from a failed load, retryable | | progress | ModelProgress \| null | Download progress: the file reported last plus overall over every file of the model | | error | unknown | The load error, if any | | runError | unknown | The error of the most recent run, if it failed | | ready / busy | boolean (computed) | Convenience for buttons and spinners |

<ngx-model-progress>

Status line + download bar for any handle. Inputs: status (required), progress, labels (override per-status text). Themeable via --nt-accent, --nt-ink, --nt-muted, --nt-track.

Default models

| Wrapper | Model | Size (q8) | License | |---|---|---|---| | createTextClassifier | Xenova/distilbert-base-uncased-finetuned-sst-2-english | ~65 MB | Apache-2.0 | | createTextEmbedder | Xenova/all-MiniLM-L6-v2 | ~23 MB | Apache-2.0 | | createSpeechRecognizer | onnx-community/whisper-tiny.en | ~41 MB (q4) | Apache-2.0 | | createZeroShotClassifier | Xenova/mobilebert-uncased-mnli | ~26 MB | unlisted on the Hub (base MobileBERT: Apache-2.0) | | createTranslator | Xenova/opus-mt-{from}-{to} | ~105 MB per pair | varies per pair (Apache-2.0 or CC-BY-4.0) | | createTextGenerator | HuggingFaceTB/SmolLM2-135M-Instruct | ~100 MB (q4) | Apache-2.0 |

Swap any compatible checkpoint via { model: '...' }. Check the license of the model you ship.

SSR

Model loading is browser-only (WASM/WebGPU). Creating handles is safe on the server - nothing downloads until load()/run() - but call those only in browser code paths.

Device selection

WebGPU is several times faster than WASM where it works, but navigator.gpu existing is not enough: requestAdapter() can still hand back nothing on a machine without a usable GPU. hasWebGpu() runs that probe once (cached) and detectDevice() turns the answer into a device name; both resolve to the WASM answer on the server.

import { createTextClassifier, detectDevice } from 'ngx-transformers';

const device = await detectDevice(); // 'webgpu' | 'wasm'
const classifier = createTextClassifier({ device });

Or let every handle decide for itself:

bootstrapApplication(App, {
  providers: [provideTransformers({ autoDevice: true, dtype: 'q8' })],
});

With autoDevice, a handle that sets no device (or 'auto') probes WebGPU on its first load and uses it when available, else WASM. An explicit device on the handle or in the global config still wins. resetDeviceDetection() clears the cached answer.

License

MIT (c) Muzaffar Qosimov