A transcript the hallucination filter throws away is named in the message
that says so, and the placeholder it goes into is called {text}. So is the
first parameter of t(), which made the call two values for one argument and
a TypeError: the user was told "Unexpected error" rather than what happened.
Both strings are positional-only now, so no placeholder can ever collide with
them again.
The socket carried a bare word and said nothing back, so a script could press
the buttons but never learn what was said. A request is JSON now, it can ask to
be answered when the run ends rather than when it starts, and the reply carries
the transcript, the agent's answer, or why nothing happened. The verbs that do
not need the microphone run in the caller's own process, which is what makes
transcribing a file or reading a setting work over ssh.
Cleaning up a dictation and cleaning up a file are not the same job, and until
now they shared one prompt. A dictation is read afterwards, so dropping a
filler and tightening a sentence is a favour. A file becomes an SRT, and there
the same favour is damage: the viewer hears the words while the line is on
screen, so a word that was said and is not written is noticed, and a phrase
pulled onto the line above is on screen before it is spoken.
So the file path gets its own system prompt. It says what the text is and what
it is for, and it spends its room on the one repair only context can make: the
word the transcriber misheard. Speech models fail phonetically on names, and
somebody talking about Anthropic said "Claude", not "cloud". The lines stay
where they are, nothing is shortened, nothing is turned into an abbreviation,
and the filler words stay because they were said out loud.
The glossary and the timestamp rule are appended as before, so a name listed
under Cleanup rules still reaches this prompt, and a timestamped run still gets
told to leave the stamps alone. The dictation prompt is untouched, and so are
the meeting and agent paths.
Cleanup rules now has a tab each. An untouched prompt is still stored empty, so
switching the interface language keeps switching the prompt language with it.
Two things a real command turned up.
A job can run for ten minutes. "Report on the front page" is not a question
with an answer a second later, and having the corner narrate every tool it
touches for those ten minutes is worse than saying nothing. So that indicator
can be clicked away. It is the only one that can: a dictation is over in
seconds, and an indicator that swallows a click meant for the window underneath
has to earn it. The work carries on; what is muted is the progress, not the
outcome, which shows up whether or not the box was sent away. A faint cross on
the right says the box can be clicked, because a feature nobody can see is not
one. The next run starts visible again.
Muting leaves the state alone rather than setting it to something hidden, so no
later repaint puts the box back on the screen behind its own back.
Thinking effort is a setting now, offered once rather than three times: how hard
to think is one thing to want, and only the rungs differ. Claude takes it as
--effort, Codex as a model_reasoning_effort override, OpenRouter in the
reasoning field it already understood for cleanup. A level a provider does not
have lands on the nearest one it does, so "maximum" is xhigh on Claude and high
on Codex rather than an error or a silent drop. Left alone, nothing is sent and
each model does what it would have done.
The two shared one state machine, which meant an agent thinking for a minute
was a minute in which nothing could be dictated. That is the wrong trade: the
work has nothing in common past the microphone, and the microphone is busy for
the seconds you are speaking, not the minute afterwards.
So they are two flows now, each with its own state, its own pipeline and its own
indicator. Either can be working while the other records. Only the recorder is
taken in turns, because there is one of it: whichever asks second is refused
while the first holds it, and the menu entry greys out to say so rather than
failing quietly. "Am I recording" is read off the two states rather than off the
recorder's owner, which outlives the recording by the moment it takes the audio
to arrive, and would otherwise report a microphone as busy after it was free.
Two indicators in one corner would sit on top of each other, so an indicator can
be told what it stacks on: it offsets by that one's height while it is showing
and drops back into the corner when it goes, which the animation timer notices
without anything having to tell it.
Pasting is now under a lock. It is three steps rather than one, read the
clipboard, write ours, press the key, and two runs finishing together would
paste one answer and restore the other's clipboard over it.
Calling off the agent is its own menu entry and its own command, instead of a
cancel that meant different things depending on what was running. The tray icon
still ends whichever recording is going.
Four things the first use of it turned up.
The tab was called Claude while the setting inside it chooses between three
providers, two of which are not Claude. It is Agent now, and the messages that
pointed at it by name point at the new one.
The three global shortcuts were the only fields in the window you had to know
the syntax of to fill in, next to a paste key that has been a dropdown all
along. They are editable combo boxes now, sharing one shortlist of combinations
that are usually free. Typing your own still works; it is just no longer the
only way.
The instruction was read as discouraging the tools, which is the opposite of the
point. "Keep it short" is about the answer, and a model that reads it as "answer
off the top of your head" hands back a caveat about its training data instead of
the thing it was asked. Asked who won the 2026 World Cup it answered, twice, that
the tournament had not been played, on the 28th of July. So shortness is now
stated as being about the answer rather than the work, and writing a caveat is
named as the moment to go and look instead. Same question, same model, only the
prompt changed: no tool calls and a wrong answer before, a web search and the
right one after. It also appended its sources, which a reply being pasted into
someone's text field has no use for, so that is ruled out alongside the headings
and the bullet lists.
Clicking the tray icon during a Claude recording did nothing, because the icon
was wired to the dictation toggle and that one leaves the other mode's recording
alone on purpose. The shortcuts should stay tied to their own mode; the icon is
one button and now ends whichever recording is running.
Everything the last commit built assumed one agent was installed, which is a
poor assumption to bake into a dictation tool. So the provider is a setting, and
what it selects is one of three quite different things.
Claude Code and Codex are the same shape: a CLI, streaming JSONL, a session id
to resume, tools that reach the machine and whatever is connected to it. They
share the runner. What differs is spelled out where it differs, which is more
than the flag names: Codex has no system prompt to append, so the instruction
rides in front of the command with a rule between them; it confines its commands
in a sandbox rather than asking about them, so the permission setting is a
sandbox mode; and `-s` is not accepted by `exec resume`, so both settings go
through `-c` overrides, which are.
OpenRouter is the odd one and is meant to be. No tools, no files, no calendar:
it can say what the capital of Peru is and not what is in your diary, and the
settings box says so rather than letting it be discovered. It also has no
session to resume, so the conversation is kept here and resent, capped at 24
messages.
A stored conversation names the provider that made it, and is ignored by any
other: none of them can pick up another's thread, and a stale id would otherwise
fail every command until the timeout cleared it.
The interface calls the thing by its name, which in Turkish means the suffix has
to agree with it: Claude'a but Codex'e, Claude'u but Codex'i. A name dropped
into a sentence through t() cannot be inflected by that sentence, so it arrives
inflected, from a small table in i18n. English takes the name as it is and keeps
the preposition in the sentence.
Dictation ends the same way every time: the words you said land in the window
you were in. But half of what you say to a machine is not text to place, it is
something to do, and the answer to "what is on my calendar today" is not that
sentence written out.
`claude -p` is that session run without a window: the same skills, the same
connected services, the same account. So a second shortcut records exactly as
the first one does and sends the transcript there instead, and what comes back
is pasted where the transcript would have been. Which is what puts "save that
to my calendar on Thursday at three" inside a text field that has nothing to do
with Claude.
Permissions are the part worth being deliberate about, because nothing here can
answer a prompt: a mode that would have asked denies instead. It runs in `auto`,
which decides for itself with the prompt-injection checks left on, and the other
two modes are one setting away. What Claude was not allowed to touch is reported
rather than swallowed, since a reply that reads perfectly normal is otherwise
the only sign that the job did not happen.
Commands close together stay in one conversation, so "and move that to Thursday"
knows what "that" is; half an hour of silence ends it, as does the tray. The
answer is written to be read where it lands: no headings, no lists, and a
sentence naming what was done when something was done.
Its output is read as it streams rather than waited out, so the corner names the
tool it is on. A calendar lookup took 29 s in testing, which a still indicator
would have made indistinguishable from a hang. The clock and the stop button are
watched from a thread of their own, because the stream blocks between lines and
a model that thinks for a minute sends none: they end the run by killing the
process, which closes the stream and unwinds everything else on its own.
Transcript cleanup is off on this path. Claude reads through "erm" and "hani"
without help, and skipping it saves an API call and a second or two in front of
a screen you are standing at.
Who said what is the hard part of a meeting transcript, and the usual answer
is to hand one mixed recording to a model and ask it to tell the voices apart.
That guess is wrong often enough to be worse than useless in minutes, where a
decision attributed to the wrong person is a decision nobody made.
So the question never reaches a model. ffmpeg records the microphone and the
default sink's monitor as one stereo stream, you on the left and everyone else
on the right, and one process reading both is what keeps them aligned over an
hour. Each channel is transcribed on its own and the two are interleaved on a
single timeline, so attribution is settled by the wire a voice arrived on.
What a microphone picks up from the speakers lands on both channels; our copy
is dropped when it overlaps theirs in time and says nearly the same thing.
The stream is written to disk as it arrives rather than held in memory, so
length costs nothing and a crash costs the tail instead of the whole meeting.
Every stage the run reaches is recorded in meetings.jsonl, so a failure while
summarising does not throw away the transcription of an hour of audio: the
retry reads the transcript back out of the document and picks up from there.
A run that dies keeps its recording whether or not audio is being kept.
The minutes model is configured on its own, under Settings, with its own
prompt, and it is told who was expected in the room so the names come out
spelled right. It is told outright that the transcript is a record of other
people talking, not instructions addressed to it.
The built-in listener now holds several bindings rather than one, and the KDE
side is parameterised by desktop id, so the meeting toggle gets a shortcut of
its own on the same footing as the dictation one.
The listener reads /dev/input and reacts the instant a key goes down. The
KDE shortcut answers the same press by launching a whole Python process,
which then talks over IPC, so its toggle lands a few hundred milliseconds
later. The 400 ms guard caught that echo only when the machine happened to
be quick, and otherwise the recording was started and stopped by one press:
"No speech detected".
Route the two apart. A toggle arriving from outside the process right
behind a listener trigger is that echo, and its lateness also proves the
KDE shortcut is live, which leaves the listener with nothing to do but
double every press. So retire it, remember that in the config, and say so
in the tray rather than changing behaviour silently.
The Turkish prompt stopped naming filler words and started weighing what a
word does in its sentence, and the English one had the same flaw waiting:
"like" and "you know" were listed as words that carry no meaning, which they
are only half the time. A model reading that next to the MINIMUM
interference rule leaves them where they are.
It now reads like the Turkish one. Thinking sounds are their own line,
everything else is dropped when the sentence survives without it and kept
when it points at something or carries the clause, the listed words are
examples rather than a closed set, and doubt resolves towards dropping.
Measured over three dictated paragraphs written to mix both uses, same model
and only the prompt swapped: 36 candidates in the raw text, 17 surviving the
old prompt, 6 the new one, the survivors being real uses such as "works like
a charm" and "you know the one I mean". The old default is fingerprinted as
legacy alongside the Turkish one.
The Turkish cleanup prompt listed "hani" among the filler words from the
first version on, and the model kept it anyway. The history file shows why:
in one and the same dictation "eee" goes and "hani" stays. There is nothing
ambiguous about a thinking sound, so it always goes, while "hani" is a real
word half the time ("hani su adam vardi ya"), and against the MINIMUM
interference the rest of the prompt asks for, the model resolves that doubt
by leaving it alone. Naming the word once more would not have changed that.
So the rule no longer turns on which word it is. Thinking sounds are their
own line, and everything else is measured by what it does in the sentence:
drop it if the meaning survives, keep it if it points at something or
carries the clause, and when in doubt drop it. The words that used to be the
rule are now examples, explicitly not a closed set, so the ones nobody
thought to list are judged the same way.
Measured over the six most recent dictations that carry such words, cleaned
with the same model and only the prompt swapped: 18 in the raw text, 17
surviving the old prompt, 6 the new one, and most of those six are genuine
uses. The old default is fingerprinted as legacy so that a settings window
still showing it does not write it back into the config.
whisper-1's verbose response carries a start and an end for every segment,
and the file tab was reading only the start, to build the [mm:ss] prefix.
Keeping the end as well is all an SRT needs.
The text stays the authority on wording and the segments on timing; they
meet at that prefix, which the cleanup model is already told to leave alone.
So a transcript that went through cleanup still turns into properly timed
subtitles. A line whose stamp matches no segment runs until the next line
starts, a line with no stamp at all joins the cue above it, and an end that
would run into the next cue is trimmed back.
The button is dead until a timestamped run finishes, because without
timestamps there are no segments to time anything with.
Hiding the indicator unmapped its window, so KWin tore that window down and
built a new one for the next dictation. On a tiled desktop the compositor
repainted whatever sat underneath as it went, and the terminal behind
visibly flinched every time a transcript landed. Measured with a KWin
script: five show-and-hide rounds raised five windowAdded and five
windowRemoved events, where there is now one of each for the life of the
process.
So the window stays mapped and paints nothing while idle. That has to be a
real repaint rather than zero opacity: with the animation timer stopped
nothing else damages the surface, and the stale frame sat on the screen
until something unrelated, moving the mouse, made the compositor redraw it.
The timer was also left running after the indicator went away, ticking
thirty times a second over a window nobody could see. It stops now, and the
finished message lingers for two seconds rather than one.
OpenRouter mirrors OpenAI's /audio/transcriptions field for field, so one
multipart request serves both providers and only the key, the base URL and
the model id change. That puts a dozen speech models behind the key that was
already there for the cleanup, and makes a single OpenRouter key enough for
the whole chain.
Its transcription endpoint takes no hint field, so the words from Cleanup
rules are not sent there; they still reach the cleanup model as a glossary.
Timestamps switch to openai/whisper-1, the namespaced id of the only model
that returns segment times.
The API tab is now grouped by role rather than by service, because a key no
longer belongs to a single job: both keys sit at the top, the two jobs below.
Each provider keeps its own model, so switching back and forth does not
overwrite the other one's.
"Shortcut presses during Transcribing… are ignored" read as though Dikte
blocked the session's shortcuts while it worked. It only ignores its own:
pressing Ctrl+Space with a transcript in flight does nothing and queues
nothing, while the rest of KDE keeps working normally. That is now said
where it belongs, next to the indicator.
The rest is shorter. Silence detection, misheard words and file
transcription became bullets under one heading, the settings table is gone
because the window already says what each setting does, and Known limits
folded into a line under the file map. The numbers worth knowing stayed: the
10 dB over each recording's own noise floor, the 0.3 s, the -55 dBFS, and
the one logout KWin needs before the global shortcut fires.
The History tab was read-only, so the only way to get rid of a dictation
was to edit history.jsonl by hand. Entries can now be removed: right-click
or Delete drops the selection, "Clear history" empties the file, and the
Ctrl/Shift selection lets several go at once. Deleting more than one asks
first, since there is no undo.
The cap on the file has existed since the first commit but was never
reachable from the interface. It is now a spinbox in the same tab, applied
the moment you save rather than on the next dictation, and 0 turns it off.
Trimming rewrites the file only when it is actually over the cap, instead
of on every single dictation, and does it through a temporary file so a
crash cannot leave a half-written history behind.
Deletion matches an entry on its whole content, not on its line number: the
worker may well have appended a new dictation while the window sat open.
Save no longer closes the window. It confirms and stays put, so you can
settle several things in one sitting; the cross closes it. Cancel is gone
with it, because it would be a lie next to a Save that already wrote.
The cleanup prompt now asks the model to fix words the transcriber misheard
when the context makes the intended one clear, and to leave them alone when
it does not. Speech models fail phonetically on proper nouns, and that is
exactly what context can recover.
The names you enter for the transcription hint are handed to the cleanup
model as a glossary too. Knowing the spelling is what lets it recognise
"kuber netis" as Kubernetes.
A failed cleanup used to be almost invisible: the raw transcript was pasted
and a progress line flashed by, so a rejected key looked exactly like
working dictation for days. It now leaves the indicator amber with the
reason, sends a notification, and records the error in the history. HTTP
401, 402 and 429 are reported as what they are, naming the service.
Also:
- Settings can test the OpenRouter key, not just the OpenAI one
- Tray menu and CLI gained Restart, which re-execs in place
- Defaults saved into the config by older versions are recognised by their
fingerprint and dropped, so an untouched prompt keeps getting improvements
- The IPC socket is user-only; Qt puts it in /tmp
The screenshots were downscaled to 430 px wide, which made the UI text
blurry. Restore them at native 1292 px as lossless WebP, which is also
half the size of the original PNGs (72 KB against 155 KB for the largest).
Rewrite every em dash in prose, comments, docstrings and interface strings
as ordinary punctuation.
Ctrl+Space starts and stops a recording. The audio goes to OpenAI for
transcription, a model on OpenRouter strips the fillers and restores
punctuation, and the result is copied and pasted into the focused window.
Only the Python standard library and PyQt6 — HTTP, multipart uploads and
WAV writing are all hand-rolled.
- pw-record captures raw 16 kHz mono PCM with a live level meter
- the corner indicator is drawn through XWayland, since a Wayland client
cannot position its own window
- silence is caught before it costs an API call, relative to each
recording's own noise floor, plus a filter for the stock phrases models
invent when handed silence
- audio and video files can be transcribed too, optionally with [mm:ss]
timestamps, chunked through ffmpeg for long inputs
- global shortcut installs as a KDE custom shortcut, with an evdev
listener as a fallback until the session is restarted
- Turkish and English interface, following the system locale by default