Skip to content
5 min read · 1,027 words

Generation — Transformers.js

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.

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

OptionTypeDefaultNotes
modelstring(required)e.g. 'onnx-community/Janus-Pro-1B-ONNX'. No default anywhere in this domain.
janusModelTransformersJsGenerationModelPre-built model instance; skips lazy creation entirely.
processorTransformersJsGenerationProcessorPre-built processor instance; skips lazy creation entirely.
createModelCreateTransformersJsGenerationModeldynamic-import MultiModalityCausalLM.from_pretrainedOverride the model factory.
createProcessorCreateTransformersJsGenerationProcessordynamic-import AutoProcessor.from_pretrainedOverride the processor factory.
devicestringenvironment defaultForwarded to from_pretrained().
dtypestringenvironment defaultQuantization/precision, forwarded to from_pretrained().
modelSource(req: {repo, filename}) => bytes|string|Response|undefinedCustom OPFS/other byte resolver; falls through to HF when it returns undefined.
onInitProgress(info: unknown) => voidModel-load progress callback.
isAvailable() => booleanalways returns true (see below)Override the availability probe.
encodeImage(image) => Promise<Uint8Array>env-branched toBlob/toSharpBypasses the PNG-encoding env-branch entirely — the primary hermetic-test seam.
doSamplebooleantruedo_sample. Matches the model card's own image-generation example.
temperaturenumbertemperature, read only when doSample is truthy.
topKnumbertop_k, read directly in MultinomialSampler.sample().
guidanceScalenumberguidance_scale; classifier-free guidance activates when > 1.
repetitionPenaltynumberrepetition_penalty.
minNewTokensnumberprocessor.num_image_tokensmin_new_tokens — the fixed per-image token budget when omitted.
maxNewTokensnumberprocessor.num_image_tokensmax_new_tokens — the fixed per-image token budget when omitted.
chatTemplatestring'text_to_image'Forwarded to the processor call.
rolestring'<|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.

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.

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 or 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 (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 for the exact wiring.

Exceptions

ExceptionStatusFatal?Thrown when
E_INVALID_TRANSFORMERS_JS_GENERATION_OPTIONS529YesAdapter options fail schema validation (missing/empty model, unknown key).
E_TRANSFORMERS_JS_GENERATION_ENGINE_ERROR502NoModel/processor fail to load, generate_images throws, or the result is empty.
E_TRANSFORMERS_JS_GENERATION_UNSUPPORTED_OPERATION501Yesedit() is called — always, unconditionally.

Where to go next