anyformer 0.4.13 → 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/backends/diskformer-onnx.js +22 -5
- package/src/load.js +2 -2
- package/src/models.js +4 -3
- package/src/sources.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.14 — 2026-10-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `diskformer-onnx`: LFM3-Omni speech out. `load(MODELS.chat.LFM3_OMNI_2B, { speech: true })` also fetches the depth
|
|
7
|
+
and codec decoders (515 MB, Cache Storage 'diskformer-models'); `generate(messages, { speech: true })` answers in
|
|
8
|
+
interleaved text and speech, and a chunk with new speech carries `audio` (24 kHz Float32 PCM since the last chunk)
|
|
9
|
+
and `sampleRate`. `seed`, `audioTemperature`, `audioRepetitionPenalty` and `audioRepetitionWindow` pass through.
|
|
10
|
+
Catalog flag `audioOut: true` on `MODELS.chat.LFM3_OMNI_2B`.
|
|
11
|
+
- Browser test `omni-speech` (Chrome, this Mac, 2026-10-30, local files): the reference speech clip in, a spoken reply
|
|
12
|
+
that names Mount Everest, 13.5 s of 24 kHz speech in 15 chunks over 9.3 s (first sound after 1.1 s), RMS 0.037
|
|
13
|
+
(the reference's 0.048); an abort from the first speech chunk ended the reply after 0.9 s of speech; a text reply
|
|
14
|
+
after it still equals the text reference. Code-level exactness is diskformer's own gate.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- diskformer pin 48f2c2a → 1fe19f8 (v0.6.12, LFM3-Omni speech out; changes only `engines/lfm3_omni.js`). The `omni`
|
|
18
|
+
test passes again on it (3/3 equal to the reference, streaming, abort, tool call).
|
|
19
|
+
|
|
3
20
|
## 0.4.13 — 2026-10-30
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measu
|
|
|
55
55
|
| Back end | Runs | Models |
|
|
56
56
|
|---|---|---|
|
|
57
57
|
| `transformers` | transformers.js 4.3.2: text generation (Nemotron 3.5 Bolt E4B and LFM3 1.7B among them), image and audio chat (Gemma 4, Qwen3.5), embeddings, speech to text, detection, classification, depth, segmentation | ONNX repos in the onnx-community / Xenova layout |
|
|
58
|
-
| `diskformer-onnx` | diskformer's hand engines on onnxruntime-web WebGPU, for ONNX models transformers.js has no class for | LFM3-Omni-2B: text, image and speech in
|
|
58
|
+
| `diskformer-onnx` | diskformer's hand engines on onnxruntime-web WebGPU, for ONNX models transformers.js has no class for | LFM3-Omni-2B: text, image and speech in; text or text and speech out |
|
|
59
59
|
| `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion`; Nemotron-Labs-Diffusion-3B |
|
|
60
60
|
| `localmind` | LocalMind's hand-written WebGPU kernels (shipped in diskformer), the whole model on the GPU | any LFM2 / LFM2.5 GGUF; Gemma 4 E2B / E4B QAT-mobile; Ternary Bonsai 2 |
|
|
61
61
|
| `wllama` | llama.cpp in WebAssembly: WebGPU when the budget holds the model, the CPU otherwise (no WebGPU needed) | any GGUF up to about 3.6 GB that no engine above covers; files over 2 GB are split into OPFS shards once |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "One call to run a model in a browser tab: anyformer reads the Hub repo, probes the browser, and picks the back end (transformers.js, LocalMind's WebGPU kernels, diskformer, kohra) that fits the model's format, architecture and your GPU budget.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -11,7 +11,9 @@ import { SOURCES } from '../sources.js';
|
|
|
11
11
|
|
|
12
12
|
// Keyed by config.model_type. graphs: the ONNX files the engine reads (each with its _data file); json: the small files.
|
|
13
13
|
export const ONNX_ENGINES = {
|
|
14
|
-
// LiquidAI LFM3-Omni-2B: text, image and speech in (16 kHz Float32Array, at most 30 s a clip)
|
|
14
|
+
// LiquidAI LFM3-Omni-2B: text, image and speech in (16 kHz Float32Array, at most 30 s a clip); text out, or text and
|
|
15
|
+
// speech interleaved (24 kHz, from the depth and codec decoders, 515 MB more, fetched on the first spoken reply or by
|
|
16
|
+
// load({ speech: true })). transformers.js
|
|
15
17
|
// 4.3.2 has no lfm3_omni class; swap to transformers.js when it adds the model. diskformer's gate
|
|
16
18
|
// (test/browser/lfm3-omni.html, onnxruntime-web 1.30.0): prompt ids and 32 greedy tokens equal Liquid's runner on
|
|
17
19
|
// onnxruntime's CPU EP for text, image and speech, 3/3.
|
|
@@ -57,11 +59,12 @@ export const diskformerOnnxBackend = {
|
|
|
57
59
|
return { ok: true, estimate: { gpuBytes: bytes, diskBytes: bytes, tokPerSec: null } };
|
|
58
60
|
},
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
// speech: true also fetches the speech-out graphs now, so the first spoken reply does not wait for them.
|
|
63
|
+
async load(model, { speech = false, onProgress, signal }) {
|
|
61
64
|
const e = ONNX_ENGINES[model.architecture];
|
|
62
65
|
const mod = await import(`${diskformerOnnxBackend.source}/${e.engine}`);
|
|
63
66
|
const baseUrl = diskformerOnnxBackend.url ? diskformerOnnxBackend.url(model) : `https://huggingface.co/${model.repo}/resolve/${model.revision || 'main'}/`;
|
|
64
|
-
const engine = await mod[e.Engine].load({ baseUrl, onProgress: onProgress && ((p) => onProgress({ backend: 'diskformer-onnx', ...p })) });
|
|
67
|
+
const engine = await mod[e.Engine].load({ baseUrl, ...(speech && { speech: true }), onProgress: onProgress && ((p) => onProgress({ backend: 'diskformer-onnx', ...p })) });
|
|
65
68
|
if (signal?.aborted) { await engine.dispose(); signal.throwIfAborted(); }
|
|
66
69
|
return {
|
|
67
70
|
task: 'text-generation',
|
|
@@ -73,13 +76,27 @@ export const diskformerOnnxBackend = {
|
|
|
73
76
|
// or { data, width, height }) and { type: 'audio', audio } (16 kHz mono Float32Array). thinking: false (the
|
|
74
77
|
// default, as on every back end here) answers directly. tools: the chat template's tool list; tool calls stay
|
|
75
78
|
// raw text in the reply (LFM3 writes Qwen3-Coder XML), for the caller to parse. An abort ends the reply quietly.
|
|
76
|
-
|
|
79
|
+
// speech: true answers in interleaved text and speech (the engine adds its speech system turn when the messages
|
|
80
|
+
// have none, and thinks never): a chunk that carries new speech has `audio`, the 24 kHz Float32 PCM since the last
|
|
81
|
+
// chunk, and `sampleRate`; maxNewTokens then counts text tokens and speech frames (80 ms each) together. Speech is
|
|
82
|
+
// sampled (generation_config.json: temperature 0.4, repetition penalty 1.2 over 64 frames); audioTemperature,
|
|
83
|
+
// audioRepetitionPenalty, audioRepetitionWindow and seed override it.
|
|
84
|
+
async *generate(messages, { maxNewTokens = 512, signal, thinking = false, tools, repetitionPenalty, speech = false, seed,
|
|
85
|
+
audioTemperature, audioRepetitionPenalty, audioRepetitionWindow } = {}) {
|
|
77
86
|
const queue = [];
|
|
78
87
|
let wake = () => {}, done = false, failed = null, text = '', tokens = 0;
|
|
79
88
|
const msgs = await media(messages);
|
|
89
|
+
const sampleRate = engine.speech?.sampling_rate ?? 24000;
|
|
80
90
|
engine.generate(msgs, {
|
|
81
91
|
max_new_tokens: maxNewTokens, do_sample: false, enable_thinking: thinking === true, signal,
|
|
82
|
-
...(tools && { tools }), ...(repetitionPenalty && { repetition_penalty: repetitionPenalty }),
|
|
92
|
+
...(tools && { tools }), ...(repetitionPenalty && { repetition_penalty: repetitionPenalty }), ...(seed !== undefined && { seed }),
|
|
93
|
+
...(speech && {
|
|
94
|
+
output_modalities: ['text', 'audio'],
|
|
95
|
+
onAudio: (pcm) => { queue.push({ text, tokens, audio: pcm, sampleRate }); wake(); },
|
|
96
|
+
...(audioTemperature !== undefined && { audio_temperature: audioTemperature }),
|
|
97
|
+
...(audioRepetitionPenalty !== undefined && { audio_repetition_penalty: audioRepetitionPenalty }),
|
|
98
|
+
...(audioRepetitionWindow !== undefined && { audio_repetition_window: audioRepetitionWindow }),
|
|
99
|
+
}),
|
|
83
100
|
onToken: () => { tokens++; },
|
|
84
101
|
onText: (delta) => { text += delta; queue.push({ text, tokens }); wake(); },
|
|
85
102
|
}).catch((err) => { failed = err; }).finally(() => { done = true; wake(); });
|
package/src/load.js
CHANGED
|
@@ -43,14 +43,14 @@ export async function plan(repoOrEntry, opts = {}) {
|
|
|
43
43
|
|
|
44
44
|
export async function load(repoOrEntry, options = {}) {
|
|
45
45
|
const [repo, opts] = entry(repoOrEntry, options);
|
|
46
|
-
const { backends = BACKENDS, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, onProgress, signal } = opts;
|
|
46
|
+
const { backends = BACKENDS, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, speech, onProgress, signal } = opts;
|
|
47
47
|
let p;
|
|
48
48
|
try { p = await plan(repo, opts); } catch (e) { throw wrap(e, 'LOAD_FAILED', signal, `${repo}: `); }
|
|
49
49
|
if (!p.chosen) throw noBackend(p);
|
|
50
50
|
const backend = backends.find((b) => b.id === p.chosen);
|
|
51
51
|
let session;
|
|
52
52
|
try {
|
|
53
|
-
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, caps: p.caps, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, onProgress, signal });
|
|
53
|
+
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, caps: p.caps, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, speech, onProgress, signal });
|
|
54
54
|
} catch (e) { throw wrap(e, 'LOAD_FAILED', signal, `${backend.id}: `); }
|
|
55
55
|
// Added onto serialize()'s object, not spread: a spread would read its getters once and freeze them.
|
|
56
56
|
return Object.assign(serialize(session), { backend: backend.id, task: session.task || p.task || p.model.task || null, ...(session.device && { device: session.device }), plan: p });
|
package/src/models.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// repo id; it carries the task and, where the back end needs one, the budget. DEFAULT is the smallest download that
|
|
3
3
|
// passed. `test` names the page under test/browser/ that proves the entry; test/models.test.js checks it exists.
|
|
4
4
|
// `diffusion: true` marks a text-diffusion model (the reply denoises in blocks); `experimental: true` marks an entry its
|
|
5
|
-
// app shows as opt-in until that app's own checks pass
|
|
5
|
+
// app shows as opt-in until that app's own checks pass; `audioOut: true` marks a model that can also answer in speech
|
|
6
|
+
// (generate({ speech: true })).
|
|
6
7
|
|
|
7
8
|
const M = (repo, backend, task, test, extra = {}) => ({ repo, backend, task, test, ...extra });
|
|
8
9
|
|
|
@@ -23,7 +24,7 @@ const GEMMA4_26B_A4B = M('google/gemma-4-26B-A4B-it-qat-q4_0-gguf', 'diskformer'
|
|
|
23
24
|
const LFM3_1_7B = M('LiquidAI/LFM3-1.7B-ONNX', 'transformers', 'text-generation', 'chat', { downloadBytes: 1.63e9 });
|
|
24
25
|
// transformers.js 4.3.2 has no lfm3_omni class, so diskformer's hand engine runs it; swap the entry to transformers.js
|
|
25
26
|
// when it adds the model.
|
|
26
|
-
const LFM3_OMNI_2B = M('LiquidAI/LFM3-Omni-2B-ONNX', 'diskformer-onnx', 'any-to-any', 'omni', { downloadBytes: 2.14e9 });
|
|
27
|
+
const LFM3_OMNI_2B = M('LiquidAI/LFM3-Omni-2B-ONNX', 'diskformer-onnx', 'any-to-any', 'omni', { downloadBytes: 2.14e9, audioOut: true });
|
|
27
28
|
const NEMOTRON_3_5_BOLT_E4B = M('danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU', 'transformers', 'text-generation', 'transformers', { budget: 5e9, downloadBytes: 4.94e9 });
|
|
28
29
|
// A tech demo, not a gated model: 231 GB on disk (4 IQ1_M parts plus the 110.6 GB Engram side file), about 0.4 tok/s.
|
|
29
30
|
// diskformer checked one hand-written prompt through complete(); generate() renders the GGUF's chat template, which no
|
|
@@ -60,7 +61,7 @@ export const MODELS = {
|
|
|
60
61
|
NEMOTRON_3_5_BOLT_E4B,
|
|
61
62
|
/** LiquidAI's LFM3 1.7B (dense), LiquidAI's own ONNX export; thinking off unless a request asks for it. */
|
|
62
63
|
LFM3_1_7B,
|
|
63
|
-
/** LiquidAI's LFM3-Omni-2B: text, image and speech in (16 kHz, at most 30 s a clip)
|
|
64
|
+
/** LiquidAI's LFM3-Omni-2B: text, image and speech in (16 kHz, at most 30 s a clip); text or text and speech out (diskformer's engine). */
|
|
64
65
|
LFM3_OMNI_2B,
|
|
65
66
|
/** DeepSeek V4.1 Flash (763B MoE), a tech demo on diskformer's llama.cpp path: 231 GB on disk, very slow. */
|
|
66
67
|
DEEPSEEK_V4_1_FLASH,
|
package/src/sources.js
CHANGED
|
@@ -9,6 +9,8 @@ export const PINS = {
|
|
|
9
9
|
// v0.3.1: seeded block-KV graphs (Nemotron-Labs-Diffusion-3B), chatFormat, split external data; download progress
|
|
10
10
|
// (progress_callback) and the graph files cached in Cache Storage 'kohra-models'.
|
|
11
11
|
kohra: 'bf95d44c3e3cfc001201baa03fa121c2bf9c7d73',
|
|
12
|
+
// v0.6.12 (1fe19f8): LFM3-Omni speech out (interleaved text and 24 kHz speech; depth and codec decoders). Changes
|
|
13
|
+
// only engines/lfm3_omni.js; its text, image and speech-in gate (3/3) is unchanged.
|
|
12
14
|
// 48f2c2a (main after v0.6.9): engines/lfm3_omni.js, LFM3-Omni-2B on onnxruntime-web 1.30.0 (text, image and speech
|
|
13
15
|
// in; text out; files in Cache Storage 'diskformer-models'). Adds files only: no other engine or ggml/ changed.
|
|
14
16
|
// v0.6.9: llama.load({ extra }) mounts side files beside the GGUF (DeepSeek V4.1 Flash's Engram tables).
|
|
@@ -16,7 +18,7 @@ export const PINS = {
|
|
|
16
18
|
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs
|
|
17
19
|
// and drafters, and reads past 2 GiB (Nemotron 3.5 Bolt E4B needs v0.6.8 or later). The hand engines' ingest
|
|
18
20
|
// streams the GGUF header with progress and resumes a dropped range.
|
|
19
|
-
diskformer: '
|
|
21
|
+
diskformer: '1fe19f8f430f3af2c78e5d2534fdd7938b36b307',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;
|