mlx-bun 0.4.0 → 0.5.0

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.
Files changed (158) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +55 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  5. package/docs/design/batching.md +220 -7
  6. package/docs/design/decode-speed-program.md +121 -66
  7. package/docs/design/generic-model-support.md +116 -0
  8. package/docs/design/kv-cache.md +380 -211
  9. package/docs/design/speculative-decoding.md +165 -26
  10. package/docs/design/turboquant.md +20 -7
  11. package/docs/design/unified-engine-frontier-plan.md +120 -13
  12. package/docs/planning/PRODUCT_ROADMAP.md +5 -6
  13. package/docs/planning/public-readiness-handoff.md +244 -0
  14. package/docs/planning/public-readiness.md +149 -0
  15. package/docs/reference/benchmarks.md +3463 -3489
  16. package/docs/reference/cli.md +62 -1
  17. package/docs/reference/distribution.md +29 -4
  18. package/docs/reference/environment.md +104 -8
  19. package/docs/reference/library-api.md +18 -4
  20. package/docs/reference/memory.md +1 -1
  21. package/docs/reference/models.md +17 -5
  22. package/docs/reference/server-api.md +181 -11
  23. package/docs/reference/server-config.md +232 -105
  24. package/package.json +2 -2
  25. package/src/audio/audiotoolbox.ts +112 -0
  26. package/src/audio/conformer.ts +8 -0
  27. package/src/audio/mic-capture.ts +98 -0
  28. package/src/audio/silero-vad.ts +379 -0
  29. package/src/audio/whisper-decode.ts +774 -0
  30. package/src/audio/whisper-fast.ts +732 -0
  31. package/src/audio/whisper-format.ts +73 -0
  32. package/src/audio/whisper-mel.ts +185 -0
  33. package/src/audio/whisper-timing.ts +274 -0
  34. package/src/audio/whisper-tokenizer.ts +239 -0
  35. package/src/audio/whisper-transcribe.ts +410 -0
  36. package/src/backends/mlx/autoregressive.ts +22 -1
  37. package/src/backends/mlx/batch-group.ts +111 -27
  38. package/src/backends/mlx/cache-layout.ts +3 -3
  39. package/src/backends/mlx/continuation-persistence.ts +5 -4
  40. package/src/backends/mlx/encoder-cache.ts +25 -0
  41. package/src/backends/mlx/fill-append.ts +21 -8
  42. package/src/backends/mlx/fill-group.ts +497 -0
  43. package/src/backends/mlx/gateway-binding.ts +56 -12
  44. package/src/backends/mlx/kv-maintenance.ts +30 -2
  45. package/src/backends/mlx/mixed-iteration.ts +62 -0
  46. package/src/backends/mlx/model-prompt.ts +46 -32
  47. package/src/backends/mlx/model-serving.ts +4 -3
  48. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  49. package/src/backends/mlx/prefill-cohort.ts +15 -4
  50. package/src/backends/mlx/prefill-policy.ts +26 -0
  51. package/src/backends/mlx/prefill-rows.ts +57 -13
  52. package/src/backends/mlx/prompt-input.ts +29 -0
  53. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  54. package/src/backends/mlx/request-state-policy.ts +18 -5
  55. package/src/backends/mlx/rollback.ts +6 -3
  56. package/src/backends/mlx/serial-executor.ts +16 -7
  57. package/src/backends/mlx/speculative-group.ts +105 -26
  58. package/src/backends/mlx/speculative.ts +24 -7
  59. package/src/backends/mlx/target-layout-capability.ts +1 -1
  60. package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
  61. package/src/cli.ts +322 -0
  62. package/src/contracts/execution.ts +10 -0
  63. package/src/contracts/object-cache.ts +8 -0
  64. package/src/contracts/pi-provider.ts +1 -1
  65. package/src/contracts/pi.ts +3 -1
  66. package/src/contracts/prefix-cache.ts +13 -3
  67. package/src/contracts/scheduling.ts +9 -0
  68. package/src/engine/execution-plan.ts +23 -12
  69. package/src/engine/execution-tasks.ts +44 -0
  70. package/src/engine/namespaced-cache.ts +13 -0
  71. package/src/engine/scheduler.ts +22 -2
  72. package/src/fill/fill-session.ts +14 -3
  73. package/src/generate.ts +28 -22
  74. package/src/grammar.ts +13 -0
  75. package/src/inference/prefill.ts +6 -0
  76. package/src/kv-scheme.ts +22 -0
  77. package/src/kv-store.ts +115 -79
  78. package/src/lab/README.md +5 -1
  79. package/src/lab/paged-kv/paged-attention.ts +117 -0
  80. package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
  81. package/src/lab/paged-kv/paged-kv.ts +108 -42
  82. package/src/md-modules.d.ts +5 -0
  83. package/src/mlx/array.ts +37 -0
  84. package/src/mlx/ffi.ts +19 -0
  85. package/src/mlx/normalized-argmax.ts +116 -0
  86. package/src/mlx/ops.ts +97 -10
  87. package/src/model/batched-turboquant-kv.ts +6 -5
  88. package/src/model/delayed-quantized-kv.ts +3 -3
  89. package/src/model/delayed-rotating-quantized-kv.ts +1 -1
  90. package/src/model/delayed-turboquant-kv.ts +6 -4
  91. package/src/model/factory.ts +26 -0
  92. package/src/model/fingerprint.ts +12 -0
  93. package/src/model/full-prefill-row.ts +1 -1
  94. package/src/model/gemma4-base.ts +25 -6
  95. package/src/model/gemma4.ts +82 -2
  96. package/src/model/kv-attention-view.ts +4 -1
  97. package/src/model/profile.ts +29 -4
  98. package/src/model/quantized-append-attention.ts +31 -0
  99. package/src/model/qwen3-mrope.ts +7 -9
  100. package/src/model/qwen3_5.ts +99 -37
  101. package/src/model/support.ts +8 -0
  102. package/src/model/token-groups.ts +66 -0
  103. package/src/model/trellis-linear.ts +2 -2
  104. package/src/model/turboquant-codec.ts +6 -0
  105. package/src/model/whisper.ts +435 -0
  106. package/src/native/mic_capture.swift +92 -0
  107. package/src/native-pack.ts +6 -5
  108. package/src/pi-web.ts +9 -1
  109. package/src/prompt-cache.ts +265 -43
  110. package/src/runtime-config.ts +1 -5
  111. package/src/safetensors.ts +9 -2
  112. package/src/sampler.ts +5 -4
  113. package/src/serve/audio-routes.ts +323 -0
  114. package/src/serve/chat-request.ts +2 -0
  115. package/src/serve/chat-stage.ts +17 -12
  116. package/src/serve/completion-executor.ts +9 -1
  117. package/src/serve/discovery-routes.ts +17 -1
  118. package/src/serve/generated-token-history.ts +57 -0
  119. package/src/serve/generation-gateway.ts +52 -11
  120. package/src/serve/model-binding.ts +7 -1
  121. package/src/serve/preparation.ts +3 -3
  122. package/src/serve/prompt-response-trace.ts +14 -0
  123. package/src/serve/request-plan.ts +2 -0
  124. package/src/serve/request-prep.ts +38 -21
  125. package/src/serve/transcription-server.ts +64 -0
  126. package/src/serve/transcription-service.ts +502 -0
  127. package/src/server.ts +132 -164
  128. package/src/spec/assistant-rows.ts +1 -0
  129. package/src/spec/deepspec-rows.ts +1 -1
  130. package/src/spec/dflash-source.ts +1 -1
  131. package/src/spec/glm52-mtp-source.ts +1 -0
  132. package/src/spec/ngram-source.ts +27 -5
  133. package/src/spec/projected-draft-rows.ts +2 -2
  134. package/src/spec/qwen-mtp-rows.ts +11 -0
  135. package/src/spec/qwen-mtp-source.ts +3 -0
  136. package/src/spec/serve-loop.ts +5 -3
  137. package/src/spec/source.ts +14 -1
  138. package/src/spec/two-model-rows.ts +1 -0
  139. package/src/spec/two-model.ts +2 -2
  140. package/src/ssd-cache.ts +76 -17
  141. package/src/ssd-durability.ts +20 -22
  142. package/src/storage/host-buffer.ts +24 -0
  143. package/src/storage/kv-writer.ts +83 -0
  144. package/src/storage/kv-writer.worker.js +264 -0
  145. package/src/storage/retention-policy.ts +37 -0
  146. package/src/tiered-prompt-cache.ts +45 -0
  147. package/src/vision/embedder.ts +8 -0
  148. package/src/vision/prompt.ts +101 -65
  149. package/src/vision/qwen3vl-prompt.ts +101 -64
  150. package/src/vision/qwen3vl-tower.ts +11 -0
  151. package/src/vision/siglip.ts +8 -0
  152. package/src/web/app.html +12 -0
  153. package/src/web/app.js +160 -0
  154. package/src/web/src/chat.ts +4 -1
  155. package/src/web/src/composer.ts +3 -0
  156. package/src/web/src/voice.ts +141 -0
  157. package/src/web/sw.js +1 -1
  158. /package/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
