Commit Graph
14 Commits
Author SHA1 Message Date
yusufipk 352f7b6c22 Record a meeting from both sides, and write its minutes
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.
2026-07-28 16:32:31 +07:00
yusufipk 0bdee07564 Stop the KDE shortcut and the built-in listener from doubling a press
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.
2026-07-27 23:00:14 +07:00
yusufipk 32e0fb061d Let the cleanup model be told how hard to think 2026-07-26 23:14:06 +07:00
yusufipk e3e678a0f2 Apply the same filler rule to the English prompt
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.
2026-07-26 22:51:02 +07:00
yusufipk f77fd44991 Judge a filler word by its job in the sentence, not by a list
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.
2026-07-26 22:47:16 +07:00
yusufipk 37e360743d Save the transcript as subtitles too, not just as text
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.
2026-07-26 22:33:32 +07:00
yusufipk c3f22f0794 Stop the indicator from making the window behind it flinch
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.
2026-07-26 12:54:23 +07:00
yusufipk 1dc7eceee4 Drop the provider note, and reshoot the API tab
The line under Speech to text repeated the provider box directly above it,
so nothing was lost by removing it.
2026-07-25 23:05:40 +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 f901c995c4 Trim both READMEs, and fix what they claimed about shortcuts
"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.
2026-07-25 20:12: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 011493a9bc Ship full-resolution WebP screenshots, drop em dashes everywhere
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.
2026-07-25 19:31:10 +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