---
url: 'https://adk.nht.io/batteries/generation/transformers_js.md'
description: >-
  The EXPERIMENTAL on-device Generation engine: DeepSeek Janus text-to-image via
  @huggingface/transformers, real sampling knobs, perf reality, no edit()
  support.
---

# Generation — Transformers.js

::: warning EXPERIMENTAL
This engine is newer and less battle-tested than the OpenAI/Gemini engines, downloads a ~2GB model on
first use, and takes on the order of minutes per image on CPU/WASM. Treat it as a viable path for
sovereignty-sensitive or offline deployments, not yet as a drop-in production default.
:::

## LLM summary — transformers.js Generation adapter

* [`TransformersJsGenerationAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/generation/transformers_js/adapter/classes/TransformersJsGenerationAdapter) — `@nhtio/adk/batteries/generation/transformers_js`. On-device
  text→image via DeepSeek Janus (`onnx-community/Janus-Pro-1B-ONNX` et al.), driven through
  `MultiModalityCausalLM.generate_images()` — the **only** image-generation surface transformers.js
  exposes. There is no `pipeline('text-to-image')` task; verified against the installed
  `@huggingface/transformers` source (`src/models/multi_modality/modeling_multi_modality.js`).
* Extends `BaseGenerationAdapterOptions` (`model` required, no default — e.g.
  `onnx-community/Janus-Pro-1B-ONNX`) plus `BatteryLifecycleHooks` (shared boot-progress contract with
  the other transformers.js batteries).
* `MultiModalityCausalLM`/`VLChatProcessor` are typed **structurally** (duck-typed local interfaces), not
  imported as values or hard types — same posture as every other transformers.js battery — so
  fake-model/fake-processor unit tests never load the real ~2GB peer.
* Real, source-verified knobs only (cross-checked against `GenerationConfig` defaults, `logits_sampler.js`,
  `_get_logits_processor` in `modeling_utils.js`, and the official model-card example): `doSample→do_sample`,
  `temperature→temperature`, `topK→top_k`, `guidanceScale→guidance_scale`,
  `repetitionPenalty→repetition_penalty`, `minNewTokens`/`maxNewTokens→min_new_tokens`/`max_new_tokens`.
  **`top_p` is deliberately NOT exposed** — the `TopPLogitsWarper` branch is dead/commented-out code in
  the installed build; `MultinomialSampler.sample()` never reads it.
* `generate()`: builds a single-turn conversation via the processor (`processor([{ role, content: prompt
  }], { chat_template })`), merges knobs into a snake\_case options bag, calls
  `model.generate_images(options)`. An empty result array throws `E_TRANSFORMERS_JS_GENERATION_ENGINE_ERROR`
  (empty is a failure, not a valid empty generation). Each resolved `RawImage`-like result is encoded to
  PNG via `rawImageToEncodedBytes` (env-branched `toBlob`/`toSharp`, or the `encodeImage` test seam) —
  output `mimeType` is always `'image/png'`.
* `edit()` **always throws** `E_TRANSFORMERS_JS_GENERATION_UNSUPPORTED_OPERATION(['edit', reason])` — Janus
  is text-conditioned generation only; there is no image-conditioned edit/inpaint entry point in the
  installed API.
* Seams: `janusModel`/`processor` (pre-built instances, skip lazy creation), `createModel`/
  `createProcessor` (factory overrides), `modelSource` (OPFS/custom byte resolver), `device`/`dtype`,
  `onInitProgress`, `isAvailable` override, `encodeImage` (bypasses the `toBlob`/`toSharp` env-branch
  entirely — the primary hermetic-test seam).
* Perf reality: ~2GB model download on first use; no WebGPU requirement (ONNX Runtime auto-selects an
  execution provider); expect minutes per image on WASM/CPU. Running it behind
  [`forkIsolated`](/batteries/isolation/node) keeps that weight and latency off the host process/thread —
  see [Recipes](./recipes#recipe-d-running-the-on-device-engine-behind-isolation).

The transformers.js engine is the only one in this domain that runs entirely on-device — no API key, no
network call per image, at the cost of a large model download and materially slower per-image latency. It
drives DeepSeek's Janus model family through `@huggingface/transformers`' `MultiModalityCausalLM`.

```ts
import { TransformersJsGenerationAdapter } from '@nhtio/adk/batteries/generation/transformers_js'

const generator = new TransformersJsGenerationAdapter({
  model: 'onnx-community/Janus-Pro-1B-ONNX',
})

const [image] = await generator.generate('A watercolor illustration of a lighthouse at dawn', {
  doSample: true,
})
```

## Janus-only, no pipeline task

`@huggingface/transformers` does not expose a `pipeline('text-to-image')` task — text-to-image generation
only exists through Janus's own `MultiModalityCausalLM.generate_images()` method, confirmed against the
installed package's own source. That means this adapter is necessarily model-family-specific: it targets
Janus checkpoints (e.g. `onnx-community/Janus-Pro-1B-ONNX`), not an arbitrary diffusion model, and does not
generalize to other transformers.js model families the way the embeddings/caption batteries' `pipeline()`
calls do.

## Options

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `model` | `string` | *(required)* | e.g. `'onnx-community/Janus-Pro-1B-ONNX'`. No default anywhere in this domain. |
| `janusModel` | `TransformersJsGenerationModel` | — | Pre-built model instance; skips lazy creation entirely. |
| `processor` | `TransformersJsGenerationProcessor` | — | Pre-built processor instance; skips lazy creation entirely. |
| `createModel` | `CreateTransformersJsGenerationModel` | dynamic-import `MultiModalityCausalLM.from_pretrained` | Override the model factory. |
| `createProcessor` | `CreateTransformersJsGenerationProcessor` | dynamic-import `AutoProcessor.from_pretrained` | Override the processor factory. |
| `device` | `string` | environment default | Forwarded to `from_pretrained()`. |
| `dtype` | `string` | environment default | Quantization/precision, forwarded to `from_pretrained()`. |
| `modelSource` | `(req: {repo, filename}) => bytes\|string\|Response\|undefined` | — | Custom OPFS/other byte resolver; falls through to HF when it returns `undefined`. |
| `onInitProgress` | `(info: unknown) => void` | — | Model-load progress callback. |
| `isAvailable` | `() => boolean` | always returns `true` (see below) | Override the availability probe. |
| `encodeImage` | `(image) => Promise<Uint8Array>` | env-branched `toBlob`/`toSharp` | Bypasses the PNG-encoding env-branch entirely — the primary hermetic-test seam. |
| `doSample` | `boolean` | `true` | `do_sample`. Matches the model card's own image-generation example. |
| `temperature` | `number` | — | `temperature`, read only when `doSample` is truthy. |
| `topK` | `number` | — | `top_k`, read directly in `MultinomialSampler.sample()`. |
| `guidanceScale` | `number` | — | `guidance_scale`; classifier-free guidance activates when `> 1`. |
| `repetitionPenalty` | `number` | — | `repetition_penalty`. |
| `minNewTokens` | `number` | `processor.num_image_tokens` | `min_new_tokens` — the fixed per-image token budget when omitted. |
| `maxNewTokens` | `number` | `processor.num_image_tokens` | `max_new_tokens` — the fixed per-image token budget when omitted. |
| `chatTemplate` | `string` | `'text_to_image'` | Forwarded to the processor call. |
| `role` | `string` | `'<\|User\|>'` | Conversation role for the single-turn prompt (Janus convention). |

The nine sampling/template knobs — `doSample`, `temperature`, `topK`, `guidanceScale`,
`repetitionPenalty`, `minNewTokens`, `maxNewTokens`, `chatTemplate`, `role` — can also be passed
per-call to `generate()`, overriding the adapter-level default of the same name. `model` and every
construction seam (`janusModel`/`processor`/`createModel`/`createProcessor`/`device`/`dtype`/
`modelSource`/`onInitProgress`/`isAvailable`/`encodeImage`) are constructor-only.

::: warning `isAvailable()` always returns `true`
Unlike a WebGPU-gated battery, transformers.js is environment-neutral, so the default availability
probe unconditionally returns `true` — it does **not** test whether `@huggingface/transformers` is
actually installed. A missing optional peer surfaces later, as an
`E_TRANSFORMERS_JS_GENERATION_ENGINE_ERROR` thrown from `preload()`/`generate()` when the lazy import
fails. If you need a real up-front capability check, supply your own `isAvailable` override, or handle
the lazy-load failure at first use.
:::

::: tip `top_p` is intentionally absent
Unlike `topK`, there is no `topP` knob. The installed `@huggingface/transformers` build's
`TopPLogitsWarper` branch is dead/commented-out code — `MultinomialSampler.sample()` (the sampler
`generate_images` actually drives) never reads `top_p`. Exposing it would be a documented option that
silently does nothing, so it is omitted rather than shipped as a no-op.
:::

## `generate()`

```ts
const outputs = await generator.generate('A neon-lit alley in the rain', {
  doSample: true,
  temperature: 1.0,
  topK: 100,
  guidanceScale: 5,
})
```

Internally: the processor builds the conversation tensors (`processor([{ role, content: prompt }], {
chat_template: chatTemplate })`), the per-call and adapter-level knobs are merged into a single snake\_case
options bag (`do_sample`, `temperature`, `top_k`, `guidance_scale`, `repetition_penalty`,
`min_new_tokens`, `max_new_tokens` — the last two default to the processor's own
`num_image_tokens`, mirroring the official model-card example verbatim), and
`model.generate_images(options)` runs. An empty result array throws
`E_TRANSFORMERS_JS_GENERATION_ENGINE_ERROR` — this engine treats "no images" as a failure, never a valid
empty success. Each `RawImage`-like result is encoded to PNG bytes via `rawImageToEncodedBytes` (see
below); every output's `mimeType` is `'image/png'`.

## `edit()` is unsupported

```ts
await generator.edit([{ bytes, mimeType: 'image/png' }], 'Add a red bicycle')
// throws E_TRANSFORMERS_JS_GENERATION_UNSUPPORTED_OPERATION(['edit', '...'])
```

Janus's `MultiModalityCausalLM.generate_images()` is text-conditioned image generation only — there is no
image-conditioned edit/inpaint entry point anywhere in the installed `@huggingface/transformers` API. This
call always throws; it is not conditionally supported based on model or options. If you need edits, use
the [OpenAI](./openai) or [Gemini](./gemini) engine.

## Perf reality

* **Download**: ~2GB of model weights on first use (cached thereafter via the standard transformers.js
  cache, or your own `modelSource` resolver).
* **Latency**: minutes per image on CPU/WASM. There is no WebGPU requirement — ONNX Runtime auto-selects
  an execution provider — but this engine is materially slower than the OpenAI/Gemini engines regardless
  of accelerator.
* **Isolation**: because of the download size and per-call latency, running this engine behind
  [`forkIsolated`](/batteries/isolation/node) (construct the real adapter inside the guest process,
  expose a thin `generate`/`edit` facade host-side) keeps that weight off your main process — see
  [Recipes](./recipes#recipe-d-running-the-on-device-engine-behind-isolation) for the exact wiring.

## Exceptions

| Exception | Status | Fatal? | Thrown when |
| --- | --- | --- | --- |
| `E_INVALID_TRANSFORMERS_JS_GENERATION_OPTIONS` | 529 | Yes | Adapter options fail schema validation (missing/empty `model`, unknown key). |
| `E_TRANSFORMERS_JS_GENERATION_ENGINE_ERROR` | 502 | No | Model/processor fail to load, `generate_images` throws, or the result is empty. |
| `E_TRANSFORMERS_JS_GENERATION_UNSUPPORTED_OPERATION` | 501 | Yes | `edit()` is called — always, unconditionally. |

## Where to go next

* [Recipes](./recipes#recipe-d-running-the-on-device-engine-behind-isolation) — running this engine behind
  `forkIsolated`, unchanged.
* [Isolation battery](/batteries/isolation/) — the wrapper this engine is designed to run behind.
* [OpenAI](./openai) / [Gemini](./gemini) — the engines to reach for when you need `edit()`.
* [Assembly → Generation batteries](/assembly/batteries-generation) — full option/exception tables, all three engines.