@@ -0,0 +1,141 @@
1
+ // Hold-to-talk voice input for the composer: microphone → 16 kHz float32
2
+ // (an AudioContext opened at 16 kHz lets the browser resample the mic) →
3
+ // 250 ms chunks streamed to /v1/audio/sessions/<id>/audio while the button
4
+ // is held → finish on release → transcript inserted at the caret. Works
5
+ // with ANY served chat model: transcription is the Whisper companion, not
6
+ // the chat model's audio input. Shown only when `ready.transcription` is
7
+ // true (composer.ts updateAttachHint).
8
+
9
+ import { $, toast } from "./shell";
10
+
11
+ const SAMPLE_RATE = 16_000;
12
+ const CHUNK_SECONDS = 0.25;
13
+
14
+ interface Take {
15
+ id: string;
16
+ ctx: AudioContext;
17
+ stream: MediaStream;
18
+ node: ScriptProcessorNode;
19
+ pending: Float32Array[];
20
+ pendingLen: number;
21
+ sending: Promise<void>;
22
+ failed: string | null;
23
+ }
24
+
25
+ let take: Take | null = null;
26
+ let starting = false;
27
+
28
+ async function postChunk(id: string, pcm: Float32Array): Promise<void> {
29
+ const r = await fetch(`/v1/audio/sessions/${id}/audio`, {
30
+ method: "POST", headers: { "content-type": "audio/pcm;rate=16000" },
31
+ body: new Uint8Array(pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + pcm.byteLength) as ArrayBuffer),
32
+ });
33
+ if (!r.ok) throw new Error(`audio chunk rejected (${r.status})`);
34
+ }
35
+
36
+ function flush(t: Take): void {
37
+ if (t.pendingLen === 0) return;
38
+ const buf = new Float32Array(t.pendingLen);
39
+ let o = 0;
40
+ for (const p of t.pending) { buf.set(p, o); o += p.length; }
41
+ t.pending = []; t.pendingLen = 0;
42
+ t.sending = t.sending.then(() => postChunk(t.id, buf)).catch((e) => { t.failed = (e as Error).message; });
43
+ }
44
+
45
+ async function begin(btn: HTMLButtonElement): Promise<void> {
46
+ if (take || starting) return;
47
+ starting = true;
48
+ btn.classList.add("busy");
49
+ try {
50
+ const r = await fetch("/v1/audio/sessions", {
51
+ method: "POST", headers: { "content-type": "application/json" },
52
+ body: JSON.stringify({ vad: true, vad_min_speech_ms: 120, temperature: 0 }),
53
+ });
54
+ if (!r.ok) throw new Error(r.status === 503 ? "no Whisper model on this server" : `session failed (${r.status})`);
55
+ const { id } = await r.json() as { id: string };
56
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } });
57
+ const ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
58
+ const src = ctx.createMediaStreamSource(stream);
59
+ // ScriptProcessorNode is deprecated but universally available and enough
60
+ // for 16 kHz mono capture; an AudioWorklet needs a separately served file.
61
+ const node = ctx.createScriptProcessor(4096, 1, 1);
62
+ const t: Take = { id, ctx, stream, node, pending: [], pendingLen: 0, sending: Promise.resolve(), failed: null };
63
+ node.onaudioprocess = (e) => {
64
+ if (take !== t) return;
65
+ const data = e.inputBuffer.getChannelData(0);
66
+ t.pending.push(new Float32Array(data));
67
+ t.pendingLen += data.length;
68
+ if (t.pendingLen >= SAMPLE_RATE * CHUNK_SECONDS) flush(t);
69
+ };
70
+ src.connect(node);
71
+ node.connect(ctx.destination); // required for onaudioprocess to fire in some browsers
72
+ take = t;
73
+ btn.classList.add("recording");
74
+ } catch (e) {
75
+ toast(`Voice input failed: ${(e as Error).message}`, "err");
76
+ } finally {
77
+ starting = false;
78
+ btn.classList.remove("busy");
79
+ }
80
+ }
81
+
82
+ async function end(btn: HTMLButtonElement, box: HTMLTextAreaElement): Promise<void> {
83
+ const t = take;
84
+ if (!t) return;
85
+ take = null;
86
+ btn.classList.remove("recording");
87
+ btn.classList.add("busy");
88
+ try {
89
+ t.node.disconnect();
90
+ for (const track of t.stream.getTracks()) track.stop();
91
+ void t.ctx.close();
92
+ flush(t);
93
+ await t.sending;
94
+ if (t.failed) throw new Error(t.failed);
95
+ const r = await fetch(`/v1/audio/sessions/${t.id}/finish`, { method: "POST" });
96
+ if (!r.ok) throw new Error(`transcription failed (${r.status})`);
97
+ const body = await r.json() as { text: string; mlx_bun?: { vad?: { speech: boolean } } };
98
+ const text = (body.text || "").trim();
99
+ if (!text) { toast(body.mlx_bun?.vad?.speech === false ? "No speech detected" : "Nothing transcribed", ""); return; }
100
+ insertAtCaret(box, text);
101
+ } catch (e) {
102
+ toast(`Voice input failed: ${(e as Error).message}`, "err");
103
+ } finally {
104
+ btn.classList.remove("busy");
105
+ }
106
+ }
107
+
108
+ function insertAtCaret(box: HTMLTextAreaElement, text: string): void {
109
+ const start = box.selectionStart ?? box.value.length;
110
+ const endPos = box.selectionEnd ?? start;
111
+ const before = box.value.slice(0, start);
112
+ const after = box.value.slice(endPos);
113
+ const lead = before.length && !/\s$/.test(before) ? " " : "";
114
+ const trail = after.length && !/^\s/.test(after) ? " " : "";
115
+ box.value = before + lead + text + trail + after;
116
+ const caret = (before + lead + text + trail).length;
117
+ box.setSelectionRange(caret, caret);
118
+ box.dispatchEvent(new Event("input", { bubbles: true }));
119
+ box.focus();
120
+ }
121
+
122
+ /** Wire hold-to-talk on `btn`: pointer hold, or Space/Enter held while focused. */
123
+ export function initVoiceInput(btn: HTMLButtonElement, box: HTMLTextAreaElement): void {
124
+ if (!btn) return;
125
+ if (!navigator.mediaDevices?.getUserMedia) { btn.title = "Microphone capture needs a secure context (https or localhost)"; }
126
+ btn.addEventListener("pointerdown", (e) => { e.preventDefault(); btn.setPointerCapture(e.pointerId); void begin(btn); });
127
+ const release = () => { void end(btn, box); };
128
+ btn.addEventListener("pointerup", release);
129
+ btn.addEventListener("pointercancel", release);
130
+ btn.addEventListener("lostpointercapture", release);
131
+ let keyHeld = false;
132
+ btn.addEventListener("keydown", (e) => {
133
+ if ((e.key === " " || e.key === "Enter") && !keyHeld) { e.preventDefault(); keyHeld = true; void begin(btn); }
134
+ });
135
+ btn.addEventListener("keyup", (e) => {
136
+ if ((e.key === " " || e.key === "Enter") && keyHeld) { e.preventDefault(); keyHeld = false; release(); }
137
+ });
138
+ btn.addEventListener("blur", () => { if (keyHeld) { keyHeld = false; release(); } });
139
+ // expose for tests / the palette
140
+ ($ as unknown as { voice?: unknown }).voice = { begin: () => begin(btn), end: () => end(btn, box) };
141
+ }
package/src/web/sw.js CHANGED
@@ -18,7 +18,7 @@
18
18
  //
19
19
  // Versioned by CACHE_NAME so a deploy invalidates old shells; bump the
20
20
  // suffix here if the shell file set changes.
21
- const CACHE_NAME = "mlxbun-shell-v1";
21
+ const CACHE_NAME = "mlxbun-shell-v2";
22
22
  const SHELL_FILES = [
23
23
  "/",
24
24
  "/assets/app.js",