The menu bar app

A mark that tells you whether it is listening, and a popover with a meter, the last four lines, two numbers, the model and its speed, who is connected and how an app connects, and a strip saying which build this is. Nothing in it that the API cannot do.

Sonocles.app has no window and no Dock icon. It lives in the menu bar and opens as a popover, and it is a thin shell over the same Service the CLI drives and the HTTP API controls. That is deliberate: anything clever added to the popover would be behaviour the measurements never saw and the API cannot reach, so there is nothing clever in it.

The mark

The mark is the state. Dimmed, it is idle; at full strength, it is listening. It is a template image, so it takes the menu bar’s colour in light and dark and the dimming survives as opacity.

The sockets are up either way. Idle means capture is off, not that the app is unreachable — POST /start works while the mark is dim, and that is the point of the split.

The popover

The meter. Twenty cells over the useful range, −60 dBFS to clipping, and a numeric peak beside them. The number is driven straight from the audio thread, independent of the model, which is what makes it useful: a quiet room sits at the bottom of the bar but still moves the number, and that is the proof that the microphone is live when nobody is speaking. –– means no reading at all.

The words. The last four lines of a running transcript, newest at the bottom. The live line — the utterance still being revised — is bright; what has settled sits dim above it, and what no longer fits fades off the top. The window is a fixed height, so an arriving word never shoves the rest of the popover down — at five frames a second that would be a twitch, not an interface. Before the first word it says so: Words appear here as you say them. The transcript is the popover’s own accumulation, the way any consumer’s is; the protocol’s text is the utterance, not the session.

Two numbers. lag is how far behind the live audio the last frame was; every is the gap since the frame before it. An unmeasured value shows as ··, never as 0. Rendering absence as a number is how an earlier build spent a session insisting it was real-time while measuring nothing at all.

Engine. Two levels, in the words of the person choosing. The picker is the model: Parakeet, which runs on this Mac’s Neural Engine and delivers words as you speak, or Apple, Apple’s own recogniser, whose words arrive in bursts about every 4 seconds — offered only on macOS 26, and only because the measurement needs a control. One line under the picker says which of those you have.

Under Parakeet, Speed: three rows, each the one thing it costs and is for. The rows are the control — click one and it is chosen, the accent bar beside it.

speed the row
160 ms Fastest. For prompters and cues.
320 ms Steadier words, a beat later.
1280 ms Most accurate. For captions and transcripts.

Switching restarts capture: the models differ, and a half-swapped pipeline would report numbers belonging to neither. Every choice is POST /engine with a click on it — the picker and the rows map onto the same four engine ids, nothing on the wire changed — and it follows the engine event, so a switch made over HTTP moves it too: Engine selection. Choosing Parakeet after Apple goes back to the speed that was set.

Connected apps. How many apps are on the sockets right now — 2 apps connected, 1 app connected, no apps connected — from /status’s clients, both transports together, refreshed every second. Under it, everything an app needs to connect:

  • Key — the bearer token, masked to its ends, with Show, Copy and Rotate. The key an app needs to connect — Copy it into the app. Rotate makes a new key and disconnects everything. Apps on this Mac can also read it from ~/Library/Application Support/Sonocles/token. Rotate is two clicks: the first arms it for six seconds, the second does it.
  • HTTP API :7357 — where apps ask Sonocles things: the control routes and the /events feed.
  • WebSocket :7358 — the live word stream.

The ports are labels, not switches — the sockets bind at launch and stay up for the life of the app.

Start listening / Stop. The same thing as POST /start and POST /stop. Disabled while the models are being prepared.

Quit. Takes the sockets down with it. There is no daemon behind the app; the app is the process.

The strip. A dark line under everything: sonocles 0.1.3 · Parakeet · 160 ms · 2 apps — the version GET / answers, the model and speed the app is set to, and the connected count. The dot is olive while listening. A screenshot of the popover is a bug report.

Down

If the sockets could not bind — another Sonocles holding 7357, usually — nothing below the header would be true: no port is listening, no POST /start can start anything, there is no key. So the popover says exactly that: the pill reads Down, an Engine · down panel carries the reason, Relaunch tries the sockets again, and the strip reads engine not running. The NativePHP app shows the same panel while its engine is starting, when the engine refuses its key, or when the bundled binary is missing.

The first run

Before the first listen, the popover shows what the engine is doing instead of the meter: Finding model files, then a progress bar for the download, then a pulse for compiling. The bar is real when there is a fraction to report and a pulse when there is not — compiling has no measurable progress, and animating a bar to look busy would be the same lie as reporting an unmeasured latency as zero.

What it costs to leave running

Idle, nothing: two bound ports and a dimmed mark. Listening, the Neural Engine and a microphone tap, and no CPU your encoder is competing for. That is the entire reason the thing was written, and it is what makes it something to leave listening for the length of a take.