Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6155795
feat: config-only post-transcribe LLM rewrite before paste
sumerc Oct 22, 2026
956171c
feat(posttranscribe): keep mixed-language words; save rewrite per rec…
sumerc Oct 22, 2026
8196f3a
feat(posttranscribe): fix vocabulary terms heard as ordinary words
sumerc Oct 22, 2026
f315c1b
feat(posttranscribe): tray picker, overlay tag, sticky colours; keep …
sumerc Oct 22, 2026
e7853ac
feat(posttranscribe): minimal generic prompt, measured on real dictation
sumerc Oct 22, 2026
9249a9d
docs: drop design-notes; fix same-millisecond recording folders
sumerc Oct 23, 2026
9a89885
fix(log): escape line breaks and tabs in the logged rewrite
sumerc Oct 23, 2026
197ed96
fix(posttranscribe): keep code, paths, URLs, commands and numbers ver…
sumerc Oct 23, 2026
13c4a21
fix(posttranscribe): ask for a bare reply and strip an echoed <transc…
sumerc Oct 23, 2026
8e22fa2
fix(posttranscribe): cut a long HTTP error body before it reaches the…
sumerc Oct 23, 2026
a9cbe81
fix: give a post-transcribe profile its tag colour on first use
sumerc Oct 23, 2026
5648807
docs: make the example user profile fit the base rules
sumerc Oct 23, 2026
639a749
fix(posttranscribe): send one warm-up per recording, not two on the f…
sumerc Oct 23, 2026
3a560b0
fix: paste a held-back stream's text after a session error or auto-close
sumerc Oct 23, 2026
758a705
feat(posttranscribe): clean profile, tray tooltips, neon tag; fix rev…
sumerc Oct 23, 2026
7cef516
docs: re-flow two comments that lost their design-notes pointer
sumerc Oct 23, 2026
6302a4d
posttranscribe: cut the rewrite timeout to 5 s
sumerc Oct 23, 2026
6f1aed7
log: escape the raw transcript in transcribe_log.txt too
sumerc Oct 23, 2026
c9ca911
post-transcribe: an unknown profile name acts as None
sumerc Oct 23, 2026
29bea15
main: set the profile and its first colour in one config write
sumerc Oct 23, 2026
7f0373a
docs: note the rewrite's 5 s budget for post_transcribe.model; fix a …
sumerc Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Rules
- **CHANGELOG.md** — do not touch it. It is written only at release time, by hand. Never add an entry for a fix, a feature, or anything else during normal work.
- **docs/design-notes.md** — the *why* behind non-obvious choices, and the record of what was measured (including options tried and rejected). Read it before revisiting an engine/model/backend decision; a surprising line of code usually has its reason there. Add an entry when a decision rests on a measurement or a rejected alternative — facts, not narrative. **Facts are updated; decisions are superseded.** A statement about the world (what an API offers, what a model file contains) that turns out to be wrong or has changed gets edited in place — never leave a knowingly-false sentence standing for the record, since the next reader cannot tell it from a live one. A *decision* overturned by later evidence gets marked superseded rather than deleted; the code often still reflects why it *used* to be the right call, and a rejected option must stay rejected on the record. **Every claim carries a timestamp** — the date checked plus the version/commit checked against; no open-ended market claims ("nobody beats X", "no model has Y").
- **No performance numbers in code comments** — they are machine-specific and go stale silently. Say *what* is slow and *why* ("-play blocks while it starts the audio hardware"), and put the measurement in `docs/design-notes.md`, where the hardware is on the record. If a number really must appear in a comment, name the machine with it (e.g. "~47 ms on an M5 Pro").
- **No performance numbers in code comments** — they are machine-specific and go stale silently. Say *what* is slow and *why* ("-play blocks while it starts the audio hardware"). If a number really must appear in a comment, name the machine with it (e.g. "~47 ms on an M5 Pro").
Comment thread
sumerc marked this conversation as resolved.
Resolved
- **Clean package interface** — every package must expose a single, platform-neutral interface describing *what it provides*, defined once (typically in `<pkg>.go`). Public API, shared types, and guard logic live there; platform/provider files (build-tag variants) only implement the backend hooks. Never duplicate the public API across build-tag files (see `audio/`: `audio.go` owns the capture interface plus `PlayStart/PlayEnd/...`, platform files provide only the backends — `initSound`/`playOne` for playback, the malgo/pulse capture impls).

## Build & Run
Expand Down Expand Up @@ -107,6 +106,7 @@ Option+Space keydown → record audio → encode (mode-based) → API call → c
- `vad.go` - voice activity detection using WebRTC VAD with debounced speech confirmation
- `silence.go` - silence monitoring with warnings, repeat beeps, and auto-close (toggle mode)
- `config/` - persistent settings (`config.json`) and vocabulary hints (`hints.txt`)
- `posttranscribe/` - optional LLM rewrite of the transcript before paste (Groq chat completions); built-in profiles + user prompts from `config.json`'s `post_transcribe` block
- `log.go` - diagnostic logging and panic capture to `diagnostics_log.txt`

