Skip to content

Post-transcribe: LLM rewrite before paste, tray picker, overlay tag - #40

Merged
sumerc merged 21 commits into
mainfrom
post-transcribe
Oct 23, 2026
Merged

sumerc merged 21 commits into
mainfrom
post-transcribe

Conversation

@sumerc

@sumerc sumerc commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Closes #4

What

An optional LLM pass rewrites each transcript before it is pasted. Off by default.

  • Pick a profile from the tray (Settings → Post-Transcribe: None, grammar, friendly, professional, concise, then your own) or in config.json. A pick is saved and applies from the next recording — no restart.
  • Overlay tag: while a profile is active, the recording overlay shows its name top-right in its colour.
  • Sticky colours: post_transcribe.colors holds each profile's colour; a profile without one gets a random palette pick at startup/reload, saved so it stays the same.
"post_transcribe": {
  "active": "friendly",
  "model": "openai/gpt-oss-120b",
  "prompts": { "slack": "Rewrite it as a short, casual Slack message. Fix all grammatical errors." },
  "colors": { "friendly": "#FFD60A" }
}
  • Uses the groq key from credentials.json; any transcription provider.
  • Shared rules for every profile: never answer the dictated text; keep the sentence in the language it was spoken in (English words in a Turkish sentence stay English, sound-spelled ones like "puşladım" become "pushladım"); keep code/paths/URLs; correct misheard words toward hints.txt, including terms heard as ordinary words ("cloud" → "Claude" only when the sentence is about Claude).
  • On error or timeout (15 s) the raw transcript is pasted and the tray shows the error.
  • Streaming mode: while a profile is active, text is pasted once at the end.
  • -debug-transcribe: each recordings/<timestamp>/ folder keeps transcript.txt (raw) beside post_transcript.txt; info.json records profile, model and any error; transcribe_log.txt appends the rewrite as a fifth field. felt_latency gains post_transcribe_ms.

