Commit Graph
10 Commits
Author SHA1 Message Date
yusufipk 034c2bec7e Clean the transcript up on the subscription you already have
Cleanup was the one step with only one place to run. Speech to text has three
providers behind a setting and the agent has three behind another, but the
model that drops the "eee"s out of a sentence was always a request to
OpenRouter, which meant a second key on a machine that already pays for a model
and already hands whole dictations to it as commands. Claude Code and Codex can
rewrite a sentence as easily as they can put something in your calendar, and
now they may.

cleanup.py is where that choice lives, so worker, the file transcriber and the
meeting all ask the same question rather than each building the same OpenRouter
request. What comes out of a CLI that failed is a CleanupError, which is an
ApiError, because to the chain a cleanup that failed is a cleanup that failed
however it was run: the raw transcript is still pasted and the reason still
shows in the corner, unchanged.

Neither CLI is given anything it does not need for the job. No tools, no MCP
servers, no session to resume, and the home directory rather than wherever the
agent is pointed, since a project's instructions have opinions about how text
should be written and none of them are about this transcript. The transcript
goes in fenced the same way the OpenRouter call fences it, because it is
material rather than an instruction however much of it reads like one. Claude
takes the cleanup rules as its whole system prompt; Codex has no system prompt
of its own, so they ride in front of the text, and its answer is read from the
file it writes on the way out rather than from a stdout that also carries a
header, its thinking and a token count.

The cost is seconds. OpenRouter answers in about one, a CLI in six or seven,
because each one opens a whole session to do it. That is the trade the box
says out loud, and the default has not moved: OpenRouter cleans up until you
say otherwise.

Codex's two lowest thinking levels now ask for "low". "minimal" was its bottom
rung until the newer models replaced it with "none", and each of them answers
the other's word with a 400, which the agent has been quietly hitting too.

In the settings window the model box belongs to whoever is chosen rather than
meaning three different things in turn, since an OpenRouter id and a Claude
alias do not belong in the same field, and under it is the same "found it or
not" line the agent tab has. dikte doctor asks about the program instead of the
key when a CLI does the cleaning, and the history records which model actually
did it.
2026-08-01 19:30:55 +03:00
yusufipk bb3b0bcb94 Give every window a verb, and answer what the terminal asks
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.
2026-08-01 18:58:57 +07:00
yusufipk e2551343e4 Run a dictation and a command to the agent at the same time
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.
2026-07-28 18:29:48 +07:00
yusufipk 01eea7a363 Let the command go to Codex or OpenRouter, not only Claude Code
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.
2026-07-28 17:08:14 +07:00
yusufipk 0206303751 Let a dictation be a command for Claude Code, not only text
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.
2026-07-28 16:57:10 +07:00
yusufipk 32e0fb061d Let the cleanup model be told how hard to think 2026-07-26 23:14:06 +07:00
yusufipk 761245a305 Transcribe on OpenRouter too, not just OpenAI
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.
2026-07-25 23:01:21 +07:00
yusufipk 2cbc369132 Let the history be pruned, and keep the settings window open on save
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.
2026-07-25 20:07:36 +07:00
yusufipk 60c8006725 Repair misheard words, surface cleanup failures, add a restart action
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
2026-07-25 19:51:32 +07:00
yusufipk efa8687b23 Voice dictation for KDE Wayland: record, transcribe, clean up, paste
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
2026-07-25 19:24:46 +07:00