diskformer 0.6.10 → 0.6.11
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 +5 -0
- package/engines/lfm3_omni.js +14 -240
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.11 (2026-10-30)
|
|
4
|
+
- Republishes 0.6.10's tagged files. npm 0.6.10 was packed from a working tree that held uncommitted, unchecked
|
|
5
|
+
speech-output work in `engines/lfm3_omni.js` (854 lines against the tag's 628); 0.6.11 carries the tag's engine.
|
|
6
|
+
No code change from v0.6.10.
|
|
7
|
+
|
|
3
8
|
## 0.6.10 (2026-10-30)
|
|
4
9
|
- LFM3-Omni-2B (`engines/lfm3_omni.js`, `Lfm3Omni`): LiquidAI's ONNX export (decoder_q4f16, vision_encoder_q8,
|
|
5
10
|
audio_encoder; 2.14 GB) on onnxruntime-web 1.30.0's WebGPU EP, because transformers.js 4.3.2 has no `lfm3_omni`
|
package/engines/lfm3_omni.js
CHANGED
|
@@ -28,14 +28,9 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Text slots follow lfm3_audio.session.generate_ids in TEXT mode: the placeholders (<|audio|>, <|audio_out|>,
|
|
30
30
|
* <|image|>, <|video|>), <|mixed_start|> and <|audio_start|> are suppressed; <|im_end|> or <|text_end|> ends the turn.
|
|
31
|
-
* Speech output (
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* <|text_end|>, after which speech goes on to its end-of-audio frame. A frame is 12 codes from the fp32 depth
|
|
35
|
-
* decoder, MaskGIT in 3 passes of 4 codebooks over the decoder's hidden state (audio_temperature 0.4, repetition
|
|
36
|
-
* penalty 1.2 over the last 64 frames, from generation_config.json); it goes back into the decoder as <|audio_out|>
|
|
37
|
-
* with audio_out_codes. Every 12 frames (and at the end) the fp32 codec decoder turns the new frames, with 20 frames
|
|
38
|
-
* of left context, into STFT rows, and an overlap-add inverse STFT gives 24 kHz Float32 PCM to onAudio.
|
|
31
|
+
* Speech output (layer 2, not built): the step already returns the last hidden state, which the depth decoder
|
|
32
|
+
* (MaskGIT, 12 codes per frame) reads, and takes `audio_out_codes`, the codes of the frame fed back as <|audio_out|>;
|
|
33
|
+
* output_modalities: ['text', 'audio'] will switch generate() to lfm3_audio's TEXT / AUDIO / INTERLEAVED machine.
|
|
39
34
|
*
|
|
40
35
|
* Model files go to Cache Storage ('diskformer-models', keyed by URL) on the first load and come from there later;
|
|
41
36
|
* load({ force: true }) downloads again, Lfm3Omni.deleteCache({ baseUrl }) removes this model's entries by name.
|
|
@@ -51,8 +46,6 @@ export const LFM3_OMNI_2B = {
|
|
|
51
46
|
repo: 'LiquidAI/LFM3-Omni-2B-ONNX',
|
|
52
47
|
baseUrl: 'https://huggingface.co/LiquidAI/LFM3-Omni-2B-ONNX/resolve/main/',
|
|
53
48
|
graphs: { decoder: 'onnx/decoder_q4f16.onnx', vision: 'onnx/vision_encoder_q8.onnx', audio: 'onnx/audio_encoder.onnx' },
|
|
54
|
-
// speech out, loaded with load({ speech: true }) or on the first generate() that asks for audio
|
|
55
|
-
speechGraphs: { depth: 'onnx/depth_decoder.onnx', codec: 'onnx/codec_decoder.onnx' },
|
|
56
49
|
json: ['config.json', 'generation_config.json', 'processor_config.json', 'tokenizer.json', 'tokenizer_config.json'],
|
|
57
50
|
};
|
|
58
51
|
// The model card: prefill at most this many ids per decoder run
|
|
@@ -415,127 +408,12 @@ export async function fetchCached(url, { cache = null, force = false, onProgress
|
|
|
415
408
|
return out;
|
|
416
409
|
}
|
|
417
410
|
|
|
418
|
-
// ── Speech out: MaskGIT frames, the codec, the inverse STFT ───────────────────────────────────────────────────────
|
|
419
|
-
// Codec frames re-decoded before each chunk's new frames. The codec is causal (5 conv layers of kernel 7, 3 attention
|
|
420
|
-
// layers of window 30, over 6 rows per frame), so a row sees at most 5·6 + 3·29 = 117 rows back: 20 frames covers it.
|
|
421
|
-
export const CODEC_CONTEXT_FRAMES = 20;
|
|
422
|
-
export const SPEECH_SYSTEM_PROMPT = 'Respond with interleaved text and audio.';
|
|
423
|
-
|
|
424
|
-
// A random permutation of 0..n-1 (Fisher-Yates over rand): one MaskGIT order
|
|
425
|
-
export function randomOrder(n, rand = Math.random) {
|
|
426
|
-
const o = Array.from({ length: n }, (_, i) => i);
|
|
427
|
-
for (let i = n - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [o[i], o[j]] = [o[j], o[i]]; }
|
|
428
|
-
return o;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Codes [12] of one frame (lfm3_audio.session.sample_frame): the order split by the schedule; per pass one depth run
|
|
432
|
-
// on (hidden, codes so far, mask of the hidden codebooks) and each picked codebook sampled from its logits with the
|
|
433
|
-
// repetition penalty over its own column of history; all audio_eos_code when any code is.
|
|
434
|
-
// depth(hidden, codes BigInt64Array [12], masked Uint8Array [12]) → Float32Array [12 · codebook_size]
|
|
435
|
-
export async function sampleFrame(hidden, depth, { order, schedule = [4, 4, 4], codebook_size = 2017, audio_eos_code = 2016, sampling = {}, history = [], rand = Math.random }) {
|
|
436
|
-
const K = order.length;
|
|
437
|
-
if ([...order].sort((a, b) => a - b).some((v, i) => v !== i)) throw new Error(`MaskGIT order ${order} is not a permutation`);
|
|
438
|
-
const codes = new BigInt64Array(K), masked = new Uint8Array(K).fill(1);
|
|
439
|
-
let at = 0;
|
|
440
|
-
for (const n of schedule) {
|
|
441
|
-
const pick = order.slice(at, at + n); at += n;
|
|
442
|
-
const logits = await depth(hidden, codes, masked);
|
|
443
|
-
for (const k of pick) {
|
|
444
|
-
const col = history.map((f) => f[k]);
|
|
445
|
-
codes[k] = BigInt(sampleToken(logits.subarray(k * codebook_size, (k + 1) * codebook_size), sampling, col, [], rand));
|
|
446
|
-
}
|
|
447
|
-
for (const k of pick) masked[k] = 0;
|
|
448
|
-
}
|
|
449
|
-
const frame = Array.from(codes, Number);
|
|
450
|
-
return frame.includes(audio_eos_code) ? frame.fill(audio_eos_code) : frame;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Speech sampling of generate()'s audio_* options, as lfm3_audio's sample_token reads them: temperature 0 (or none
|
|
454
|
-
// and no top-k) is argmax; a top-k without a temperature samples at temperature 1.
|
|
455
|
-
export function audioSampling({ audio_temperature, audio_top_k, audio_repetition_penalty } = {}) {
|
|
456
|
-
const greedy = audio_temperature === 0 || (audio_temperature == null && !audio_top_k);
|
|
457
|
-
return { do_sample: !greedy, temperature: audio_temperature ?? 1, top_k: audio_top_k ?? 0, repetition_penalty: audio_repetition_penalty ?? 1 };
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// liquid-audio's inverse STFT with "same" padding (lfm2_audio.infer.istft_same_padding), fed row by row: each row is
|
|
461
|
-
// codec_decoder.onnx's [log-magnitude (n_fft/2+1), phase (n_fft/2+1)]; irfft, periodic Hann (float64), overlap-add,
|
|
462
|
-
// divide by the summed window², trim (n_fft − hop)/2 at both ends. push() returns the samples no later row can change;
|
|
463
|
-
// end() the rest. Offline and streamed output are the same samples.
|
|
464
|
-
export class Istft {
|
|
465
|
-
constructor(n_fft = 1280, hop = 320) {
|
|
466
|
-
this.N = n_fft; this.hop = hop; this.bins = n_fft / 2 + 1; this.pad = (n_fft - hop) >> 1;
|
|
467
|
-
this.win = Float64Array.from({ length: n_fft }, (_, i) => 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / n_fft));
|
|
468
|
-
this.cos = Float64Array.from({ length: n_fft }, (_, i) => Math.cos((2 * Math.PI * i) / n_fft));
|
|
469
|
-
this.sin = Float64Array.from({ length: n_fft }, (_, i) => Math.sin((2 * Math.PI * i) / n_fft));
|
|
470
|
-
this.wave = new Float64Array(0); this.env = new Float64Array(0); this.rows = 0; this.emitted = 0;
|
|
471
|
-
}
|
|
472
|
-
#grow(size) {
|
|
473
|
-
if (this.wave.length >= size) return;
|
|
474
|
-
const n = Math.max(size, this.wave.length * 2), w = new Float64Array(n), e = new Float64Array(n);
|
|
475
|
-
w.set(this.wave); e.set(this.env); this.wave = w; this.env = e;
|
|
476
|
-
}
|
|
477
|
-
// rows: Float32Array of whole rows (n_fft + 2 each)
|
|
478
|
-
push(rows) {
|
|
479
|
-
const { N, bins, hop } = this, width = 2 * bins, count = rows.length / width;
|
|
480
|
-
this.#grow((this.rows + count - 1) * hop + N);
|
|
481
|
-
const re = new Float64Array(bins), im = new Float64Array(bins);
|
|
482
|
-
for (let r = 0; r < count; r++) {
|
|
483
|
-
const base = r * width, o = (this.rows + r) * hop;
|
|
484
|
-
for (let k = 0; k < bins; k++) { const m = Math.exp(rows[base + k]), ph = rows[base + bins + k]; re[k] = m * Math.cos(ph); im[k] = m * Math.sin(ph); }
|
|
485
|
-
for (let n = 0; n < N; n++) {
|
|
486
|
-
// numpy irfft: the imaginary parts of bins 0 and N/2 are dropped
|
|
487
|
-
let x = re[0] + (n & 1 ? -re[bins - 1] : re[bins - 1]);
|
|
488
|
-
for (let k = 1, a = n; k < bins - 1; k++, a = (a + n) % N) x += 2 * (re[k] * this.cos[a] - im[k] * this.sin[a]);
|
|
489
|
-
this.wave[o + n] += (x / N) * this.win[n];
|
|
490
|
-
this.env[o + n] += this.win[n] * this.win[n];
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
this.rows += count;
|
|
494
|
-
return this.#take(this.rows * hop);
|
|
495
|
-
}
|
|
496
|
-
end() { return this.rows ? this.#take((this.rows - 1) * this.hop + this.N - this.pad) : new Float32Array(0); }
|
|
497
|
-
#take(upTo) {
|
|
498
|
-
const from = this.pad + this.emitted, to = Math.max(from, upTo);
|
|
499
|
-
const out = new Float32Array(to - from);
|
|
500
|
-
for (let i = 0; i < out.length; i++) out[i] = this.wave[from + i] / this.env[from + i];
|
|
501
|
-
this.emitted += out.length;
|
|
502
|
-
return out;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Streams one utterance's frames to PCM: decode(final) runs the codec on the frames not yet decoded plus up to
|
|
507
|
-
// CODEC_CONTEXT_FRAMES before them, keeps the new frames' rows and pushes them through the inverse STFT.
|
|
508
|
-
// codec(codes BigInt64Array [12 · F], F) → Float32Array [6F · (n_fft + 2)]
|
|
509
|
-
export class SpeechStream {
|
|
510
|
-
constructor(codec, { num_codebooks = 12, frame_stack = 6, n_fft = 1280, hop = 320, context = CODEC_CONTEXT_FRAMES } = {}) {
|
|
511
|
-
Object.assign(this, { codec, K: num_codebooks, stack: frame_stack, width: n_fft + 2, context });
|
|
512
|
-
this.istft = new Istft(n_fft, hop); this.frames = []; this.done = 0;
|
|
513
|
-
}
|
|
514
|
-
push(frame) { this.frames.push(frame); }
|
|
515
|
-
get pending() { return this.frames.length - this.done; }
|
|
516
|
-
async decode(final = false) {
|
|
517
|
-
const parts = [];
|
|
518
|
-
if (this.pending) {
|
|
519
|
-
const start = Math.max(0, this.done - this.context), F = this.frames.length - start, K = this.K;
|
|
520
|
-
// audio_codes [1, 12, F]: codebook-major
|
|
521
|
-
const codes = new BigInt64Array(K * F);
|
|
522
|
-
for (let f = 0; f < F; f++) for (let k = 0; k < K; k++) codes[k * F + f] = BigInt(this.frames[start + f][k]);
|
|
523
|
-
const rows = await this.codec(codes, F);
|
|
524
|
-
parts.push(this.istft.push(rows.subarray((this.done - start) * this.stack * this.width)));
|
|
525
|
-
this.done = this.frames.length;
|
|
526
|
-
}
|
|
527
|
-
if (final) parts.push(this.istft.end());
|
|
528
|
-
return concat(parts);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
411
|
// ── The engine ────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
533
412
|
export class Lfm3Omni {
|
|
534
413
|
// opts: baseUrl (the repo layout) or files { 'onnx/decoder_q4f16.onnx': URL | Blob | ArrayBuffer | Uint8Array, … };
|
|
535
414
|
// ort (onnxruntime-web/webgpu module), transformers (for PreTrainedTokenizer) or tokenizer; device 'webgpu';
|
|
536
415
|
// onProgress({ status: 'download' | 'cache', file, loaded, total }) while a file arrives, then 'session' per graph;
|
|
537
|
-
// cacheName (CACHE_NAME; null: no Cache Storage) and force (download again, replacing the stored copies)
|
|
538
|
-
// speech: also load the speech-out graphs now (else the first generate() with audio output loads them)
|
|
416
|
+
// cacheName (CACHE_NAME; null: no Cache Storage) and force (download again, replacing the stored copies)
|
|
539
417
|
static async load(opts = {}) {
|
|
540
418
|
const { baseUrl = LFM3_OMNI_2B.baseUrl, files = {}, device = 'webgpu', onProgress = () => {}, cacheName = CACHE_NAME, force = false } = opts;
|
|
541
419
|
const src = (name) => files[name] ?? new URL(name, baseUrl.endsWith('/') ? baseUrl : baseUrl + '/').href;
|
|
@@ -564,7 +442,7 @@ export class Lfm3Omni {
|
|
|
564
442
|
// the graph and its external data, both read here (byte progress); the session is built from the bytes, which
|
|
565
443
|
// are dropped once onnxruntime holds the weights
|
|
566
444
|
const graph = async (role) => {
|
|
567
|
-
const file = LFM3_OMNI_2B.graphs[role]
|
|
445
|
+
const file = LFM3_OMNI_2B.graphs[role];
|
|
568
446
|
const model = await bytes(file), data = await bytes(file + '_data');
|
|
569
447
|
onProgress({ status: 'session', file });
|
|
570
448
|
return { model, data, dataPath: file.split('/').pop() + '_data' };
|
|
@@ -573,47 +451,19 @@ export class Lfm3Omni {
|
|
|
573
451
|
const vision = await createSession(ort, await graph('vision'), sessionOpts);
|
|
574
452
|
const audio = await createSession(ort, await graph('audio'), sessionOpts);
|
|
575
453
|
const decoder = await OrtLfm3Decoder.create(ort, await graph('decoder'), sessionOpts, config.text_config);
|
|
576
|
-
const m = new Lfm3Omni({ ort, tokenizer, config, gen, processor, vision, audio, decoder });
|
|
577
|
-
// the speech graphs, built when first needed from the same file source and cache
|
|
578
|
-
m.loadSpeechGraphs = async () => ({ depth: await createSession(ort, await graph('depth'), sessionOpts), codec: await createSession(ort, await graph('codec'), sessionOpts) });
|
|
579
|
-
if (opts.speech) await m.loadSpeech();
|
|
580
454
|
onProgress({ status: 'ready' });
|
|
581
|
-
return
|
|
455
|
+
return new Lfm3Omni({ ort, tokenizer, config, gen, processor, vision, audio, decoder });
|
|
582
456
|
}
|
|
583
457
|
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (!this.loadSpeechGraphs) throw new Error('no speech graphs: load the engine with Lfm3Omni.load()');
|
|
588
|
-
this.speechLoading ??= this.loadSpeechGraphs().then(({ depth, codec }) => { this.depth = depth; this.codec = codec; });
|
|
589
|
-
await this.speechLoading;
|
|
590
|
-
}
|
|
591
|
-
return this;
|
|
592
|
-
}
|
|
593
|
-
// depth_decoder.onnx: logits [12 · codebook_size] of one frame
|
|
594
|
-
async depthLogits(hidden, codes, masked) {
|
|
595
|
-
const T = this.ort.Tensor, K = codes.length;
|
|
596
|
-
const out = await this.depth.run({ hidden_states: new T('float32', hidden, [1, hidden.length]), codes: new T('int64', codes, [1, K]), masked: new T('bool', masked, [1, K]) });
|
|
597
|
-
const l = Float32Array.from(out.logits.data); out.logits.dispose?.();
|
|
598
|
-
return l;
|
|
599
|
-
}
|
|
600
|
-
// codec_decoder.onnx: stft_features [6F · (n_fft + 2)] of frames (codebook-major codes [12 · F])
|
|
601
|
-
async codecRows(codes, F) {
|
|
602
|
-
const out = await this.codec.run({ audio_codes: new this.ort.Tensor('int64', codes, [1, this.speech.num_codebooks, F]) });
|
|
603
|
-
const r = Float32Array.from(out.stft_features.data); out.stft_features.dispose?.();
|
|
604
|
-
return r;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Every file load() reads, relative to baseUrl (with speech: also the speech-out graphs)
|
|
608
|
-
static files({ speech = false } = {}) {
|
|
609
|
-
const graphs = { ...LFM3_OMNI_2B.graphs, ...(speech ? LFM3_OMNI_2B.speechGraphs : {}) };
|
|
610
|
-
return [...LFM3_OMNI_2B.json, 'chat_template.jinja', ...Object.values(graphs).flatMap((g) => [g, g + '_data'])];
|
|
458
|
+
// Every file load() reads, relative to baseUrl
|
|
459
|
+
static files() {
|
|
460
|
+
return [...LFM3_OMNI_2B.json, 'chat_template.jinja', ...Object.values(LFM3_OMNI_2B.graphs).flatMap((g) => [g, g + '_data'])];
|
|
611
461
|
}
|
|
612
462
|
// Stored copies of this model's files under baseUrl: [{ file, url, bytes }] (bytes from the stored length)
|
|
613
463
|
static async cached({ baseUrl = LFM3_OMNI_2B.baseUrl, cacheName = CACHE_NAME } = {}) {
|
|
614
464
|
if (!globalThis.caches) return [];
|
|
615
465
|
const cache = await caches.open(cacheName), out = [];
|
|
616
|
-
for (const file of Lfm3Omni.files(
|
|
466
|
+
for (const file of Lfm3Omni.files()) {
|
|
617
467
|
const url = new URL(file, baseUrl.endsWith('/') ? baseUrl : baseUrl + '/').href, r = await cache.match(url);
|
|
618
468
|
if (r) out.push({ file, url, bytes: Number(r.headers.get('content-length')) || null });
|
|
619
469
|
}
|
|
@@ -644,10 +494,9 @@ export class Lfm3Omni {
|
|
|
644
494
|
mixed_start: t('mixed_start_token_id', 125015), eos: config.text_config.eos_token_id,
|
|
645
495
|
think_end: tokenizer.convert_tokens_to_ids?.('</think>') ?? null,
|
|
646
496
|
};
|
|
647
|
-
// speech
|
|
497
|
+
// the speech-output layer reads these (lfm3_audio.config): not used in text mode
|
|
648
498
|
this.speech = { num_codebooks: t('num_codebooks', 12), codebook_size: t('codebook_size', 2017), audio_eos_code: t('audio_eos_code', 2016),
|
|
649
|
-
maskgit_schedule: t('maskgit_schedule', [4, 4, 4]), interleaved_n_text: t('interleaved_n_text', 6), interleaved_n_audio: t('interleaved_n_audio', 12)
|
|
650
|
-
frame_stack: 6, n_fft: 1280, hop: 320, sampling_rate: 24000 };
|
|
499
|
+
maskgit_schedule: t('maskgit_schedule', [4, 4, 4]), interleaved_n_text: t('interleaved_n_text', 6), interleaved_n_audio: t('interleaved_n_audio', 12) };
|
|
651
500
|
}
|
|
652
501
|
|
|
653
502
|
// The chat template's text and ids (placeholders expanded) of messages; media come back for the encoders.
|
|
@@ -712,10 +561,7 @@ export class Lfm3Omni {
|
|
|
712
561
|
// (AbortSignal) is read before every decode step; an abort resolves, it does not throw.
|
|
713
562
|
async generate(messages, opts = {}) {
|
|
714
563
|
const { max_new_tokens = 1024, signal, onText, onToken, streamer, seed, output_modalities = ['text'] } = opts;
|
|
715
|
-
if (output_modalities.includes('audio'))
|
|
716
|
-
if (!output_modalities.includes('text')) throw new Error("LFM3-Omni speaks in interleaved mode: output_modalities ['text', 'audio']");
|
|
717
|
-
return this.#generateSpeech(messages, opts);
|
|
718
|
-
}
|
|
564
|
+
if (output_modalities.includes('audio')) throw new Error('LFM3-Omni speech output is not built yet (text only)');
|
|
719
565
|
const sampling = { do_sample: opts.do_sample ?? false, temperature: opts.temperature ?? 1, top_k: opts.top_k ?? 0,
|
|
720
566
|
top_p: opts.top_p ?? 1, min_p: opts.min_p ?? 0, repetition_penalty: opts.repetition_penalty ?? 1 };
|
|
721
567
|
const rand = seed === undefined ? Math.random : mulberry32(seed);
|
|
@@ -749,74 +595,6 @@ export class Lfm3Omni {
|
|
|
749
595
|
return { text, stopReason, tokens, ...this.decodeText(tokens) };
|
|
750
596
|
}
|
|
751
597
|
|
|
752
|
-
// Interleaved text and speech (lfm3_audio.session.generate_ids from <|mixed_start|>). max_new_tokens counts slots,
|
|
753
|
-
// text tokens and frames alike. Text streams as in generate(); speech as onAudio(Float32Array 24 kHz) every
|
|
754
|
-
// audio_chunk_frames frames and at the end; signal is read before every slot. orders: a function returning one
|
|
755
|
-
// MaskGIT order per frame, or an array of them (a reference replay); default random from seed.
|
|
756
|
-
// Resolves { text, stopReason, tokens, reasoning, content, audio: Float32Array, audio_codes: frames, sampling_rate }.
|
|
757
|
-
async #generateSpeech(messages, opts) {
|
|
758
|
-
const { max_new_tokens = 1024, signal, onText, onToken, onAudio, streamer, seed, audio_chunk_frames = 12 } = opts;
|
|
759
|
-
await this.loadSpeech();
|
|
760
|
-
const g = this.generationConfig, S = this.speech, T = this.tokens;
|
|
761
|
-
const text = { do_sample: opts.do_sample ?? false, temperature: opts.temperature ?? 1, top_k: opts.top_k ?? 0,
|
|
762
|
-
top_p: opts.top_p ?? 1, min_p: opts.min_p ?? 0, repetition_penalty: opts.repetition_penalty ?? 1 };
|
|
763
|
-
const audio = audioSampling({ audio_temperature: opts.audio_temperature ?? g.audio_temperature ?? 0.4, audio_top_k: opts.audio_top_k ?? g.audio_top_k,
|
|
764
|
-
audio_repetition_penalty: opts.audio_repetition_penalty ?? g.audio_repetition_penalty ?? 1.2 });
|
|
765
|
-
const window = opts.audio_repetition_window ?? g.audio_repetition_window ?? 64;
|
|
766
|
-
const rand = seed === undefined ? Math.random : mulberry32(seed);
|
|
767
|
-
const orderOf = Array.isArray(opts.orders) ? ((i) => opts.orders[i]) : typeof opts.orders === 'function' ? opts.orders : () => randomOrder(S.num_codebooks, rand);
|
|
768
|
-
const msgs = messages.some((m) => m.role === 'system') ? messages : [{ role: 'system', content: SPEECH_SYSTEM_PROMPT }, ...messages];
|
|
769
|
-
const p = await this.prepare(msgs, { ...opts, enable_thinking: false });
|
|
770
|
-
const ids = [...p.ids, T.mixed_start];
|
|
771
|
-
const side = { image_features: p.pics.length ? await this.encodeImages(p.pics) : undefined, audio_features: p.mels.length ? await this.encodeAudio(p.mels) : undefined };
|
|
772
|
-
const suppress = [T.audio, T.audio_out, T.image, T.video];
|
|
773
|
-
const stream = new SpeechStream((c, F) => this.codecRows(c, F), { num_codebooks: S.num_codebooks, frame_stack: S.frame_stack, n_fft: S.n_fft, hop: S.hop });
|
|
774
|
-
const tokens = [], textIds = [], textHistory = [], history = [], frames = [], chunks = [];
|
|
775
|
-
let stopReason = 'length', sent = 0, mode = 'interleaved', slotsLeft = S.interleaved_n_text, speaking = false, textDone = false, nFrames = 0;
|
|
776
|
-
const emitText = () => {
|
|
777
|
-
if (!onText) return;
|
|
778
|
-
const full = this.tokenizer.decode(textIds, { skip_special_tokens: true });
|
|
779
|
-
if (full.length > sent && !full.endsWith('\uFFFD')) { onText(full.slice(sent)); sent = full.length; }
|
|
780
|
-
};
|
|
781
|
-
const flush = async (final) => { const pcm = await stream.decode(final); if (pcm.length) { chunks.push(pcm); onAudio?.(pcm); } };
|
|
782
|
-
try {
|
|
783
|
-
let { logits, hidden } = await this.prefill(ids, side);
|
|
784
|
-
for (let step = 0; step < max_new_tokens; step++) {
|
|
785
|
-
if (signal?.aborted) { stopReason = 'aborted'; break; }
|
|
786
|
-
const interleaving = mode === 'interleaved';
|
|
787
|
-
let next, codes, finished = false;
|
|
788
|
-
if (mode === 'audio' || (interleaving && speaking)) {
|
|
789
|
-
const frame = await sampleFrame(hidden, (h, c, m) => this.depthLogits(h, c, m), { order: orderOf(nFrames++), schedule: S.maskgit_schedule,
|
|
790
|
-
codebook_size: S.codebook_size, audio_eos_code: S.audio_eos_code, sampling: audio, history, rand });
|
|
791
|
-
if (frame[0] === S.audio_eos_code) mode = 'text';
|
|
792
|
-
else {
|
|
793
|
-
history.push(frame); if (history.length > window) history.shift();
|
|
794
|
-
frames.push(frame); tokens.push(T.audio_out); stream.push(frame);
|
|
795
|
-
if (stream.pending >= audio_chunk_frames) await flush(false);
|
|
796
|
-
}
|
|
797
|
-
next = T.audio_out; codes = BigInt64Array.from(frame, BigInt);
|
|
798
|
-
} else {
|
|
799
|
-
const tok = sampleToken(logits, text, textHistory, suppress, rand);
|
|
800
|
-
tokens.push(tok); textHistory.push(tok); if (textHistory.length > 32) textHistory.shift();
|
|
801
|
-
if (tok === T.text_end) { if (interleaving) textDone = speaking = true; else finished = true; }
|
|
802
|
-
else if (tok === T.eos) finished = true;
|
|
803
|
-
else if (!interleaving && tok === T.audio_start) mode = 'audio';
|
|
804
|
-
else if (!interleaving && tok === T.mixed_start) { mode = 'interleaved'; slotsLeft = S.interleaved_n_text; speaking = false; textDone = false; }
|
|
805
|
-
if (tok !== T.text_end && tok !== T.eos) { textIds.push(tok); streamer?.put?.([[BigInt(tok)]]); onToken?.(tok); emitText(); }
|
|
806
|
-
next = tok;
|
|
807
|
-
}
|
|
808
|
-
if (interleaving && !textDone && --slotsLeft === 0) { speaking = !speaking; slotsLeft = speaking ? S.interleaved_n_audio : S.interleaved_n_text; }
|
|
809
|
-
if (finished) { stopReason = 'eos'; break; }
|
|
810
|
-
if (step === max_new_tokens - 1) break;
|
|
811
|
-
({ logits, hidden } = await this.decoder.step([next], codes ? { audio_out_codes: codes } : {}));
|
|
812
|
-
}
|
|
813
|
-
if (stopReason !== 'aborted') await flush(true);
|
|
814
|
-
} finally { this.decoder.reset(); streamer?.end?.(); }
|
|
815
|
-
const all = this.tokenizer.decode(textIds, { skip_special_tokens: true });
|
|
816
|
-
if (onText && all.length > sent) onText(all.slice(sent));
|
|
817
|
-
return { text: all, stopReason, tokens, ...this.decodeText(textIds), audio: concat(chunks), audio_codes: frames, sampling_rate: S.sampling_rate };
|
|
818
|
-
}
|
|
819
|
-
|
|
820
598
|
// Greedy ids after prompt ids (the gate's comparison): no template, no stop on EOS unless asked.
|
|
821
599
|
async greedyIds(ids, side = {}, n = 32, { stopAtEos = true } = {}) {
|
|
822
600
|
const T = this.tokens, suppress = [T.audio, T.audio_out, T.image, T.video, T.mixed_start, T.audio_start], out = [];
|
|
@@ -840,11 +618,7 @@ export class Lfm3Omni {
|
|
|
840
618
|
return { content: dec(tokens.slice(k + 1)).trimStart(), reasoning: dec(tokens.slice(0, k)).replace(/^\s*<think>/, '').trim() };
|
|
841
619
|
}
|
|
842
620
|
|
|
843
|
-
async dispose() {
|
|
844
|
-
this.ready = false;
|
|
845
|
-
await this.decoder.dispose(); await this.vision.release(); await this.audio.release();
|
|
846
|
-
if (this.depth) { await this.depth.release(); await this.codec.release(); this.depth = this.codec = null; }
|
|
847
|
-
}
|
|
621
|
+
async dispose() { this.ready = false; await this.decoder.dispose(); await this.vision.release(); await this.audio.release(); }
|
|
848
622
|
}
|
|
849
623
|
|
|
850
624
|
function concat(parts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diskformer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "Run a model larger than your GPU memory in a browser tab: weights stay in OPFS, WebGPU holds only what each step needs. llama.cpp with paged MoE experts (diskformer/llama), a disk tier, and hand-written MoE engines.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|