Where

  • posttranscribe/ — profiles (fixed built-in order), prompt rules, Groq call (reuses transcriber.TracedClient).
  • config/ — PostTranscribe schema (here to avoid a transcriber → config → posttranscribe cycle).
  • tray/ — Post-Transcribe submenu; click handlers never wait on a lock held across a systray call (the deadlock class fixed in fix: blocking-bug and silence-warning fixes #38).
  • overlay/ — SetTag + palette (Apple dark-mode colours minus red/amber, which mean Recording/Silent).
  • main.go — warm on record start, rewrite before paste, tag per recording, colour assignment.
  • log/ — post-transcribe fields in saved recordings.

Measured (2026-10-22, Groq gpt-oss-120b, reasoning_effort: low)

  • ~200–450 ms per rewrite. gpt-oss-20b is faster but answered a dictated question instead of rewriting it.
  • friendly, 8 runs each, before → after the language rules: Turkish sentence flipped to English 1/8 → 8/8 kept; "crash" kept 5/10 → 8/8. Still open: "silence detection" translated to Turkish ~3 in 8.
  • "cloud" → "Claude": plain vocabulary list 0/18; with the "heard as an ordinary word" sentence 18/18 fixed, and "the cloud" / "cloud costs" kept 18/18.

Test

go test -race ./... passes. Manual: pick a profile in the tray, dictate, check the overlay tag and the pasted text.

🤖 Generated with Claude Code

@upsun-dispatch

Copy link
Copy Markdown

Upsun Dispatch™ did not run this request: no model provider API key is registered for this organization.

Register one in settings, then trigger again.

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reviewed — No blocking findings · 🔵 3 minor points · ⚪ 1 nitpick

🔍 Full review · 7 files reviewed

🔵 Minor points

  • posttranscribe/posttranscribe_test.go:75 — strings.Contains(sys, "Vocabulary:") looks for a header the code never writes. systemPrompt writes "Vocabulary — names and terms the user says often:", so the string never contains Vocabulary:. The check passes whether or not empty hints add a vocabulary section, which means it cannot catch the regression it was written for.
  • posttranscribe/posttranscribe.go:159 — On a non-200 status, the whole raw response body goes into the error (string(resp.Body)). main.go shows that error verbatim through tray.SetError("Post-transcribe failed, ..." + err.Error()). A Groq JSON error, or an HTML page from a proxy or captive portal, lands in the tray as one unbounded blob with nothing to shorten it.
  • posttranscribe/posttranscribe.go:27 — Two choices rest on measurements and a rejected alternative that appear only in the PR description: DefaultModel is gpt-oss-120b because 20b answered questions instead of rewriting them, and reasoning_effort: low because medium fixed no more vocabulary. docs/design-notes.md gets no entry. CLAUDE.md says a decision resting on a measurement or a rejected alternative must be recorded there, with a date and version.

⚪ Nitpick

  • main.go:1172 — On the first recording with a profile active, go posttranscribe.Warm() calls client(). That builds the client with transcriber.NewTracedClient, which already runs go tc.Warm() itself. Warm then sends a second HEAD right away, so the first warm-up makes two parallel requests and may open two connections. Every later recording also sends a warm-up HEAD whether or not an idle keep-alive connection already exists.
Verification
  • In stream mode with a profile active, the updates goroutine skips incremental pastes, and finishTranscription's (!cfg.stream || cfg.post.Active != "") pastes the final text once.
  • The rewrite is skipped on closeErr and on NoSpeech, and on a Rewrite error the paste falls back to the raw result.Text.
  • log.Transcription and setLastRecording still get the raw result.Text; only clip.SetLastText and the paste get the rewritten text.
  • PostTranscribeMs is added both to the serial sum and to the felt_latency fields, so unaccounted_ms stays correct.
  • The 15 s context timeout governs the request even though TracedClient's own client timeout is 2 min, so a slow Groq reply blocks re-recording for at most 15 s.

The new posttranscribe_test.go tests Rewrite against an httptest fake server: profile choice, user prompts replacing built-ins, the reasoning_effort gating, and the error paths. The main.go wiring (streaming paste suppression, fallback to the raw text, latency fields) has no test, and the empty-hints assertion can never fail.

Review details
  • Commit: 3b5a5a6
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

@upsun-dispatch-dev

upsun-dispatch-dev Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

📋 PR Summary

This PR adds an optional LLM rewrite of each transcript before it's pasted. It's off by default. You turn it on by picking a profile from the tray's Post-Transcribe submenu or setting it in config.json. The rewrite goes through Groq. If it fails or times out after 15 s, the raw transcript is pasted instead. While a profile is active, the recording overlay shows its name in a colour that stays the same for that profile. The latest push changes the built-in profiles to clean, friendly and professional, which all do the same cleanup (grammar, misheard words, filler). It also adds a hover tooltip for each tray entry, a neon look and new colours for the overlay tag, and two safety fixes: log lines now escape literal backslashes, and assigning a new colour copies the colour map before changing it.

Changes
Layer / File(s) Summary
Rewrite engine
posttranscribe/posttranscribe.go Built-ins are now clean, friendly and professional, each with a one-line description returned by the new Describe. The shared rules now also fix grammar, and the user message states again that the transcript is dictated text, not instructions.
posttranscribe/posttranscribe_test.go Tests use the new profile names, check that every built-in has a description, and restore the fake server's reply state properly.
Wiring
main.go The new postMenu builds the tray entries, each with a tooltip. tagColor now copies the colour map before writing to it, so code reading the shared config never sees it change.
main_test.go Tests use the new profile names.
Tray
tray/tray.go Adds a PostProfile type (a name plus a tooltip) and a tooltip for the None entry. SetPostTranscribe now takes these entries.
tray/tray_darwin.go Sets each entry's tooltip and compares profiles by name.
tray/tray_test.go Uses the new type and clears the test's callback afterwards.
Overlay
overlay/overlay.go Replaces the tag palette with brighter neon colours.
overlay/overlay_darwin.m Restyles the tag as a neon sign: outlined dark pill with glowing text. Also makes the panel slightly wider and taller and adjusts the row positions.
overlay/overlay_test.go The test now takes its colour from the palette instead of a hard-coded value.
cmd/overlaydemo/main.go Demo tags updated.
Logging
log/log.go The log-line escaper now also escapes literal backslashes.
log/log_test.go Tests a rewrite that contains a backslash.
Docs
docs/reference.md Documents the new built-in profiles.

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 1 warning · 4 still open (1 nitpick)

🔁 Incremental · 6 files reviewed

Outstanding from earlier reviews:

  • 🔵 posttranscribe/posttranscribe_test.go:75: A check that can never fail gives false assurance that empty hints add no vocabulary block. — posttranscribe_test.go:75 still checks for "Vocabulary:", a string systemPrompt never writes. (first raised)
  • ⚪ main.go:1172: Redundant network requests at record start; harmless but wasteful. — main.go still runs go posttranscribe.Warm() on each recording, and NewTracedClient still starts go tc.Warm() itself the first time. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:162: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — Rewrite still puts string(resp.Body) unbounded into the error, and that error is shown in the tray. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:27: A later contributor may switch back to 20b for speed without knowing it was already measured and rejected. — docs/design-notes.md still has no post-transcribe or gpt-oss entry. (first raised)
Verification
  • post is filled only inside the branch where post-transcribe ran, so for a recording with post-transcribe off, Recording.Post stays zero and saveRecording writes no post_transcribe key and no post_transcript.txt.
  • When the rewrite fails, post.Text stays empty, so the log line keeps four fields while info.json still records the profile, the model and the error.
  • posttranscribe.Model is now the only place cmp.Or(c.Model, DefaultModel) appears, so the model recorded in info.json is the same model Rewrite sends.

This change adds log_test.go's TestTranscriptionPostTranscribe, which covers a successful rewrite and a failed one, and extends TestTranscription to check that no post_transcript.txt is written. No test covers a rewrite that contains newlines or tabs, or the main.go wiring that fills post.

Review details

Review 2 of 10 for this pull request · View the full run

Comment thread log/log.go Resolved

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reviewed — No new issues found · 5 still open (1 nitpick)

🔁 Incremental · 1 file reviewed

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — log.go still adds "\t" + r.Post.Text to the line without escaping, so a rewrite containing newlines or tabs still breaks the one-record-per-line format.
  • 🔵 posttranscribe/posttranscribe_test.go:75: A check that can never fail gives false assurance that empty hints add no vocabulary block. — posttranscribe_test.go:75 still checks for Vocabulary:, a header systemPrompt never writes (it writes Vocabulary — ...). (first raised)
  • ⚪ main.go:1172: Redundant network requests at record start; harmless but wasteful. — main.go:1172 still runs go posttranscribe.Warm() on every recording, and the first call builds the client with NewTracedClient, which starts its own warm-up too. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:165: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — On a non-200 status, Rewrite still puts the whole string(resp.Body) into the error, which then reaches the tray. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:27: A later contributor may switch back to 20b for speed without knowing it was already measured and rejected. — docs/design-notes.md still has no entry for gpt-oss-120b or reasoning_effort; the model appears only in docs/reference.md. (first raised)
Verification
  • The new vocabulary sentences are inside the if hints != "" branch, so an empty hints list still produces a prompt with no vocabulary section.
  • The added string concatenation is well-formed: line 86 now ends with + and joins the three new literals into the same s += expression.
  • The new guidance names only terms "from this list", so the cloud→Claude example does not tell the model to swap words that are not in the user's hints.

This change only edits the prompt text, and no test checks the new sentences. The Vocabulary: check in posttranscribe_test.go still looks for a header the code never writes.

Review details

Review 3 of 10 for this pull request · View the full run

@sumerc sumerc changed the title Post-transcribe: config-only LLM rewrite before paste Post-transcribe: LLM rewrite before paste, tray picker, overlay tag Oct 22, 2026

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 1 warning · 5 still open (1 nitpick)

🔁 Incremental (head + base moved) · 16 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — log.go still appends r.Post.Text unescaped, so a rewrite containing a newline or tab still breaks the one-record-per-line format.
  • 🔵 posttranscribe/posttranscribe_test.go:77: A check that can never fail gives false assurance that empty hints add no vocabulary block. — posttranscribe_test.go:77 still checks for Vocabulary:, but systemPrompt writes Vocabulary — names and terms…. (first raised)
  • ⚪ main.go:1187: Redundant network requests at record start; harmless but wasteful. — main.go still runs go posttranscribe.Warm(), and client() → NewTracedClient warms the connection again on first use. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:176: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — posttranscribe.go still puts the whole string(resp.Body) into the error with no length limit. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:27: A later contributor may switch back to 20b for speed without knowing it was already measured and rejected. — docs/design-notes.md still has no entry on the gpt-oss-120b default or on reasoning_effort: low. (first raised)
Verification
  • Profiles lists the built-ins in builtinOrder and leaves out user prompts that shadow a built-in, so the tray entries and the colour assignment are stable and contain no duplicates.
  • choosePost checks idx against postEntries() while holding trayMu, and calls the callback only after releasing the lock, so the click handler cannot deadlock on trayMu.
  • SetTag falls back to the first palette colour when the hex is unreadable, and layoutTag hides the pill when the text is empty, so Active: "" shows no tag.
  • Each recording reads the overlay tag from config.Get() at session start, so a profile picked in the tray shows up from the next recording, as the updated reference.md says.

Added in this push: TestEnsureTagColors, TestParseHex, TestPickTagColor, TestProfiles, TestBuiltinOrder and TestChoosePost. The Objective-C overlay drawing and the systray submenu rendering are not covered by any test.

Review details

Review 4 of 10 for this pull request · View the full run

Comment thread main.go Resolved

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

1 critical finding in this review. Details below.

Caution

Changes requested — 🔴 1 critical · 🟡 4 warnings · 🔵 1 minor point · 4 still open (1 nitpick)

🔁 Incremental · 3 files reviewed

🔴 Critical (P0 — merge blockers) — outside diff range

general · merge-conflict

This PR does not merge cleanly onto the current base branch: docs/design-notes.md conflicts. The new "Post-transcribe prompt" section is appended at the end of the file, and the base has changed there too. Rebase or resolve the conflict before merging.

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — log/log.go:335 still appends r.Post.Text unescaped, so newlines or tabs in the rewrite still break the one-record-per-line format.
  • 🟡 #4350272205 — main.go:490: A silent config write pins today's defaults, so a future default change (such as the tail-wait trial) never reaches existing users. — ensureTagColors() still runs on every startup (main.go:490) and on reload, and calls config.Update whenever a built-in profile has no colour, so it still writes the full Settings to disk.
  • ⚪ main.go:1187: Redundant network requests at record start; harmless but wasteful. — main.go:1187 still runs go posttranscribe.Warm() on every recording, and the client is still built with NewTracedClient. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:170: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — complete() still formats string(resp.Body) into the error with no truncation. (first raised)
Verification
  • TestRewriteUserProfileShadowsBuiltin still works: its new "Vocabulary" check cannot false-positive, because the new rules text never contains that word.
  • complete() sends the system and model it is given unchanged, so moving the request out of Rewrite keeps the same request shape and the gpt-oss reasoning_effort gating.
  • The new design-notes entry gives the date, model, reasoning effort and commit (c2a4673) behind the prompt and model choices, as CLAUDE.md requires.

This increment changes only the prompt text and one assertion in posttranscribe_test.go. The tests check the request structure, not whether the model follows the new rules. The prompt's quality rests on the offline eval described in docs/design-notes.md, which no automated check reruns.

Review details

Review 5 of 10 for this pull request · View the full run

Comment thread posttranscribe/posttranscribe.go Resolved
Comment thread posttranscribe/posttranscribe.go Resolved
Comment thread posttranscribe/posttranscribe.go Resolved
Comment thread posttranscribe/posttranscribe.go Resolved
Comment thread docs/design-notes.md Resolved

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 1 warning · 9 still open (1 nitpick)

🔍 Full review · 20 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — Transcription still appends "\t" + r.Post.Text unescaped, so newlines or tabs in the rewrite still break the one-record-per-line format.
  • 🟡 #4350272205 — main.go:490: A silent config write pins today's defaults, so a future default change (such as the tail-wait trial) never reaches existing users. — ensureTagColors still calls config.Update on startup and on Reload whenever a built-in has no colour, which writes every default to config.json.
  • 🟡 #4350590746 — posttranscribe/posttranscribe.go:78: Users who dictate commands or paths get them silently changed in the pasted text, against the documented behaviour. — rules still has no line telling the model to keep code, paths, URLs and commands as they are.
  • 🟡 #4350590753 — posttranscribe/posttranscribe.go:36: Users who pick grammar do not get the punctuation and grammar fixes the docs describe. — The grammar built-in is still "Change nothing more.", and reference.md still describes spelling/punctuation/grammar fixes.
  • 🟡 #4350590759 — posttranscribe/posttranscribe.go:79: User profiles and tone profiles can be silently cancelled by the global "do not rewrite" rule, so a chosen profile appears to do nothing. — rules still says "do not rewrite, and add nothing", which contradicts the tone profiles and the documented slack prompt.
  • 🟡 #4350590766 — posttranscribe/posttranscribe.go:82: Stray tags or a preamble can end up in the user's pasted text. — The reply rule is still only "Reply with the cleaned text only." and complete() only trims whitespace, so echoed tags or a preamble get pasted.
  • 🔵 #4350590771 — docs/design-notes.md:2226: Contradictory measurements make the record hard to trust when the prompt is revisited. — The design note still says 0/24 → 24/24, and the PR description still says 0/18 → 18/18.
  • ⚪ main.go:1187: Redundant network requests at record start; harmless but wasteful. — client() still builds the client through NewTracedClient, which already starts go tc.Warm(), and Warm() then calls client().Warm() again. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:170: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — posttranscribe.go:170 still puts string(resp.Body) into the error, and main.go shows that error in the tray without shortening it. (first raised)
Verification
  • SetPostTranscribe at startup runs before tray.Init, so mPost is nil and onReady builds the submenu itself; the reload path calls it from go reloadCfgCb(), off the main thread.
  • The post-transcribe rewrite is skipped on closeErr, !HasText or NoSpeech; a rewrite error keeps text = result.Text, so batch paste falls back to the raw transcript.
  • Logs and saved recordings keep result.Text (raw); clip.SetLastText and the paste use the rewritten text.
  • ReleaseToText adds PostTranscribeMs to the serial sum, so unaccounted_ms stays correct.

This PR adds unit tests: posttranscribe_test.go (Rewrite against an httptest server, Profiles, error paths), log_test.go (fifth field and info.json), overlay_test.go (parseHex, PickTagColor), tray_test.go (choosePost) and main_test.go (ensureTagColors). No test covers the streaming paste path with a profile active, or finishTranscription's paste or fallback logic.

Review details
  • Commit: 8f16b54
  • Model: claude-opus-5-5

Review 6 of 10 for this pull request · View the full run

Comment thread main.go Resolved
@upsun-dispatch-dev
upsun-dispatch-dev Bot dismissed their stale review October 22, 2026 19:22

Superseded: the latest Upsun Dispatch review no longer requests changes.

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

1 critical finding in this review. Details below.

Caution

Changes requested — 🔴 1 critical · 🟡 1 warning · 10 still open (1 nitpick)

🔁 Incremental · 14 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — line += "\t" + r.Post.Text still writes the LLM output without escaping newlines or tabs.
  • 🟡 #4350272205 — main.go:490: A silent config write pins today's defaults, so a future default change (such as the tail-wait trial) never reaches existing users. — ensureTagColors() still runs at startup (main.go:490) and calls config.Update whenever a built-in profile has no colour.
  • 🟡 #4350590746 — posttranscribe/posttranscribe.go:78: Users who dictate commands or paths get them silently changed in the pasted text, against the documented behaviour. — rules still has no line telling the model to keep code, paths, URLs or commands as they are.
  • 🟡 #4350590753 — posttranscribe/posttranscribe.go:36: Users who pick grammar do not get the punctuation and grammar fixes the docs describe. — grammar is still "Change nothing more." while docs/reference.md:83 still promises spelling, punctuation and grammar fixes.
  • 🟡 #4350590759 — posttranscribe/posttranscribe.go:79: User profiles and tone profiles can be silently cancelled by the global "do not rewrite" rule, so a chosen profile appears to do nothing. — rules still says "do not rewrite", which contradicts the tone profiles and the user rewrite prompts.
  • 🟡 #4350590766 — posttranscribe/posttranscribe.go:82: Stray tags or a preamble can end up in the user's pasted text. — The reply rule is still just "Reply with the cleaned text only.", and complete() only trims the reply, so tags, quotes or a lead-in can still be pasted.
  • 🔵 #4350590771 — docs/design-notes.md:2146: Contradictory measurements make the record hard to trust when the prompt is revisited. — ⚠️ regressed this push: Not fixed: the design note was deleted outright rather than reconciled with the description, which leaves no record of the measurement at…
  • 🟡 #4352560288 — main.go:1331: A streaming user with a profile on loses a whole dictation after a mid-session error or an auto-close, where before this change it was… — The final paste still needs closeErr == nil && !skipPaste, and the stream loop is still gated on cfg.post.Active == "", so streamed text is still lost on a session error or auto-close.
  • ⚪ main.go:1187: Redundant network requests at record start; harmless but wasteful. — Warm() still calls client().Warm(), and the first client() call builds NewTracedClient, which already runs go tc.Warm(), so the first warm-up still sends two requests. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:170: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — The non-200 error still embeds the whole string(resp.Body), and main.go passes it to tray.SetError without shortening it. (first raised)
Verification
  • saveRecording now creates recordings/ first and then claims a unique folder with os.Mkdir, adding a -N suffix on IsExist, so two saves in the same millisecond no longer overwrite each other.
  • Transcription still writes line + "\n" under logMu, and the post field is appended only when r.Post.Text is non-empty.
  • posttranscribe_test.go now checks for Vocabulary, which systemPrompt writes only when hints are non-empty, so the empty-hints check can now fail when it should.
  • A grep of the merged tree finds no remaining reference to design-notes, so no link points at the deleted file.

This push adds no test for the new unique-folder loop in saveRecording; log/log_test.go covers the earlier post-transcribe fields. Apart from that loop and one test assertion, the push is documentation and comment removal, and no automated check covers those.

Review details

Review 7 of 10 for this pull request · View the full run

Comment thread docs/design-notes.md Resolved
Comment thread CLAUDE.md Resolved
sumerc and others added 6 commits October 23, 2026 11:38
A post_transcribe block in config.json picks a named profile (built-in
grammar/friendly/professional/concise, or the user's own prompts) and
rewrites each transcript through Groq chat completions before it is
pasted. Off by default. Misheard words are corrected toward hints.txt.
On failure the raw transcript is pasted. Streaming pastes once at the
end while a profile is active.

Refs #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ording

The "keep the language" rule let the friendly profile translate English
words inside Turkish speech ("crash" -> "çökme"). The rule now keeps each
word in the language it was spoken in, and restores English spelling of
phonetically-written words ("puşladım" -> "pushladım").

With -debug-transcribe, each recordings/<timestamp>/ folder now holds
post_transcript.txt beside the raw transcript.txt, info.json records the
profile, model and any error, and transcribe_log.txt appends the
rewrite as a fifth field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain vocabulary list never turned "cloud" into "Claude": the
sound-alike is a real word that fits. One prompt sentence with an
example fixes it only when the sentence is about the term, and keeps
the ordinary word ("the cloud") otherwise. Prompt-only; hints.txt and
what transcription providers receive are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sentence language

Tray: Settings → Post-Transcribe lists None and every profile (built-ins
in a fixed order, then the user's own). A pick is saved to config.json
and applies from the next recording, no restart; Reload Config re-renders
it. Click handlers never wait on a lock held across a systray call.

Overlay: the active profile shows as a small coloured tag top-right.
Each profile's colour lives in post_transcribe.colors; one without a
colour gets a random palette pick at startup or reload, saved so it
stays the same.

Prompt: short Turkish sentences full of sound-spelled English words
("mörj", "brançın") were rewritten wholly in English. The rules now
keep the sentence's grammar language, restore spelling one word at a
time, and keep English words when rephrasing for tone; friendly keeps
the speaker's own words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rules had grown patch by patch, with example words ("cloud" ->
"Claude", "dipley ettim"). They are now five generic lines: clean up
without rewriting, never answer, carry out or finish the transcript,
keep each word in the language it was spoken in, restore English
spelling of sound-spelled words. Profiles are one short line each.

On 240 rewrites of real Turkish/English dictations (4 profiles x 3
runs) failures drop from 71 to 51. A dictated "draft me a Slack
message" stays a request on every profile. Details and rejected
variants in docs/design-notes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove docs/design-notes.md and every reference to it (CLAUDE.md,
README, docs/reference.md, code comments, Makefile, install.sh,
benchmark.txt).

log: two recordings saved in the same millisecond shared one
recordings/<timestamp>/ folder and the second overwrote the first,
which made TestTranscriptionPostTranscribe flaky. A taken name now
gets a -2, -3, ... suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 1 warning · 🔵 1 minor point · 11 still open (1 nitpick)

🔍 Full review · 29 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4349630394 — log/log.go:335: The transcribe log stops being parseable line by line exactly when the most common rewrite profiles are in use. — log.go still appends "\t" + r.Post.Text unescaped, so a newline or tab in the rewrite breaks the one-record-per-line format.
  • 🟡 #4350272205 — main.go:492: A silent config write pins today's defaults, so a future default change (such as the tail-wait trial) never reaches existing users. — ensureTagColors() still runs at every startup and reload, and on first launch it calls config.Update (a full config.json write) because the built-in profiles have no colours.
  • 🟡 #4350590746 — posttranscribe/posttranscribe.go:78: Users who dictate commands or paths get them silently changed in the pasted text, against the documented behaviour. — rules still has no line protecting code, paths, URLs or commands, while it does tell the model to fix errors and remove filler.
  • 🟡 #4350590753 — posttranscribe/posttranscribe.go:36: Users who pick grammar do not get the punctuation and grammar fixes the docs describe. — The grammar built-in is still "Change nothing more." but docs/reference.md still promises spelling, punctuation and grammar fixes.
  • 🟡 #4350590759 — posttranscribe/posttranscribe.go:79: User profiles and tone profiles can be silently cancelled by the global "do not rewrite" rule, so a chosen profile appears to do nothing. — rules still says "do not rewrite, and add nothing", which contradicts the tone profiles and the documented slack rewrite example.
  • 🟡 #4350590766 — posttranscribe/posttranscribe.go:82: Stray tags or a preamble can end up in the user's pasted text. — Only "Reply with the cleaned text only." remains: nothing forbids quotes or echoing the &lt;transcript> tags, and complete() only trims whitespace.
  • 🟡 #4352560288 — main.go:1346: A streaming user with a profile on loses a whole dictation after a mid-session error or an auto-close, where before this change it was… — With a profile active in streaming mode, the only paste still requires closeErr == nil && !skipPaste, so a mid-session error or an auto-close loses all the streamed text.
  • 🔴 #4357026741 — docs/design-notes.md:1: The project's whole decision record is lost as a side effect of a feature PR, and the guards it explained become unexplained code that is… — docs/design-notes.md is still deleted, along with every pointer to it.
  • 🟡 #4357026745 — CLAUDE.md:9: Contributors and coding agents lose the rule that kept measurements recorded and dated, so future decisions are made without evidence. — The design-notes rule and its clause on where measurements go are still removed from CLAUDE.md.
  • ⚪ main.go:1202: Redundant network requests at record start; harmless but wasteful. — NewTracedClient still calls go tc.Warm() itself, and posttranscribe.Warm() then sends a second HEAD on every recording. (first raised)
  • 🔵 posttranscribe/posttranscribe.go:170: Users get an unreadable error message whenever the rewrite fails at the HTTP level. — posttranscribe.go:170 still puts the whole string(resp.Body) into the error that the tray shows. (first raised)
Verification
  • choosePost drops the tray lock before calling the callback, and renderPostChecks holds no lock during systray calls, so a click handler cannot deadlock against refreshPostMenu.
  • saveRecording's Mkdir loop with a -N suffix stops two saves in the same millisecond from sharing a folder, and returns any error other than 'already exists'.
  • In streaming mode with a profile active, the clipboard save still happens: go saveClip() runs after recording ends, so finishTranscription's <-clipCh cannot block forever.
  • ReleaseToText adds PostTranscribeMs to the serial sum, so the rewrite's time is not counted as unaccounted_ms.

New unit tests cover profile resolution and HTTP error handling (posttranscribe_test.go), log fields (log_test.go TestTranscriptionPostTranscribe), colour assignment (main_test.go TestEnsureTagColors), the palette (overlay_test.go) and tray selection (TestChoosePost). They run under make test (go test -race ./...). No test covers the streaming paste path, the overlay drawing, or concurrent config access.

Review details
  • Commit: 9249a9d
  • Model: claude-opus-5-5

Review 8 of 10 for this pull request · View the full run

Comment thread main.go Resolved
Comment thread tray/tray_test.go Resolved
sumerc and others added 8 commits October 23, 2026 11:46
A multi-paragraph LLM rewrite broke transcribe_log.txt's one-record-per-line,
tab-separated format. post_transcript.txt keeps the real text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…batim

The base rules tell every profile to fix phonetic errors, so a dictated
command or path could be "corrected".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ript> wrapper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tray

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ensureTagColors ran at startup and on Reload Config and saved the whole
Settings for every user, pinning implied defaults even for users who never
use post-transcribe. Now a profile gets its colour when it is picked in the
tray or, as a fallback, when a recording starts with it active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irst

The first Warm created the TracedClient, whose constructor already warms it,
then warmed it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a streaming provider and a post-transcribe profile, live pastes are off
and the only paste is at the end. A mid-session error (Close returns the
committed text with the error) or a silence auto-close dropped the whole
dictation, which without a profile had been pasted live. Now the raw text is
pasted and kept as last text on an error, and an auto-close pastes as the
live path did. Batch providers and recordings without a profile are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reviewed — No new blocking findings · 🔵 1 minor point · 7 still open (1 nitpick)

🔁 Incremental · 8 files reviewed

🔵 Minor point

  • log/log.go:343 — lineEscaper turns newline, CR and tab into the two-character sequences \n, \r and \t, but it leaves a literal backslash as it is. A rewrite that already contains the text \n therefore looks the same in transcribe_log.txt as one that contained a real line break. Dictated code is the likely trigger: printf("a\n"), or a Windows path such as C:\new\tmp (contains \n and \t). Both kinds of text land in the fifth field as identical bytes, so the field cannot be decoded back to the text that was pasted. The new rule says to keep code and paths exactly, which makes this input more likely.

Outstanding from earlier reviews:

  • 🟡 #4350590753 — posttranscribe/posttranscribe.go:36: Users who pick grammar do not get the punctuation and grammar fixes the docs describe. — The grammar built-in is still just "Change nothing more." and nothing asks for punctuation or grammar fixes, while docs/reference.md still says "spelling, punctuation, grammar".
  • 🟡 #4350590759 — posttranscribe/posttranscribe.go:79: User profiles and tone profiles can be silently cancelled by the global "do not rewrite" rule, so a chosen profile appears to do nothing. — The rules still say "Keep the speaker's words… do not rewrite". That rule wraps tone changes (friendly/professional), "Make it concise" and any user profile; only the docs example was softened.
  • 🔴 #4357026741 — docs/design-notes.md:1: The project's whole decision record is lost as a side effect of a feature PR, and the guards it explained become unexplained code that is… — docs/design-notes.md is still deleted (docs/ holds only reference.md), and the pointers to it are still removed.
  • 🟡 #4357026745 — CLAUDE.md:9: Contributors and coding agents lose the rule that kept measurements recorded and dated, so future decisions are made without evidence. — CLAUDE.md still drops the design-notes rule and the clause that sends measurements there.
  • 🟡 #4357696342 — main.go:1459: Adding a profile can, rarely, crash the whole app with an unrecoverable fatal error. — tagColor still writes c.Colors[name] in place into the map that config.Get() copies share. It now runs on every first use of a profile, from the tray click and from recordSessions, while save/readers read the same map without the lock.
  • 🔵 #4357696347 — tray/tray_test.go:63: The test state leaks into the rest of the package's tests, so the cleanup does not restore what it appears to. — SetPostTranscribe(nil, "", nil) still keeps the existing postCb, so the test's closure stays installed after cleanup.
  • ⚪ main.go:1201: Redundant network requests at record start; harmless but wasteful. — The double HEAD on the first Warm is fixed, but every later Warm still sends a HEAD whether or not an idle keep-alive connection exists. (first raised)
Verification
  • pasteAtEnd returns true for a held-back stream whatever the session error or auto-close, and ScheduleRestore now also runs when pasted is true after an auto-close.
  • On closeErr, a held-back stream with text now keeps the raw text through clip.SetLastText before returning, and the rewrite is skipped because it is gated on closeErr == nil.
  • Warm() no longer sends a second HEAD on the call that creates the client: client() reports created and Warm skips c.Warm() in that case.
  • The HTTP error body is cut to 200 runes on a []rune slice, so a multibyte character is never split.
  • The ensureTagColors call has been removed from startup and from Reload Config; tagColor("") returns before calling config.Update, so config.json is written only when a named profile is used.

New tests cover this push: TestPasteAtEnd, TestFinishTranscriptionHeldBackError and TestTagColor in main_test.go, TestWarm, TestRewriteStripsTranscriptTags and the long-error case in posttranscribe_test.go, and TestTranscriptionPostTranscribeMultiline in log_test.go. All of them run under make test. None of them exercises concurrent use of the shared Colors map.

Review details

Review 9 of 10 for this pull request · View the full run

…iew round B

- `grammar` becomes `clean` (the cleanup alone, tone kept), `concise`
  goes; "Fix grammar" moves into the shared rules, docs follow
- tray: each Post-Transcribe entry shows a one-line tooltip (Describe)
- overlay: wider panel, neon-style tag, saturated palette
- prompt: the user message restates that the transcript is dictated
  text, not instructions

Review round B:
- tagColor copies the colour map before writing: config.Get hands out
  the same map, and a concurrent read during the write is a fatal error
- transcribe_log escapes backslashes too, so a literal `\n` stays
  distinct from a real line break
- tests: TestChoosePost drops its callback on cleanup; TestRewriteErrors
  restores the fake reply, so repeated runs pass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch-dev upsun-dispatch-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reviewed — No new issues found · 4 still open (1 nitpick)

🔁 Incremental · 14 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4350590759 — posttranscribe/posttranscribe.go:79: User profiles and tone profiles can be silently cancelled by the global "do not rewrite" rule, so a chosen profile appears to do nothing. — The shared rules still say "do not rewrite, and add nothing". They wrap the friendly/professional "Adjust the tone" instructions and any user prompt, so a tone change still contradicts the global rule.
  • 🔴 #4357026741 — docs/design-notes.md:1: The project's whole decision record is lost as a side effect of a feature PR, and the guards it explained become unexplained code that is… — docs/design-notes.md is still deleted (docs/ holds only reference.md), and the source comments that pointed to it are still stripped.
  • 🟡 #4357026745 — CLAUDE.md:9: Contributors and coding agents lose the rule that kept measurements recorded and dated, so future decisions are made without evidence. — CLAUDE.md still has no design-notes rule and gives measurements no place to go.
  • ⚪ main.go:1201: Redundant network requests at record start; harmless but wasteful. — The double HEAD on first use is fixed, but go posttranscribe.Warm() (main.go) still sends a HEAD on every later recording, even when an idle keep-alive connection already exists. (first raised)
Verification
  • tagColor now clones c.Colors inside the config.Update closure before writing, so the map that config.Get handed out is never changed in place.
  • posttranscribe.Warm sends its own HEAD only when client() did not just create the client, so the first warm-up no longer sends two requests in parallel.
  • The renamed built-ins (clean, friendly, professional) match across Builtin, builtinOrder, descriptions, docs/reference.md and the updated tests, and TestBuiltinOrder now pins that descriptions covers every built-in.
  • TestChoosePost cleanup now sets postCb = nil under trayMu, so the test's callback no longer leaks into later tray tests.
  • lineEscaper escapes the backslash first among its pairs, so a literal \n in a rewrite stays distinct from an escaped line break, and the multiline test covers it.

This pass changes unit tests: log/log_test.go (backslash escaping), main_test.go TestTagColor, posttranscribe_test.go (descriptions coverage, profile renames) and tray_test.go TestChoosePost. The overlay neon restyle in overlay_darwin.m and the new tray tooltips (SetTooltip) have no automated test; the only check is the manual overlaydemo.

Review details

Review 10 of 10 for this pull request · View the full run

@sumerc
sumerc dismissed upsun-dispatch-dev[bot]’s stale review October 23, 2026 10:49

All findings are fixed or answered.

sumerc and others added 6 commits October 23, 2026 13:50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rewrite runs while the recorder is held, so a down provider or a
stalled network made each dictation wait the full timeout before the raw
text was pasted, and refused the hotkey meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
transcribe_log.txt is one tab-separated record per line. The rewrite was
escaped but the raw transcript was not, so a transcript with a tab or a
line break shifted the columns. transcript.txt keeps the real text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A typo in post_transcribe.active gave the name an overlay colour, showed
its tag, and made every recording run a rewrite that failed with
"unknown profile" and set a tray error. posttranscribe.Resolve clears an
Active that names no profile; main uses it for the overlay tag, the
recording config and the tray, and warns once on load and reload.
config.json keeps the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tray's post-transcribe pick ran config.Update for Active, then
tagColor ran a second one on a first pick: two config.json writes in the
click handler. setTagColor works on *config.Settings, keeps the
copy-on-write of the Colors map, and serves both tagColor and the tray
callback, which now does a single config.Update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sumerc
sumerc merged commit 2d181f6 into main Oct 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grammar/spell correction of transcribed text (cheap model) before auto-paste

1 participant