mac-voice-mcp 0.3.0 → 0.3.1

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
@@ -5,6 +5,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.3.1] - 2026-09-29
9
+
10
+ ### Fixed
11
+ - **Recording broke on Macs without SoX when Bluetooth headphones were connected** ([#6](https://github.com/jeet0007/mac-voice-mcp/issues/6)). Every turn failed with "pure digital silence".
12
+ - The ffmpeg fallback now records from the input chosen in System Settings (`:default`) instead of device number 0. `VOICE_MCP_FFMPEG_DEVICE` still overrides it.
13
+ - `voice_setup` treats ffmpeg as a fallback, recommends SoX, and installs it with your OK.
14
+ - Silence recorded through ffmpeg now points at the input device and SoX, not only at mic permissions.
15
+ - **`voice_setup` notices tools installed since the last check**, such as `brew install sox` run in a terminal, without restarting the app.
16
+
8
17
  ## [0.3.0] - 2026-09-28
9
18
 
10
19
  ### Added
package/README.md CHANGED
@@ -123,7 +123,7 @@ Setup checks what's already there before it changes anything:
123
123
  | Needed | Provided by | If it's missing |
124
124
  |---|---|---|
125
125
  | Voice | macOS `say`, using the most natural voice installed | Nothing to do. For a far better voice, add a free Premium one (see below). |
126
- | Microphone capture | SoX (`rec`) | `brew install sox` |
126
+ | Microphone capture | SoX (`rec`). ffmpeg works as a fallback, but setup recommends SoX | `brew install sox` |
127
127
  | Speech-to-text | whisper.cpp (`whisper-cli` and `whisper-server`, Metal-accelerated) | `brew install whisper-cpp` |
128
128
  | Speech model | `base.en`, ~140 MB | Downloaded once to `~/.cache/mac-voice-mcp/models/` |
129
129
 
@@ -235,7 +235,8 @@ Everything is optional. Set these in your client config's `"env": { … }` block
235
235
  | `VOICE_MCP_START_TIMEOUT_SECONDS` | `15` | How long to wait for you to start talking. |
236
236
  | `VOICE_MCP_SPEECH_MARGIN_DB` | `12` | How much louder than room noise counts as speech. Raise it in noisy rooms. |
237
237
  | `VOICE_MCP_MIN_SPEECH_DB` | `-48` | The quietest level that ever counts as speech (dBFS). |
238
- | `VOICE_MCP_RECORDER` | `auto` | `sox` or `ffmpeg` (`ffmpeg` is macOS only). |
238
+ | `VOICE_MCP_RECORDER` | `auto` | `sox` or `ffmpeg` (`ffmpeg` is macOS only). `auto` uses SoX, falling back to ffmpeg. |
239
+ | `VOICE_MCP_FFMPEG_DEVICE` | `:default` | Which input ffmpeg records from. `:default` follows System Settings; `:1` picks device 1 (list them with `ffmpeg -f avfoundation -list_devices true -i ""`). |
239
240
 
240
241
  **Speech-to-text**
241
242
 
@@ -272,7 +273,7 @@ Everything is optional. Set these in your client config's `"env": { … }` block
272
273
  | Symptom | Fix |
273
274
  |---|---|
274
275
  | "voice-mcp is not set up yet" | Ask Claude to *set up voice*, or run `npx -y mac-voice-mcp@latest setup`. |
275
- | "microphone returned pure digital silence" | macOS is blocking the mic for the host app. Go to **System Settings → Privacy & Security → Microphone**, enable Claude / Cursor / your terminal, then restart that app. |
276
+ | "microphone returned pure digital silence" | macOS is blocking the mic for the host app. Go to **System Settings → Privacy & Security → Microphone**, enable Claude / Cursor / your terminal, then restart that app. If the message says it's recording with ffmpeg, the input device is the likelier cause: run `brew install sox`. |
276
277
  | No permission prompt ever appears | Run `tccutil reset Microphone <bundle id>` and restart the app. Running `test` in Terminal only gives permission to Terminal, not to Claude Desktop. |
277
278
  | It cuts me off while I'm thinking | Set `VOICE_MCP_END_SILENCE_MS=1800` (or up to `2500`). |
278
279
  | It never stops listening | The room is too noisy for the defaults. Set `VOICE_MCP_SPEECH_MARGIN_DB=18`, or use a headset. |
package/dist/config.js CHANGED
@@ -58,8 +58,8 @@ export const CONFIG = {
58
58
  minSpeechDb: envNum("VOICE_MCP_MIN_SPEECH_DB", -48),
59
59
  /** Recorder: auto (SoX, else ffmpeg) | sox | ffmpeg. */
60
60
  recorder: (process.env.VOICE_MCP_RECORDER?.trim().toLowerCase() || "auto"),
61
- /** ffmpeg avfoundation audio input (macOS fallback recorder). */
62
- ffmpegDevice: process.env.VOICE_MCP_FFMPEG_DEVICE?.trim() || ":0",
61
+ /** ffmpeg avfoundation audio input (macOS fallback recorder). ":default" follows the input chosen in System Settings. */
62
+ ffmpegDevice: process.env.VOICE_MCP_FFMPEG_DEVICE?.trim() || ":default",
63
63
  // --- Speech-to-text
64
64
  /** whisper.cpp model name (tiny.en, base.en, small.en, large-v3-turbo-q5_0, ...). */
65
65
  modelName,
package/dist/setup.js CHANGED
@@ -76,9 +76,21 @@ async function checkRequirements() {
76
76
  : { label: "Voice", status: "ok", detail: choice.label });
77
77
  }
78
78
  const rec = findRecorder();
79
- checks.push(rec
80
- ? { label: "Recorder", status: "ok", detail: describeRecorder(rec) }
81
- : { label: "Recorder", status: brewing ? "installing" : "missing", detail: "SoX is not installed", brew: IS_WIN ? undefined : "sox" });
79
+ if (!rec) {
80
+ checks.push({ label: "Recorder", status: brewing ? "installing" : "missing", detail: "SoX is not installed", brew: IS_WIN ? undefined : "sox" });
81
+ }
82
+ else if (rec.kind === "ffmpeg" && CONFIG.recorder === "auto") {
83
+ // Works, but it's the fragile path: recommend SoX, which is what the turn-taking is tuned on.
84
+ checks.push({
85
+ label: "Recorder",
86
+ status: brewing ? "installing" : "optional",
87
+ detail: `${describeRecorder(rec)}, as a fallback. SoX is recommended: it's more reliable with Bluetooth headsets and other audio devices`,
88
+ brew: "sox",
89
+ });
90
+ }
91
+ else {
92
+ checks.push({ label: "Recorder", status: "ok", detail: describeRecorder(rec) });
93
+ }
82
94
  const cli = findWhisperCli();
83
95
  const server = findWhisperServer();
84
96
  if (cli || server) {
@@ -131,9 +143,11 @@ async function checkRequirements() {
131
143
  * @param onProgress optional heartbeat while waiting (used for MCP progress notifications).
132
144
  */
133
145
  export async function runSetupFlow(install, onProgress) {
146
+ resetWhichCache(); // see anything installed since the last check (e.g. `brew install sox` in a terminal)
134
147
  let checks = await checkRequirements();
135
148
  if (install) {
136
- const formulae = [...new Set(checks.filter((c) => c.status === "missing" && c.brew).map((c) => c.brew))];
149
+ // Missing pieces, plus recommended ones (SoX when only ffmpeg is there): the user agreed to install.
150
+ const formulae = [...new Set(checks.filter((c) => (c.status === "missing" || c.status === "optional") && c.brew).map((c) => c.brew))];
137
151
  const brew = findBrew();
138
152
  if (formulae.length && brew && !running(brewJob))
139
153
  brewJob = startBrewInstall(brew, formulae);
@@ -157,7 +171,7 @@ export async function runSetupFlow(install, onProgress) {
157
171
  const lines = [
158
172
  `voice-mcp setup — ${ready ? "READY" : installing ? "INSTALLING" : "NOT READY"}`,
159
173
  "",
160
- ...checks.map((c) => `${icon[c.status]} ${c.label}: ${c.detail}${c.brew && c.status === "missing" ? ` → brew install ${c.brew}` : ""}`),
174
+ ...checks.map((c) => `${icon[c.status]} ${c.label}: ${c.detail}${c.brew && c.status !== "ok" && c.status !== "installing" ? ` → brew install ${c.brew}` : ""}`),
161
175
  IS_MAC ? "• Microphone: macOS asks for permission the first time speak_and_listen listens — click Allow." : "",
162
176
  ];
163
177
  if (done.length)
@@ -169,6 +183,11 @@ export async function runSetupFlow(install, onProgress) {
169
183
  lines.push("");
170
184
  if (ready) {
171
185
  lines.push("Next: everything is in place — speak_and_listen is ready to use.");
186
+ const recommended = [...new Set(checks.filter((c) => c.status === "optional" && c.brew).map((c) => c.brew))];
187
+ if (recommended.length && !install && findBrew()) {
188
+ lines.push(`Recommended: ask the user whether to brew install ${recommended.join(" ")} (see • above). ` +
189
+ "Only if they agree, call voice_setup with install=true.");
190
+ }
172
191
  if (checks.some((c) => c.label === "Voice" && c.status === "optional")) {
173
192
  lines.push("Mention the optional voice tip (•) to the user once — it makes the voice sound far more natural. Nothing else to do.");
174
193
  }
package/dist/voice.js CHANGED
@@ -6,7 +6,7 @@ import { chime, findRecorder, listenForTurn, MIC_PERMISSION_HINT, RECORDER_MISSI
6
6
  import { CONFIG, debug, DEFAULT_LISTEN_SECONDS, MAX_LISTEN_SECONDS, MAX_SPEAK_CHARS } from "./config.js";
7
7
  import { acquireMicLock, MicBusyError } from "./lock.js";
8
8
  import { ensureModel } from "./model.js";
9
- import { SetupError } from "./proc.js";
9
+ import { resetWhichCache, SetupError } from "./proc.js";
10
10
  import { prepareSpeech } from "./speech-text.js";
11
11
  import { findWhisperCli, findWhisperServer, prewarm, STT_MISSING, transcribe } from "./stt.js";
12
12
  /** Everything speak_and_listen needs, checked before saying a word. Never installs anything. */
@@ -66,9 +66,16 @@ async function turn(textToSpeak, seconds, model, signal, onPhase) {
66
66
  debug("listen:", heard);
67
67
  const timing = (transcribedMs) => timingNote({ spokeMs: spoke, listenedMs: t1 - tListen, talkedSeconds: heard.speechSeconds, transcribedMs });
68
68
  if (heard.digitalSilence) {
69
+ resetWhichCache(); // if the user fixes it by installing SoX, the next turn picks it up
70
+ const viaFfmpeg = findRecorder()?.kind === "ffmpeg";
69
71
  return {
70
72
  ok: false,
71
- text: `The microphone returned pure digital silence, which usually means microphone access is blocked. ${MIC_PERMISSION_HINT}`,
73
+ text: viaFfmpeg
74
+ ? "The microphone returned pure digital silence. voice-mcp is recording with ffmpeg because SoX isn't installed, " +
75
+ "and ffmpeg may be using a silent or wrong input device (for example after connecting Bluetooth headphones). " +
76
+ "Suggest `brew install sox` (or call voice_setup), then try again. If that doesn't help: " +
77
+ MIC_PERMISSION_HINT
78
+ : `The microphone returned pure digital silence, which usually means microphone access is blocked. ${MIC_PERMISSION_HINT}`,
72
79
  notes,
73
80
  };
74
81
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mac-voice-mcp",
3
3
  "mcpName": "io.github.jeet0007/mac-voice-mcp",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "Talk with Claude out loud on your Mac: speaks with macOS `say`, listens for one natural conversational turn, and transcribes on-device with whisper.cpp. An MCP server.",
6
6
  "type": "module",
7
7
  "bin": {