@mlx-node/asr 0.0.9

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 ADDED
@@ -0,0 +1,144 @@
1
+ # @mlx-node/asr
2
+
3
+ Local Qwen3-ASR transcription and realtime meeting capture on Apple Silicon.
4
+
5
+ ## Convert the Hugging Face checkpoint
6
+
7
+ For the fastest tested decoder path, pack the Qwen text model as MXFP4. The
8
+ audio encoder and multimodal projector deliberately remain BF16 so speech
9
+ features are not quantized:
10
+
11
+ ```bash
12
+ yarn mlx convert \
13
+ -i .cache/models/qwen3-asr-1.7b-hf \
14
+ -o .cache/models/qwen3-asr-1.7b-mlx-mxfp4 \
15
+ -d bfloat16 \
16
+ -q --q-mode mxfp4
17
+ ```
18
+
19
+ Use a dense conversion when weight fidelity or quantization comparisons matter
20
+ more than decode throughput:
21
+
22
+ ```bash
23
+ yarn mlx convert \
24
+ -i .cache/models/qwen3-asr-1.7b-hf \
25
+ -o .cache/models/qwen3-asr-1.7b-mlx \
26
+ -d bfloat16
27
+ ```
28
+
29
+ The converter detects `model_type: "qwen3_asr"`, canonicalizes the checkpoint
30
+ keys, and converts the three audio convolutions to MLX layout. Packed
31
+ conversions support uniform affine, MXFP4, and MXFP8 text weights; recipe-based
32
+ or per-layer quantization is rejected.
33
+
34
+ ## Offline transcription
35
+
36
+ ```typescript
37
+ import { Qwen3AsrModel } from '@mlx-node/asr';
38
+
39
+ const model = await Qwen3AsrModel.load('.cache/models/qwen3-asr-1.7b-mlx-mxfp4');
40
+ const pcm = new Float32Array(/* mono PCM samples */);
41
+ const result = await model.transcribe(pcm, {
42
+ sampleRate: 16_000,
43
+ language: 'en', // omit for language detection
44
+ });
45
+
46
+ console.log(result.text, result.realTimeFactor);
47
+ ```
48
+
49
+ `transcribe()` accepts mono floating-point PCM at any positive sample rate and
50
+ resamples it to the model's native 16 kHz input.
51
+
52
+ ## Streaming manually supplied audio
53
+
54
+ ```typescript
55
+ const stream = await model.createStream({
56
+ sampleRate: 48_000,
57
+ chunkSeconds: 2,
58
+ provisionalTokens: 5,
59
+ unfixedChunks: 2,
60
+ maxTokens: 32,
61
+ });
62
+
63
+ for await (const pcmChunk of yourAudioSource) {
64
+ const revision = await stream.feed(pcmChunk);
65
+ if (revision) {
66
+ process.stdout.write(`\r${revision.stableText}\x1b[2m${revision.provisionalText}\x1b[0m`);
67
+ }
68
+ }
69
+
70
+ const final = await stream.finish();
71
+ console.log(`\n${final.text}`);
72
+ ```
73
+
74
+ Streaming follows Qwen's official rolling policy: every 2 seconds it feeds the
75
+ previous raw transcript minus the last 5 tokens back to the model. The first 2
76
+ chunks are decoded without transcript conditioning. `stableText` is the
77
+ current fixed frontier; render each result as a complete revision because a
78
+ later hypothesis can still move that frontier. The next revision may replace
79
+ `provisionalText`.
80
+
81
+ The audio tower itself is not causal. To keep long meetings realtime, completed
82
+ 8-second local-attention windows are encoded once and cached. The stream keeps
83
+ the latest four completed windows plus the current partial window (less than 40
84
+ seconds total), bounds the decoder's transcript prefix to 150 tokens, and
85
+ reuses KV state through the longest unchanged audio prefix. These bounds keep
86
+ memory and per-revision work approximately constant. If `reachedMaxTokens` is
87
+ true, the continuation exhausted its 32-token budget and the current
88
+ provisional suffix is worth flagging for review. Repeated-token and stalled
89
+ decode guards automatically discard a degenerate provisional tail and
90
+ re-anchor the next chunk with fresh bounded audio context.
91
+
92
+ ## Realtime meeting capture
93
+
94
+ `startMeetingTranscription()` captures the local microphone and the Mac's
95
+ system/output audio by default. Revisions are kept on separate source-tagged
96
+ tracks because the two devices have independent clocks and represent different
97
+ speakers.
98
+
99
+ ```typescript
100
+ import { Qwen3AsrModel, qwen3AsrAudioDevices, startMeetingTranscription } from '@mlx-node/asr';
101
+
102
+ console.table(qwen3AsrAudioDevices());
103
+
104
+ const model = await Qwen3AsrModel.load('.cache/models/qwen3-asr-1.7b-mlx-mxfp4');
105
+ const meeting = await startMeetingTranscription(model, {
106
+ stream: { chunkSeconds: 2, provisionalTokens: 5, unfixedChunks: 2, maxTokens: 32 },
107
+ microphone: { feedMilliseconds: 100, ringSeconds: 10 },
108
+ systemAudio: {
109
+ feedMilliseconds: 100,
110
+ ringSeconds: 10,
111
+ // Optional: capture selected apps instead of all system output.
112
+ // applicationBundleIds: ['us.zoom.xos', 'com.microsoft.teams2'],
113
+ },
114
+ onResult({ source, result }) {
115
+ console.log(`[${source}] ${result.stableText}${result.provisionalText}`);
116
+ },
117
+ onError({ source, error }) {
118
+ console.error(`[${source}]`, error);
119
+ },
120
+ });
121
+
122
+ process.once('SIGINT', async () => {
123
+ const final = await meeting.stop();
124
+ console.log('local:', final.microphone?.result.text);
125
+ console.log('remote:', final.systemAudio?.result.text);
126
+ });
127
+ ```
128
+
129
+ Set either `microphone: false` or `systemAudio: false` for a single-track
130
+ session. The lower-level `startRealtimeTranscription()` API remains available
131
+ when you want to own exactly one source; its `capture.source` defaults to
132
+ `Qwen3AsrCaptureSource.Microphone`.
133
+
134
+ The native Core Audio callbacks only write packed mono float samples into
135
+ bounded single-producer/single-consumer rings. Resampling and MLX inference run
136
+ outside the realtime callbacks. Each capture automatically binds its ASR stream
137
+ to the selected device's actual sample rate. `feedMilliseconds` controls how
138
+ often capture drains into the model buffer; `chunkSeconds` controls the
139
+ transcription update cadence.
140
+
141
+ Packaged macOS hosts must include both `NSMicrophoneUsageDescription` and
142
+ `NSAudioCaptureUsageDescription` in `Info.plist`. macOS prompts separately for
143
+ microphone and system-audio permission. System capture uses a private Core
144
+ Audio tap and does not mute normal speaker playback.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Qwen3-ASR inference and low-latency Core Audio capture on Apple Silicon.
3
+ */
4
+ import { Qwen3AsrCapture, Qwen3AsrCaptureSource, Qwen3AsrModel, Qwen3AsrStream, qwen3AsrAudioDevices, qwen3AsrInputDevices, type Qwen3AsrAudioDevice, type Qwen3AsrCaptureOptions, type Qwen3AsrCaptureStats, type Qwen3AsrInputDevice, type Qwen3AsrResult, type Qwen3AsrStreamOptions, type Qwen3AsrTranscribeOptions } from '@mlx-node/core';
5
+ export { Qwen3AsrCapture, Qwen3AsrCaptureSource, Qwen3AsrModel, Qwen3AsrStream, qwen3AsrAudioDevices, qwen3AsrInputDevices, type Qwen3AsrAudioDevice, type Qwen3AsrCaptureOptions, type Qwen3AsrCaptureStats, type Qwen3AsrInputDevice, type Qwen3AsrResult, type Qwen3AsrStreamOptions, type Qwen3AsrTranscribeOptions, };
6
+ export interface Qwen3AsrRealtimeOptions {
7
+ /** Rolling decode cadence, language, and prompting options. */
8
+ stream?: Qwen3AsrStreamOptions;
9
+ /** Core Audio source, device, application filter, and callback-ring options. */
10
+ capture?: Qwen3AsrCaptureOptions;
11
+ /** Called for every rolling transcription revision. */
12
+ onResult: (result: Qwen3AsrResult) => void;
13
+ /** Called for asynchronous capture or model-worker errors. */
14
+ onError?: (error: Error) => void;
15
+ }
16
+ export interface Qwen3AsrRealtimeFinal {
17
+ result: Qwen3AsrResult;
18
+ capture: Qwen3AsrCaptureStats;
19
+ }
20
+ /**
21
+ * Owns one model stream and one Core Audio source. Call `stop()` to drain the
22
+ * lock-free capture ring and receive the final, non-provisional transcript.
23
+ */
24
+ export declare class Qwen3AsrRealtimeSession {
25
+ #private;
26
+ readonly stream: Qwen3AsrStream;
27
+ readonly capture: Qwen3AsrCapture;
28
+ private readonly getLastError;
29
+ private constructor();
30
+ static start(model: Qwen3AsrModel, options: Qwen3AsrRealtimeOptions): Promise<Qwen3AsrRealtimeSession>;
31
+ get deviceName(): string;
32
+ get source(): Qwen3AsrCaptureSource;
33
+ get sampleRate(): number;
34
+ get lastError(): Error | undefined;
35
+ pause(): void;
36
+ resume(): void;
37
+ stop(): Promise<Qwen3AsrRealtimeFinal>;
38
+ }
39
+ export declare function startRealtimeTranscription(model: Qwen3AsrModel, options: Qwen3AsrRealtimeOptions): Promise<Qwen3AsrRealtimeSession>;
40
+ type CaptureTimingOptions = Pick<Qwen3AsrCaptureOptions, 'feedMilliseconds' | 'ringSeconds'>;
41
+ export interface Qwen3AsrMicrophoneOptions extends CaptureTimingOptions {
42
+ /** Stable input-device UID from `qwen3AsrAudioDevices()`. */
43
+ deviceId?: string;
44
+ /** Input-device name. Prefer `deviceId` when persisting a selection. */
45
+ deviceName?: string;
46
+ }
47
+ export interface Qwen3AsrSystemAudioOptions extends CaptureTimingOptions {
48
+ /** Stable output-device UID from `qwen3AsrAudioDevices()`. */
49
+ deviceId?: string;
50
+ /** Output-device name. Prefer `deviceId` when persisting a selection. */
51
+ deviceName?: string;
52
+ /** Capture only these applications. Omit to capture all system output. */
53
+ applicationBundleIds?: string[];
54
+ }
55
+ export type Qwen3AsrMeetingSource = 'microphone' | 'systemAudio';
56
+ export interface Qwen3AsrMeetingResultEvent {
57
+ source: Qwen3AsrMeetingSource;
58
+ result: Qwen3AsrResult;
59
+ }
60
+ export interface Qwen3AsrMeetingErrorEvent {
61
+ source: Qwen3AsrMeetingSource;
62
+ error: Error;
63
+ }
64
+ export interface Qwen3AsrMeetingOptions {
65
+ /** Shared rolling decode cadence, language, and prompting options. */
66
+ stream?: Qwen3AsrStreamOptions;
67
+ /** Microphone capture options. `false` disables this track. Default enabled. */
68
+ microphone?: false | Qwen3AsrMicrophoneOptions;
69
+ /** System/output audio options. `false` disables this track. Default enabled. */
70
+ systemAudio?: false | Qwen3AsrSystemAudioOptions;
71
+ /** Called for every rolling revision, tagged with its audio source. */
72
+ onResult: (event: Qwen3AsrMeetingResultEvent) => void;
73
+ /** Called for asynchronous capture or model-worker errors. */
74
+ onError?: (event: Qwen3AsrMeetingErrorEvent) => void;
75
+ }
76
+ export interface Qwen3AsrMeetingFinal {
77
+ microphone?: Qwen3AsrRealtimeFinal;
78
+ systemAudio?: Qwen3AsrRealtimeFinal;
79
+ }
80
+ /**
81
+ * A meeting owns one independently clocked transcription track per enabled
82
+ * source. Results stay source-tagged so speaker-mic audio and remote/system
83
+ * audio are never silently mixed or ordered by unrelated device clocks.
84
+ */
85
+ export declare class Qwen3AsrMeetingSession {
86
+ #private;
87
+ readonly microphone?: Qwen3AsrRealtimeSession;
88
+ readonly systemAudio?: Qwen3AsrRealtimeSession;
89
+ private constructor();
90
+ static start(model: Qwen3AsrModel, options: Qwen3AsrMeetingOptions): Promise<Qwen3AsrMeetingSession>;
91
+ pause(): void;
92
+ resume(): void;
93
+ stop(): Promise<Qwen3AsrMeetingFinal>;
94
+ private forEachTrack;
95
+ }
96
+ /** Start microphone and system-audio transcription as one meeting session. */
97
+ export declare function startMeetingTranscription(model: Qwen3AsrModel, options: Qwen3AsrMeetingOptions): Promise<Qwen3AsrMeetingSession>;
98
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Qwen3-ASR inference and low-latency Core Audio capture on Apple Silicon.
3
+ */
4
+ import { Qwen3AsrCapture, Qwen3AsrModel, Qwen3AsrStream, qwen3AsrAudioDevices, qwen3AsrInputDevices, } from '@mlx-node/core';
5
+ export { Qwen3AsrCapture, Qwen3AsrModel, Qwen3AsrStream, qwen3AsrAudioDevices, qwen3AsrInputDevices, };
6
+ /**
7
+ * Owns one model stream and one Core Audio source. Call `stop()` to drain the
8
+ * lock-free capture ring and receive the final, non-provisional transcript.
9
+ */
10
+ export class Qwen3AsrRealtimeSession {
11
+ stream;
12
+ capture;
13
+ #stopPromise;
14
+ getLastError;
15
+ constructor(stream, capture, getLastError) {
16
+ this.stream = stream;
17
+ this.capture = capture;
18
+ this.getLastError = getLastError;
19
+ }
20
+ static async start(model, options) {
21
+ const stream = await model.createStream(options.stream);
22
+ let lastError;
23
+ let capture;
24
+ try {
25
+ capture = stream.startCapture(options.capture, (error, result) => {
26
+ if (error) {
27
+ lastError = error;
28
+ options.onError?.(error);
29
+ return;
30
+ }
31
+ options.onResult(result);
32
+ });
33
+ }
34
+ catch (error) {
35
+ await stream.finish().catch(() => undefined);
36
+ throw error;
37
+ }
38
+ return new Qwen3AsrRealtimeSession(stream, capture, () => lastError);
39
+ }
40
+ get deviceName() {
41
+ return this.capture.deviceName;
42
+ }
43
+ get source() {
44
+ return this.capture.source;
45
+ }
46
+ get sampleRate() {
47
+ return this.capture.sampleRate;
48
+ }
49
+ get lastError() {
50
+ return this.getLastError();
51
+ }
52
+ pause() {
53
+ this.capture.pause();
54
+ }
55
+ resume() {
56
+ this.capture.resume();
57
+ }
58
+ stop() {
59
+ this.#stopPromise ??= (async () => {
60
+ let capture;
61
+ try {
62
+ capture = await this.capture.stop();
63
+ }
64
+ catch (error) {
65
+ await this.stream.finish().catch(() => undefined);
66
+ throw error;
67
+ }
68
+ const result = await this.stream.finish();
69
+ const error = this.lastError;
70
+ if (error)
71
+ throw error;
72
+ return { result, capture };
73
+ })();
74
+ return this.#stopPromise;
75
+ }
76
+ }
77
+ export function startRealtimeTranscription(model, options) {
78
+ return Qwen3AsrRealtimeSession.start(model, options);
79
+ }
80
+ /**
81
+ * A meeting owns one independently clocked transcription track per enabled
82
+ * source. Results stay source-tagged so speaker-mic audio and remote/system
83
+ * audio are never silently mixed or ordered by unrelated device clocks.
84
+ */
85
+ export class Qwen3AsrMeetingSession {
86
+ microphone;
87
+ systemAudio;
88
+ #stopPromise;
89
+ constructor(tracks) {
90
+ this.microphone = tracks.microphone;
91
+ this.systemAudio = tracks.systemAudio;
92
+ }
93
+ static async start(model, options) {
94
+ if (options.microphone === false && options.systemAudio === false) {
95
+ throw new Error('At least one meeting audio source must be enabled');
96
+ }
97
+ const tracks = {};
98
+ const started = [];
99
+ const startTrack = async (source, capture) => {
100
+ const session = await Qwen3AsrRealtimeSession.start(model, {
101
+ stream: options.stream,
102
+ capture,
103
+ onResult: (result) => options.onResult({ source, result }),
104
+ onError: (error) => options.onError?.({ source, error }),
105
+ });
106
+ started.push(session);
107
+ return session;
108
+ };
109
+ try {
110
+ // Ask for system-audio permission before opening the microphone. This
111
+ // avoids leaving a live mic running while the system permission sheet is
112
+ // waiting for a response.
113
+ if (options.systemAudio !== false) {
114
+ tracks.systemAudio = await startTrack('systemAudio', {
115
+ ...options.systemAudio,
116
+ source: 'systemAudio',
117
+ });
118
+ }
119
+ if (options.microphone !== false) {
120
+ tracks.microphone = await startTrack('microphone', {
121
+ ...options.microphone,
122
+ source: 'microphone',
123
+ });
124
+ }
125
+ }
126
+ catch (error) {
127
+ await Promise.allSettled(started.map((session) => session.stop()));
128
+ throw error;
129
+ }
130
+ return new Qwen3AsrMeetingSession(tracks);
131
+ }
132
+ pause() {
133
+ this.forEachTrack((track) => track.pause());
134
+ }
135
+ resume() {
136
+ this.forEachTrack((track) => track.resume());
137
+ }
138
+ stop() {
139
+ this.#stopPromise ??= (async () => {
140
+ const microphone = this.microphone?.stop();
141
+ const systemAudio = this.systemAudio?.stop();
142
+ const settled = await Promise.allSettled([microphone, systemAudio].filter((promise) => promise !== undefined));
143
+ const failure = settled.find((result) => result.status === 'rejected');
144
+ if (failure)
145
+ throw failure.reason;
146
+ const final = {};
147
+ let index = 0;
148
+ if (microphone)
149
+ final.microphone = settled[index++].value;
150
+ if (systemAudio)
151
+ final.systemAudio = settled[index].value;
152
+ return final;
153
+ })();
154
+ return this.#stopPromise;
155
+ }
156
+ forEachTrack(action) {
157
+ let firstError;
158
+ for (const track of [this.microphone, this.systemAudio]) {
159
+ if (!track)
160
+ continue;
161
+ try {
162
+ action(track);
163
+ }
164
+ catch (error) {
165
+ firstError ??= error;
166
+ }
167
+ }
168
+ if (firstError)
169
+ throw firstError;
170
+ }
171
+ }
172
+ /** Start microphone and system-audio transcription as one meeting session. */
173
+ export function startMeetingTranscription(model, options) {
174
+ return Qwen3AsrMeetingSession.start(model, options);
175
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mlx-node/asr",
3
+ "version": "0.0.9",
4
+ "homepage": "https://github.com/mlx-node/mlx-node",
5
+ "bugs": {
6
+ "url": "https://github.com/mlx-node/mlx-node/issues"
7
+ },
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/mlx-node/mlx-node.git",
12
+ "directory": "packages/asr"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -b"
28
+ },
29
+ "dependencies": {
30
+ "@mlx-node/core": "workspace:*"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^26.4.0"
34
+ }
35
+ }