## Design Philosophy
Expand All @@ -128,7 +128,7 @@ Option+Space keydown → record audio → encode (mode-based) → API call → c
- Override with `ZEE_LOG_PATH` env var or `-logpath <path>` flag (supports relative paths, use `./` for current directory)
- `crash_log.txt` - panic recovery (always enabled)
- `diagnostics_log.txt` - timing metrics, errors, warnings (always enabled, rotated at 10 MB)
- `transcribe_log.txt` - transcription text history, each line with its `recordings/<timestamp>/` folder (`audio.<ext>`, `transcript.txt`, `info.json`); requires `-debug-transcribe`, never pruned
- `transcribe_log.txt` - transcription text history, each line with its `recordings/<timestamp>/` folder (`audio.<ext>`, `transcript.txt`, `info.json`, plus `post_transcript.txt` when post-transcribe rewrote it); requires `-debug-transcribe`, never pruned

## Releasing

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ GGML_PREFIX := $(CURDIR)/$(PARAKEET_DIR)/build-release/ggml-prefix
# zee pins it here instead of taking parakeet.cpp's own pin, so a ggml bump does
# not wait for a parakeet.cpp release. parakeet-lib moves the nested checkout to
# this commit before configuring. v0.26.0: ~9% faster whisper on M5 (tensor-core
# flash attention, few-row MMA mat-mul); see design-notes "ggml 0.26".
# flash attention, few-row MMA mat-mul).
GGML_DIR := third_party/parakeet.cpp/third_party/ggml
GGML_BASE := d7cb574130e6f01ad25b3289685489200febcd74
# In-tree patches to parakeet.cpp, applied as one set like the whisper ones.
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ what installs it.

- [**Reference**](docs/reference.md) — flags, environment variables, config and log
file layout, CLI install, building from source, testing and benchmarks.
- [**Design notes**](docs/design-notes.md) — *why* the non-obvious choices were
made: which engine and model, which backend, what got measured, and which
alternatives were tried and rejected. Worth reading before changing anything in
`audio/`, `transcriber/`, or the local model registry.

## About

Expand Down
2 changes: 1 addition & 1 deletion audio/beep_darwin.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ void zeeBeepLoad(int idx, const void *wav, int len) {
// the audio hardware — prepareToPlay does not prevent it, and the hardware powers
// back down between beeps — and that landed on the push-to-talk path twice per
// dictation, at press and at release. The user hears the tone at the same moment;
// only the caller's wait is gone. Measured cost in docs/design-notes.md.
// only the caller's wait is gone.
void zeeBeepPlay(int idx) {
AVAudioPlayer *p = _players[idx];
if (p == nil) return;
Expand Down
2 changes: 1 addition & 1 deletion benchmark.txt
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ ok zee/internal/localbench 39.311s
## is no longer set on whisper_full. That flag made whisper.cpp advance a
## fixed 30 s per window and skip the failed-decode retry, so audio the
## model did not emit was dropped (11 s lost on 21-50-06; 11% of that whole
## transcript). Upstream whisper.cpp#2186; see docs/design-notes.md.
## transcript). Upstream whisper.cpp#2186.
## Speed impact measured separately (wall, model load in, mean of 3):
## 160 s clip 3.78 -> 4.08 s (+8%)
## 131 s clip 4.24 -> 3.57 s (-16%, its bad decode burned fallback retries)
Expand Down
2 changes: 1 addition & 1 deletion clipboard/clipboard_darwin.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// Both replace things that cost real felt latency: pbcopy/pbpaste fork(), and
// fork freezes every thread for O(resident memory), which is significant once a
// local model is resident — while keybd_event held Cmd+V down for a hardcoded
// 100 ms sleep. Measurements in docs/design-notes.md.
// 100 ms sleep.

// clipCopy replaces the pasteboard with one UTF-8 text item. Returns 1 on
// success. No locale involved, unlike the pbcopy child process it replaces.
Expand Down
5 changes: 4 additions & 1 deletion cmd/overlaydemo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//
// go run ./cmd/overlaydemo
//
// It cycles Recording → Are you talking? → Transcribing every few seconds.
// It cycles Recording → Are you talking? → Transcribing every few seconds, and
// steps the top-right tag through none, the palette and an over-long name.
package main

import (
Expand All @@ -29,10 +30,12 @@ func drive() {
var state atomic.Int32
go func() {
states := []overlay.State{overlay.Recording, overlay.Silent, overlay.Transcribing}
tags := []string{"", "clean", "friendly", "professional", "slack", "magenta", "lime", "a-very-long-profile-name"}
for i := 0; ; i++ {
s := states[i%len(states)]
state.Store(int32(s))
overlay.SetState(s)
overlay.SetTag(tags[i%len(tags)], overlay.TagColors[i%len(overlay.TagColors)])
time.Sleep(3 * time.Second)
}
}()
Expand Down
15 changes: 15 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ type Settings struct {
// TailWaitMs keeps the mic open this many ms after the hotkey is released so
// a fast keyup doesn't clip the last word. 0 disables the wait.
TailWaitMs int `json:"tail_wait_ms"`
// PostTranscribe rewrites each transcript with an LLM before it is pasted
// (see the posttranscribe package). Off unless Active names a profile.
PostTranscribe PostTranscribe `json:"post_transcribe"`
}

// PostTranscribe is the post_transcribe block of config.json. Active picks a
// profile by name — one of the user's Prompts, else a built-in; "" is off.
type PostTranscribe struct {
Active string `json:"active"`
Model string `json:"model,omitempty"` // "" = the package default
Prompts map[string]string `json:"prompts,omitempty"` // name → instruction
// Colors is each profile's overlay tag colour, "#RRGGBB". zee fills in a
// colour for a profile the first time it is used, so it stays the same from
// then on.
Colors map[string]string `json:"colors,omitempty"`
}

const settingsFile = "config.json"
Expand Down
Loading
Loading