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 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`
@@ -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 (output_modalities: ['text', 'audio']) is lfm3-omni-infer's s2s mode, lfm3_audio.session.generate_ids'
32
- * TEXT / AUDIO / INTERLEAVED machine: the system turn "Respond with interleaved text and audio." (when the messages
33
- * have none) and <|mixed_start|> after the generation prompt; then 6 text slots and 12 speech frames in turn until
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] ?? LFM3_OMNI_2B.speechGraphs[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 m;
455
+ return new Lfm3Omni({ ort, tokenizer, config, gen, processor, vision, audio, decoder });
582
456
  }
583
457
 
584
- // The depth and codec sessions (speech out); loaded once
585
- async loadSpeech() {
586
- if (!this.depth) {
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({ speech: true })) {
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 out (lfm3_audio.config), and the codec's STFT (audio_tokenizer/config.json's values for this checkpoint)
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.10",
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",