mlx-bun 0.3.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.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { captureKvAttention } from "../kv-attention-view";
|
|
1
2
|
// UniversalDense — the Tier-0 config-driven dense llama-family module
|
|
2
3
|
// (docs/design/generic-model-support.md §3.1). One module, shaped exactly
|
|
3
4
|
// like qwen3.ts, with every arch delta selected by the UniversalArgs
|
|
4
5
|
// descriptor (archs.ts). Monolith path only: no compiled
|
|
5
|
-
// decode
|
|
6
|
+
// decode. Standard attention uses the shared plain/affine KV port; the
|
|
7
|
+
// softcap branch retains plain KV. The bar is L1 bit-exactness
|
|
6
8
|
// vs mlx-lm on this machine's GPU (tests/parity/universal-parity.test.ts).
|
|
7
9
|
//
|
|
8
10
|
// Porting discipline: each branch transcribes its mlx-lm source op-for-op
|
|
@@ -154,19 +156,18 @@ class UniversalAttention {
|
|
|
154
156
|
? this.rope.applyDynamic(k, offArr)
|
|
155
157
|
: this.rope.apply(k, cache.offset));
|
|
156
158
|
|
|
157
|
-
const [keys, values] = cache.updateAndFetch(k, v);
|
|
158
|
-
k.dispose();
|
|
159
|
-
v.dispose();
|
|
160
|
-
|
|
161
159
|
let attn: MlxArray;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
try {
|
|
161
|
+
if (a.attnLogitSoftcap !== null) {
|
|
162
|
+
const [keys, values] = cache.updateAndFetch(k, v);
|
|
163
|
+
try { attn = this.#softcapAttention(q, keys, values, mask, B, L); }
|
|
164
|
+
finally { keys.dispose(); values.dispose(); }
|
|
165
|
+
} else {
|
|
166
|
+
const view = captureKvAttention(cache, k, v);
|
|
167
|
+
try { attn = view.attend(q, a.attnScale, mask); }
|
|
168
|
+
finally { view.dispose(); }
|
|
169
|
+
}
|
|
170
|
+
} finally { k.dispose(); v.dispose(); q.dispose(); }
|
|
170
171
|
|
|
171
172
|
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
172
173
|
attn.dispose();
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// Whisper (encoder-decoder speech recognition) — an op-for-op port of
|
|
2
|
+
// mlx-whisper's whisper.py (mlx-community/whisper-* checkpoints: fp16
|
|
3
|
+
// weights in a single weights.safetensors + config.json ModelDimensions).
|
|
4
|
+
// L1 gate: encoder output and per-step decoder logits bit-exact vs the
|
|
5
|
+
// pinned mlx-whisper oracle (tests/parity/whisper.test.ts).
|
|
6
|
+
//
|
|
7
|
+
// Graph (dtype = weight dtype, fp16 for the shipped artifacts):
|
|
8
|
+
// encoder: mel [B,3000,n_mels] → conv1(k3,p1)+gelu → conv2(k3,s2,p1)+gelu
|
|
9
|
+
// → + sinusoids(1500, D) → N × block(self-attn, mlp) → ln_post
|
|
10
|
+
// decoder: tokens [B,T] → token_embedding + positional_embedding[offset:]
|
|
11
|
+
// → N × block(causal self-attn w/ cache, cross-attn on the
|
|
12
|
+
// encoder output w/ cross-KV cache, mlp) → ln → @ embeddingᵀ
|
|
13
|
+
// gelu: mlx.nn.gelu is `@partial(mx.compile, shapeless=True)` in the
|
|
14
|
+
// oracle — the FUSED kernel rounds differently from the four
|
|
15
|
+
// separate ops (2% of conv1 outputs differ by an ulp), so the same
|
|
16
|
+
// graph is compiled here too (bit-exact, verified 2026-09-15).
|
|
17
|
+
// attention: q,k scaled by (D/H)^-0.25 each, qk in the weight dtype,
|
|
18
|
+
// additive causal mask (-inf f16), precise softmax, no fused
|
|
19
|
+
// kernels — the FAITHFUL path. The optimized path lives in
|
|
20
|
+
// src/audio/whisper-fast.ts and is gated token-exact against
|
|
21
|
+
// this one.
|
|
22
|
+
//
|
|
23
|
+
// mlx inline-temporary hazard (CLAUDE.md): every op result is held in a
|
|
24
|
+
// local and disposed; no nested `ops.foo(x.slice(...))` chains.
|
|
25
|
+
|
|
26
|
+
import type { ModelConfig } from "../config";
|
|
27
|
+
import { MlxArray } from "../mlx/array";
|
|
28
|
+
import { CompiledFunction } from "../mlx/compile";
|
|
29
|
+
import { WhisperFastPath } from "../audio/whisper-fast";
|
|
30
|
+
import { Dtype } from "../mlx/ffi";
|
|
31
|
+
import * as ops from "../mlx/ops";
|
|
32
|
+
import type { Weights } from "../weights";
|
|
33
|
+
|
|
34
|
+
export interface WhisperDims {
|
|
35
|
+
nMels: number;
|
|
36
|
+
nAudioCtx: number;
|
|
37
|
+
nAudioState: number;
|
|
38
|
+
nAudioHead: number;
|
|
39
|
+
nAudioLayer: number;
|
|
40
|
+
nVocab: number;
|
|
41
|
+
nTextCtx: number;
|
|
42
|
+
nTextState: number;
|
|
43
|
+
nTextHead: number;
|
|
44
|
+
nTextLayer: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseWhisperDims(raw: Record<string, unknown>): WhisperDims {
|
|
48
|
+
const n = (k: string): number => {
|
|
49
|
+
const v = raw[k];
|
|
50
|
+
if (typeof v !== "number") throw new Error(`whisper config.json: missing numeric ${k}`);
|
|
51
|
+
return v;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
nMels: n("n_mels"), nAudioCtx: n("n_audio_ctx"), nAudioState: n("n_audio_state"),
|
|
55
|
+
nAudioHead: n("n_audio_head"), nAudioLayer: n("n_audio_layer"), nVocab: n("n_vocab"),
|
|
56
|
+
nTextCtx: n("n_text_ctx"), nTextState: n("n_text_state"), nTextHead: n("n_text_head"),
|
|
57
|
+
nTextLayer: n("n_text_layer"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dispose = (old: MlxArray, next: MlxArray): MlxArray => {
|
|
62
|
+
old.dispose();
|
|
63
|
+
return next;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Per-layer decoder state: self-attention K/V [B, T, D] (un-split, un-scaled,
|
|
67
|
+
* exactly what the oracle concatenates) and the cross-attention K/V computed
|
|
68
|
+
* once from the encoder output. */
|
|
69
|
+
export interface WhisperLayerCache {
|
|
70
|
+
k: MlxArray | null;
|
|
71
|
+
v: MlxArray | null;
|
|
72
|
+
crossK: MlxArray | null;
|
|
73
|
+
crossV: MlxArray | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class WhisperKvCache {
|
|
77
|
+
readonly layers: WhisperLayerCache[];
|
|
78
|
+
constructor(nLayers: number) {
|
|
79
|
+
this.layers = Array.from({ length: nLayers }, () => ({ k: null, v: null, crossK: null, crossV: null }));
|
|
80
|
+
}
|
|
81
|
+
/** Tokens already in the self-attention cache. */
|
|
82
|
+
get offset(): number {
|
|
83
|
+
return this.layers[0]?.k?.shape[1] ?? 0;
|
|
84
|
+
}
|
|
85
|
+
/** Beam reorder: keep rows `indices` (mx.take along axis 0). Cross K/V
|
|
86
|
+
* rows are identical across beams and are left untouched when they were
|
|
87
|
+
* broadcast from one audio (shape[0] === 1). */
|
|
88
|
+
rearrange(indices: number[]): void {
|
|
89
|
+
if (indices.every((v, i) => v === i)) return;
|
|
90
|
+
const idx = ops.fromInt32(indices, [indices.length]);
|
|
91
|
+
for (const l of this.layers) {
|
|
92
|
+
if (l.k) l.k = dispose(l.k, ops.takeAxis(l.k, idx, 0));
|
|
93
|
+
if (l.v) l.v = dispose(l.v, ops.takeAxis(l.v, idx, 0));
|
|
94
|
+
if (l.crossK && l.crossK.shape[0] !== 1) l.crossK = dispose(l.crossK, ops.takeAxis(l.crossK, idx, 0));
|
|
95
|
+
if (l.crossV && l.crossV.shape[0] !== 1) l.crossV = dispose(l.crossV, ops.takeAxis(l.crossV, idx, 0));
|
|
96
|
+
}
|
|
97
|
+
idx.dispose();
|
|
98
|
+
}
|
|
99
|
+
arrays(): MlxArray[] {
|
|
100
|
+
const out: MlxArray[] = [];
|
|
101
|
+
for (const l of this.layers) for (const a of [l.k, l.v, l.crossK, l.crossV]) if (a) out.push(a);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
dispose(): void {
|
|
105
|
+
for (const l of this.layers) {
|
|
106
|
+
l.k?.dispose(); l.v?.dispose(); l.crossK?.dispose(); l.crossV?.dispose();
|
|
107
|
+
l.k = l.v = l.crossK = l.crossV = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class WhisperModel {
|
|
113
|
+
readonly dims: WhisperDims;
|
|
114
|
+
readonly dtype: Dtype;
|
|
115
|
+
readonly #w: Weights;
|
|
116
|
+
/** [1500, D] encoder positional sinusoids (weight dtype). */
|
|
117
|
+
#encPos: MlxArray | null = null;
|
|
118
|
+
/** [n_text_ctx, n_text_ctx] additive causal mask (weight dtype, -inf). */
|
|
119
|
+
#causalMask: MlxArray | null = null;
|
|
120
|
+
/** Transposed weight views, cached per name. */
|
|
121
|
+
readonly #transposed = new Map<string, MlxArray>();
|
|
122
|
+
/** Compiled nn.gelu (see header). */
|
|
123
|
+
#gelu: CompiledFunction | null = null;
|
|
124
|
+
#fast: WhisperFastPath | null = null;
|
|
125
|
+
|
|
126
|
+
/** The optimized execution path (src/audio/whisper-fast.ts), built lazily. */
|
|
127
|
+
get fast(): WhisperFastPath {
|
|
128
|
+
return this.#fast ??= new WhisperFastPath(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
constructor(weights: Weights, config: ModelConfig) {
|
|
132
|
+
this.#w = weights;
|
|
133
|
+
this.dims = parseWhisperDims(config.raw as Record<string, unknown>);
|
|
134
|
+
this.dtype = weights.tensor("decoder.token_embedding.weight").dtype;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get isMultilingual(): boolean {
|
|
138
|
+
return this.dims.nVocab >= 51865;
|
|
139
|
+
}
|
|
140
|
+
get numLanguages(): number {
|
|
141
|
+
return this.dims.nVocab - 51765 - (this.isMultilingual ? 1 : 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Alignment heads [(layer, head), …] from the checkpoint (word timestamps);
|
|
145
|
+
* falls back to every head of the last half of the decoder layers. */
|
|
146
|
+
get alignmentHeads(): [number, number][] {
|
|
147
|
+
if (this.#w.has("alignment_heads")) {
|
|
148
|
+
const a = this.#w.tensor("alignment_heads");
|
|
149
|
+
const flat = a.astype(Dtype.int32).toIntTokens();
|
|
150
|
+
const out: [number, number][] = [];
|
|
151
|
+
for (let i = 0; i + 1 < flat.length; i += 2) out.push([flat[i]!, flat[i + 1]!]);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
const out: [number, number][] = [];
|
|
155
|
+
for (let l = Math.floor(this.dims.nTextLayer / 2); l < this.dims.nTextLayer; l++)
|
|
156
|
+
for (let h = 0; h < this.dims.nTextHead; h++) out.push([l, h]);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- primitives -----------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
#t(name: string): MlxArray {
|
|
163
|
+
return this.#w.tensor(name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Raw checkpoint tensor (borrowed; the fast path composes its own graphs). */
|
|
167
|
+
weight(name: string): MlxArray {
|
|
168
|
+
return this.#t(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Cached Wᵀ view (borrowed). */
|
|
172
|
+
weightT(name: string): MlxArray {
|
|
173
|
+
return this.#wT(name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The compiled nn.gelu (shared with the faithful path). */
|
|
177
|
+
gelu(x: MlxArray): MlxArray {
|
|
178
|
+
return this.#geluCompiled(x);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
layerNorm(x: MlxArray, prefix: string): MlxArray {
|
|
182
|
+
return this.#layerNorm(x, prefix);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** nn.Linear against a checkpoint weight (borrowed inputs, owned output). */
|
|
186
|
+
linear(x: MlxArray, prefix: string, bias = true): MlxArray {
|
|
187
|
+
return this.#linear(x, prefix, bias);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Wᵀ view, cached (transpose is a strided view, no copy). */
|
|
191
|
+
#wT(name: string): MlxArray {
|
|
192
|
+
let t = this.#transposed.get(name);
|
|
193
|
+
if (!t) {
|
|
194
|
+
t = ops.transposeAxes(this.#t(name), [1, 0]);
|
|
195
|
+
this.#transposed.set(name, t);
|
|
196
|
+
}
|
|
197
|
+
return t;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** nn.Linear: addmm(bias, x, Wᵀ) with bias, x @ Wᵀ without. */
|
|
201
|
+
#linear(x: MlxArray, prefix: string, bias = true): MlxArray {
|
|
202
|
+
const wT = this.#wT(`${prefix}.weight`);
|
|
203
|
+
return bias ? ops.addmm(this.#t(`${prefix}.bias`), x, wT) : ops.matmul(x, wT);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** nn.gelu — compiled, shapeless, same op graph as mlx.nn.activations.gelu:
|
|
207
|
+
* x * (1 + erf(x / sqrt(2))) / 2. */
|
|
208
|
+
#geluCompiled(x: MlxArray): MlxArray {
|
|
209
|
+
this.#gelu ??= new CompiledFunction(([a]) => {
|
|
210
|
+
const s2 = ops.scalarLike(Math.SQRT2, a!);
|
|
211
|
+
const xs = ops.div(a!, s2);
|
|
212
|
+
const e = ops.erf(xs);
|
|
213
|
+
const one = ops.scalarLike(1, a!);
|
|
214
|
+
const e1 = ops.add(one, e);
|
|
215
|
+
const xe = ops.mul(a!, e1);
|
|
216
|
+
const two = ops.scalarLike(2, a!);
|
|
217
|
+
return [ops.div(xe, two)];
|
|
218
|
+
}, true);
|
|
219
|
+
return this.#gelu.apply([x])[0]!;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#layerNorm(x: MlxArray, prefix: string): MlxArray {
|
|
223
|
+
return ops.layerNorm(x, this.#t(`${prefix}.weight`), this.#t(`${prefix}.bias`), 1e-5);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** MultiHeadAttention.__call__ + qkv_attention. Returns the projected
|
|
227
|
+
* output, the (un-split) k/v to cache, and optionally the raw qk. */
|
|
228
|
+
#attention(
|
|
229
|
+
x: MlxArray, prefix: string, nHead: number,
|
|
230
|
+
opts: { xa?: MlxArray; mask?: MlxArray; kv?: [MlxArray, MlxArray] | null; crossKv?: [MlxArray, MlxArray] | null; wantQk?: boolean },
|
|
231
|
+
): { out: MlxArray; k: MlxArray; v: MlxArray; qk: MlxArray | null } {
|
|
232
|
+
const q = this.#linear(x, `${prefix}.query`);
|
|
233
|
+
let k: MlxArray;
|
|
234
|
+
let v: MlxArray;
|
|
235
|
+
if (!opts.xa) {
|
|
236
|
+
k = this.#linear(x, `${prefix}.key`, false);
|
|
237
|
+
v = this.#linear(x, `${prefix}.value`);
|
|
238
|
+
if (opts.kv) {
|
|
239
|
+
k = dispose(k, ops.concatAxis([opts.kv[0], k], 1));
|
|
240
|
+
v = dispose(v, ops.concatAxis([opts.kv[1], v], 1));
|
|
241
|
+
}
|
|
242
|
+
} else if (!opts.crossKv) {
|
|
243
|
+
k = this.#linear(opts.xa, `${prefix}.key`, false);
|
|
244
|
+
v = this.#linear(opts.xa, `${prefix}.value`);
|
|
245
|
+
} else {
|
|
246
|
+
[k, v] = opts.crossKv;
|
|
247
|
+
}
|
|
248
|
+
const [B, T, D] = q.shape as [number, number, number];
|
|
249
|
+
// k/v keep THEIR batch (the oracle reshapes with k.shape): beam rows
|
|
250
|
+
// attend to one broadcast encoder output ([1, 1500, D]) via matmul
|
|
251
|
+
// broadcasting instead of copying cross-K/V per beam.
|
|
252
|
+
const Bk = k.shape[0]!;
|
|
253
|
+
const Tk = k.shape[1]!;
|
|
254
|
+
const hd = D / nHead;
|
|
255
|
+
const scale = ops.scalarLike(hd ** -0.25, q);
|
|
256
|
+
let qh = ops.reshape(q, [B, T, nHead, hd]);
|
|
257
|
+
q.dispose();
|
|
258
|
+
qh = dispose(qh, ops.transposeAxes(qh, [0, 2, 1, 3]));
|
|
259
|
+
qh = dispose(qh, ops.mul(qh, scale));
|
|
260
|
+
let kh = ops.reshape(k, [Bk, Tk, nHead, hd]);
|
|
261
|
+
kh = dispose(kh, ops.transposeAxes(kh, [0, 2, 3, 1]));
|
|
262
|
+
kh = dispose(kh, ops.mul(kh, scale));
|
|
263
|
+
scale.dispose();
|
|
264
|
+
let vh = ops.reshape(v, [Bk, Tk, nHead, hd]);
|
|
265
|
+
vh = dispose(vh, ops.transposeAxes(vh, [0, 2, 1, 3]));
|
|
266
|
+
let qk = ops.matmul(qh, kh);
|
|
267
|
+
qh.dispose();
|
|
268
|
+
kh.dispose();
|
|
269
|
+
if (opts.mask) {
|
|
270
|
+
const m = opts.mask.slice([0, 0], [T, T]);
|
|
271
|
+
qk = dispose(qk, ops.add(qk, m));
|
|
272
|
+
m.dispose();
|
|
273
|
+
}
|
|
274
|
+
const w = ops.softmaxAxis(qk, -1, true);
|
|
275
|
+
let o = ops.matmul(w, vh);
|
|
276
|
+
w.dispose();
|
|
277
|
+
vh.dispose();
|
|
278
|
+
o = dispose(o, ops.transposeAxes(o, [0, 2, 1, 3]));
|
|
279
|
+
o = dispose(o, ops.reshape(o, [B, T, D]));
|
|
280
|
+
const out = this.#linear(o, `${prefix}.out`);
|
|
281
|
+
o.dispose();
|
|
282
|
+
if (!opts.wantQk) {
|
|
283
|
+
qk.dispose();
|
|
284
|
+
return { out, k, v, qk: null };
|
|
285
|
+
}
|
|
286
|
+
return { out, k, v, qk };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#mlp(x: MlxArray, prefix: string): MlxArray {
|
|
290
|
+
let h = this.#layerNorm(x, `${prefix}.mlp_ln`);
|
|
291
|
+
h = dispose(h, this.#linear(h, `${prefix}.mlp1`));
|
|
292
|
+
h = dispose(h, this.#geluCompiled(h));
|
|
293
|
+
h = dispose(h, this.#linear(h, `${prefix}.mlp2`));
|
|
294
|
+
return h;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- encoder ----------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
#sinusoids(): MlxArray {
|
|
300
|
+
if (this.#encPos) return this.#encPos;
|
|
301
|
+
const { nAudioCtx: length, nAudioState: channels } = this.dims;
|
|
302
|
+
const half = channels / 2;
|
|
303
|
+
const inc = Math.log(10000) / (half - 1);
|
|
304
|
+
const ar = ops.arange(0, half, 1, Dtype.int32);
|
|
305
|
+
const negInc = MlxArray.fromFloat32(new Float32Array([-inc]), []);
|
|
306
|
+
let inv = ops.mul(ar, negInc);
|
|
307
|
+
ar.dispose();
|
|
308
|
+
negInc.dispose();
|
|
309
|
+
inv = dispose(inv, ops.exp(inv));
|
|
310
|
+
inv = dispose(inv, ops.reshape(inv, [1, half]));
|
|
311
|
+
let pos = ops.arange(0, length, 1, Dtype.int32);
|
|
312
|
+
pos = dispose(pos, ops.reshape(pos, [length, 1]));
|
|
313
|
+
const scaled = ops.mul(pos, inv);
|
|
314
|
+
pos.dispose();
|
|
315
|
+
inv.dispose();
|
|
316
|
+
const s = ops.sin(scaled);
|
|
317
|
+
const c = ops.cos(scaled);
|
|
318
|
+
scaled.dispose();
|
|
319
|
+
let pe = ops.concatAxis([s, c], 1);
|
|
320
|
+
s.dispose();
|
|
321
|
+
c.dispose();
|
|
322
|
+
pe = dispose(pe, pe.astype(this.dtype));
|
|
323
|
+
this.#encPos = pe;
|
|
324
|
+
return pe;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** AudioEncoder: mel [B, 3000, n_mels] (weight dtype) → [B, 1500, D]. */
|
|
328
|
+
encode(mel: MlxArray, trace?: (name: string, a: MlxArray) => void): MlxArray {
|
|
329
|
+
let x = ops.conv1d(mel, this.#t("encoder.conv1.weight"), 1, 1);
|
|
330
|
+
x = dispose(x, ops.add(x, this.#t("encoder.conv1.bias")));
|
|
331
|
+
x = dispose(x, this.#geluCompiled(x));
|
|
332
|
+
trace?.("conv1", x);
|
|
333
|
+
x = dispose(x, ops.conv1d(x, this.#t("encoder.conv2.weight"), 2, 1));
|
|
334
|
+
x = dispose(x, ops.add(x, this.#t("encoder.conv2.bias")));
|
|
335
|
+
x = dispose(x, this.#geluCompiled(x));
|
|
336
|
+
if (x.shape[1] !== this.dims.nAudioCtx || x.shape[2] !== this.dims.nAudioState)
|
|
337
|
+
throw new Error(`whisper encoder: incorrect audio shape ${JSON.stringify(x.shape)}`);
|
|
338
|
+
x = dispose(x, ops.add(x, this.#sinusoids()));
|
|
339
|
+
trace?.("conv2pos", x);
|
|
340
|
+
for (let i = 0; i < this.dims.nAudioLayer; i++) {
|
|
341
|
+
const p = `encoder.blocks.${i}`;
|
|
342
|
+
const ln = this.#layerNorm(x, `${p}.attn_ln`);
|
|
343
|
+
const a = this.#attention(ln, `${p}.attn`, this.dims.nAudioHead, {});
|
|
344
|
+
ln.dispose();
|
|
345
|
+
a.k.dispose();
|
|
346
|
+
a.v.dispose();
|
|
347
|
+
x = dispose(x, ops.add(x, a.out));
|
|
348
|
+
a.out.dispose();
|
|
349
|
+
const m = this.#mlp(x, p);
|
|
350
|
+
x = dispose(x, ops.add(x, m));
|
|
351
|
+
m.dispose();
|
|
352
|
+
trace?.(`block${i}`, x);
|
|
353
|
+
}
|
|
354
|
+
x = dispose(x, this.#layerNorm(x, "encoder.ln_post"));
|
|
355
|
+
return x;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- decoder ----------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
#mask(): MlxArray {
|
|
361
|
+
if (this.#causalMask) return this.#causalMask;
|
|
362
|
+
const n = this.dims.nTextCtx;
|
|
363
|
+
const m = new Float32Array(n * n);
|
|
364
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) m[i * n + j] = -Infinity;
|
|
365
|
+
const f32 = MlxArray.fromFloat32(m, [n, n]);
|
|
366
|
+
const out = f32.astype(this.dtype);
|
|
367
|
+
f32.dispose();
|
|
368
|
+
this.#causalMask = out;
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** TextDecoder.__call__: tokens [B, T] int32, audioFeatures [B|1, 1500, D].
|
|
373
|
+
* Returns logits [B, T, n_vocab] (weight dtype) and, when requested, the
|
|
374
|
+
* per-layer raw cross-attention qk for alignment. Mutates `cache`. */
|
|
375
|
+
decode(
|
|
376
|
+
tokens: MlxArray, audioFeatures: MlxArray, cache: WhisperKvCache,
|
|
377
|
+
opts: { wantCrossQk?: boolean } = {},
|
|
378
|
+
): { logits: MlxArray; crossQk: (MlxArray | null)[] } {
|
|
379
|
+
const offset = cache.offset;
|
|
380
|
+
const T = tokens.shape[1]!;
|
|
381
|
+
const embed = this.#t("decoder.token_embedding.weight");
|
|
382
|
+
let x = ops.takeAxis(embed, tokens, 0);
|
|
383
|
+
const pos = this.#t("decoder.positional_embedding").slice([offset, 0], [offset + T, this.dims.nTextState]);
|
|
384
|
+
x = dispose(x, ops.add(x, pos));
|
|
385
|
+
pos.dispose();
|
|
386
|
+
const mask = this.#mask();
|
|
387
|
+
const crossQk: (MlxArray | null)[] = [];
|
|
388
|
+
for (let i = 0; i < this.dims.nTextLayer; i++) {
|
|
389
|
+
const p = `decoder.blocks.${i}`;
|
|
390
|
+
const layer = cache.layers[i]!;
|
|
391
|
+
const ln = this.#layerNorm(x, `${p}.attn_ln`);
|
|
392
|
+
const a = this.#attention(ln, `${p}.attn`, this.dims.nTextHead, {
|
|
393
|
+
mask, kv: layer.k && layer.v ? [layer.k, layer.v] : null,
|
|
394
|
+
});
|
|
395
|
+
ln.dispose();
|
|
396
|
+
layer.k?.dispose();
|
|
397
|
+
layer.v?.dispose();
|
|
398
|
+
layer.k = a.k;
|
|
399
|
+
layer.v = a.v;
|
|
400
|
+
x = dispose(x, ops.add(x, a.out));
|
|
401
|
+
a.out.dispose();
|
|
402
|
+
const cln = this.#layerNorm(x, `${p}.cross_attn_ln`);
|
|
403
|
+
const c = this.#attention(cln, `${p}.cross_attn`, this.dims.nTextHead, {
|
|
404
|
+
xa: audioFeatures,
|
|
405
|
+
crossKv: layer.crossK && layer.crossV ? [layer.crossK, layer.crossV] : null,
|
|
406
|
+
wantQk: opts.wantCrossQk,
|
|
407
|
+
});
|
|
408
|
+
cln.dispose();
|
|
409
|
+
if (!layer.crossK) {
|
|
410
|
+
layer.crossK = c.k;
|
|
411
|
+
layer.crossV = c.v;
|
|
412
|
+
}
|
|
413
|
+
x = dispose(x, ops.add(x, c.out));
|
|
414
|
+
c.out.dispose();
|
|
415
|
+
crossQk.push(c.qk);
|
|
416
|
+
const m = this.#mlp(x, p);
|
|
417
|
+
x = dispose(x, ops.add(x, m));
|
|
418
|
+
m.dispose();
|
|
419
|
+
}
|
|
420
|
+
x = dispose(x, this.#layerNorm(x, "decoder.ln"));
|
|
421
|
+
const logits = ops.matmul(x, this.#wT("decoder.token_embedding.weight"));
|
|
422
|
+
x.dispose();
|
|
423
|
+
return { logits, crossQk };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
dispose(): void {
|
|
427
|
+
this.#encPos?.dispose();
|
|
428
|
+
this.#causalMask?.dispose();
|
|
429
|
+
for (const t of this.#transposed.values()) t.dispose();
|
|
430
|
+
this.#transposed.clear();
|
|
431
|
+
this.#fast?.dispose();
|
|
432
|
+
this.#gelu?.dispose();
|
|
433
|
+
this.#w.dispose();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Microphone capture sidecar for `mlx-bun dictate`: AVAudioEngine input
|
|
2
|
+
// → 16 kHz mono float32 little-endian PCM on stdout, continuously, in
|
|
3
|
+
// 20 ms buffers. Control lines go to stderr: "ready", and with --hotkey
|
|
4
|
+
// <keycode>, "hotkey down" / "hotkey up" from a CGEvent listen-only tap
|
|
5
|
+
// (needs the terminal app to have Input Monitoring; the mic needs
|
|
6
|
+
// Microphone permission — macOS prompts on first use).
|
|
7
|
+
//
|
|
8
|
+
// mic-capture [--rate 16000] [--hotkey 61] (61 = Right Option)
|
|
9
|
+
|
|
10
|
+
import AVFoundation
|
|
11
|
+
import CoreGraphics
|
|
12
|
+
import Foundation
|
|
13
|
+
|
|
14
|
+
var rate: Double = 16000
|
|
15
|
+
var hotkey: Int64? = nil
|
|
16
|
+
var args = Array(CommandLine.arguments.dropFirst())
|
|
17
|
+
while !args.isEmpty {
|
|
18
|
+
let a = args.removeFirst()
|
|
19
|
+
switch a {
|
|
20
|
+
case "--rate": rate = Double(args.removeFirst()) ?? 16000
|
|
21
|
+
case "--hotkey": hotkey = Int64(args.removeFirst())
|
|
22
|
+
default:
|
|
23
|
+
FileHandle.standardError.write("usage: mic-capture [--rate 16000] [--hotkey <keycode>]\n".data(using: .utf8)!)
|
|
24
|
+
exit(2)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let err = FileHandle.standardError
|
|
29
|
+
let out = FileHandle.standardOutput
|
|
30
|
+
let writeLock = NSLock()
|
|
31
|
+
func note(_ s: String) { err.write((s + "\n").data(using: .utf8)!) }
|
|
32
|
+
|
|
33
|
+
var hotkeyDown = false
|
|
34
|
+
let engine = AVAudioEngine()
|
|
35
|
+
let input = engine.inputNode
|
|
36
|
+
let inFormat = input.outputFormat(forBus: 0)
|
|
37
|
+
guard let outFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: rate, channels: 1, interleaved: true),
|
|
38
|
+
let converter = AVAudioConverter(from: inFormat, to: outFormat) else {
|
|
39
|
+
note("error: cannot build audio converter"); exit(1)
|
|
40
|
+
}
|
|
41
|
+
input.installTap(onBus: 0, bufferSize: AVAudioFrameCount(inFormat.sampleRate * 0.02), format: inFormat) { buffer, _ in
|
|
42
|
+
let ratio = rate / inFormat.sampleRate
|
|
43
|
+
let capacity = AVAudioFrameCount(Double(buffer.frameLength) * ratio) + 32
|
|
44
|
+
guard let outBuf = AVAudioPCMBuffer(pcmFormat: outFormat, frameCapacity: capacity) else { return }
|
|
45
|
+
var consumed = false
|
|
46
|
+
var convError: NSError? = nil
|
|
47
|
+
converter.convert(to: outBuf, error: &convError) { _, status in
|
|
48
|
+
if consumed { status.pointee = .noDataNow; return nil }
|
|
49
|
+
consumed = true
|
|
50
|
+
status.pointee = .haveData
|
|
51
|
+
return buffer
|
|
52
|
+
}
|
|
53
|
+
if convError != nil || outBuf.frameLength == 0 { return }
|
|
54
|
+
let bytes = Data(bytes: outBuf.floatChannelData![0], count: Int(outBuf.frameLength) * 4)
|
|
55
|
+
writeLock.lock(); out.write(bytes); writeLock.unlock()
|
|
56
|
+
}
|
|
57
|
+
do { try engine.start() } catch { note("error: audio engine failed: \(error)"); exit(1) }
|
|
58
|
+
note("ready rate=\(Int(rate)) input=\(Int(inFormat.sampleRate))Hz/\(inFormat.channelCount)ch")
|
|
59
|
+
|
|
60
|
+
if let key = hotkey {
|
|
61
|
+
let mask: CGEventMask = (1 << CGEventType.flagsChanged.rawValue) | (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
|
|
62
|
+
let callback: CGEventTapCallBack = { _, type, event, _ in
|
|
63
|
+
let code = event.getIntegerValueField(.keyboardEventKeycode)
|
|
64
|
+
if code == hotkey! {
|
|
65
|
+
var isDown = false
|
|
66
|
+
switch type {
|
|
67
|
+
case .keyDown: isDown = true
|
|
68
|
+
case .keyUp: isDown = false
|
|
69
|
+
case .flagsChanged:
|
|
70
|
+
let f = event.flags
|
|
71
|
+
isDown = f.contains(.maskAlternate) || f.contains(.maskCommand) || f.contains(.maskControl) || f.contains(.maskShift) || f.contains(.maskSecondaryFn)
|
|
72
|
+
default: return Unmanaged.passUnretained(event)
|
|
73
|
+
}
|
|
74
|
+
if isDown != hotkeyDown { hotkeyDown = isDown; note(isDown ? "hotkey down" : "hotkey up") }
|
|
75
|
+
}
|
|
76
|
+
return Unmanaged.passUnretained(event)
|
|
77
|
+
}
|
|
78
|
+
guard let tap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .listenOnly, eventsOfInterest: mask, callback: callback, userInfo: nil) else {
|
|
79
|
+
note("error: cannot create event tap — grant Input Monitoring to your terminal (System Settings → Privacy & Security)")
|
|
80
|
+
exit(3)
|
|
81
|
+
}
|
|
82
|
+
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
83
|
+
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
|
|
84
|
+
CGEvent.tapEnable(tap: tap, enable: true)
|
|
85
|
+
note("hotkey \(key) armed")
|
|
86
|
+
}
|
|
87
|
+
// stdin EOF (parent gone) ends the helper
|
|
88
|
+
DispatchQueue.global().async {
|
|
89
|
+
_ = FileHandle.standardInput.readDataToEndOfFile()
|
|
90
|
+
exit(0)
|
|
91
|
+
}
|
|
92
|
+
CFRunLoopRun()
|
package/src/native-pack.ts
CHANGED
|
@@ -17,23 +17,24 @@ import { dirname, join } from "node:path";
|
|
|
17
17
|
import { downloadOne } from "./download";
|
|
18
18
|
import { runtimeValue } from "./runtime-config";
|
|
19
19
|
|
|
20
|
-
export const NATIVE_PACK_VERSION = "0.
|
|
20
|
+
export const NATIVE_PACK_VERSION = "0.5.0";
|
|
21
21
|
export const MLX_CORE_VERSION = "0.32.2";
|
|
22
22
|
export const NATIVE_PACK_FILES = [
|
|
23
23
|
"libmlxc.dylib", "libmlx.dylib", "libjaccl.dylib", "mlx.metallib",
|
|
24
|
-
"libmlx_bun_expert_io.dylib", "mlx-bun-frame-extract",
|
|
24
|
+
"libmlx_bun_expert_io.dylib", "mlx-bun-frame-extract", "mlx-bun-mic-capture",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
27
|
// RELEASE-TIME BAKE: scripts/build-native-pack.sh prints the sha256/size for
|
|
28
28
|
// the freshly built tarball — publish `native-v0.4.0` with both assets and
|
|
29
|
-
// paste the values here BEFORE tagging the package release (the
|
|
29
|
+
// paste the values here BEFORE tagging the package release (0.5.0 adds the
|
|
30
|
+
// mlx-bun-mic-capture dictation sidecar) (the download
|
|
30
31
|
// 404s until the GitHub release exists; dev trees resolve via homebrew/env
|
|
31
32
|
// and never hit this path).
|
|
32
33
|
const SHA256: Record<string, string> = {
|
|
33
|
-
arm64: "
|
|
34
|
+
arm64: "6ff825499ca48bd4e9adebeefac78108cc4328971011c233210d65a0aa78b427",
|
|
34
35
|
};
|
|
35
36
|
const SIZE: Record<string, number> = {
|
|
36
|
-
arm64:
|
|
37
|
+
arm64: 41945454,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export function nativePackName(arch = process.arch): string {
|
package/src/pi-web.ts
CHANGED
|
@@ -1014,6 +1014,7 @@ class PiWebSession {
|
|
|
1014
1014
|
private readonly opts: {
|
|
1015
1015
|
port: number | (() => number); modelId: string; contextWindow: number; readOnly: boolean;
|
|
1016
1016
|
vision: boolean; audio: boolean; thinking: boolean; genDefaults: ReadyGenDefaults;
|
|
1017
|
+
transcription: () => Promise<boolean>;
|
|
1017
1018
|
},
|
|
1018
1019
|
) {}
|
|
1019
1020
|
|
|
@@ -1046,9 +1047,11 @@ class PiWebSession {
|
|
|
1046
1047
|
await this.replaceRuntime(SessionManager.create(this.cwd, this.sessionDir));
|
|
1047
1048
|
if (this.disposed) return;
|
|
1048
1049
|
|
|
1050
|
+
let transcription = false;
|
|
1051
|
+
try { transcription = await this.opts.transcription(); } catch { /* no whisper → no mic button */ }
|
|
1049
1052
|
this.send({
|
|
1050
1053
|
type: "ready", model: this.opts.modelId, vision: this.opts.vision, audio: this.opts.audio,
|
|
1051
|
-
thinking: this.opts.thinking, genDefaults: this.opts.genDefaults,
|
|
1054
|
+
thinking: this.opts.thinking, genDefaults: this.opts.genDefaults, transcription,
|
|
1052
1055
|
});
|
|
1053
1056
|
this.sendCodingToolsState();
|
|
1054
1057
|
this.sendToolApprovals();
|
|
@@ -1854,6 +1857,10 @@ export function makePiWsHandler(opts: {
|
|
|
1854
1857
|
* overrides), sent once on the `ready` frame. Default: all-null (the
|
|
1855
1858
|
* sampling popover falls back to its own hardcoded shape). */
|
|
1856
1859
|
genDefaults?: ReadyGenDefaults;
|
|
1860
|
+
/** Whether /v1/audio/sessions can serve this browser (a Whisper checkpoint
|
|
1861
|
+
* is configured or resolvable). Async: the default checkpoint resolves
|
|
1862
|
+
* lazily through the registry. */
|
|
1863
|
+
transcription?: () => Promise<boolean>;
|
|
1857
1864
|
}): WebSocketHandler<PiWsData> {
|
|
1858
1865
|
const resolved = {
|
|
1859
1866
|
port: opts.port,
|
|
@@ -1864,6 +1871,7 @@ export function makePiWsHandler(opts: {
|
|
|
1864
1871
|
audio: opts.audio ?? false,
|
|
1865
1872
|
thinking: opts.thinking ?? false,
|
|
1866
1873
|
genDefaults: opts.genDefaults ?? { temperature: null, topP: null, topK: null },
|
|
1874
|
+
transcription: opts.transcription ?? (async () => false),
|
|
1867
1875
|
};
|
|
1868
1876
|
|
|
1869
1877
|
return {
|