Commit Graph
80 Commits
Author SHA1 Message Date
yusufipek ca559e3961 Fill the clipboard buffer before emptying the clipboard
EmptyClipboard is the point of no return: after it, whatever was there is gone,
and the allocation that failed on the next line left the clipboard holding
nothing. That is the one path where restoring what a dictation borrowed could
lose it instead. The buffer is filled first, and the clipboard is opened only
once there is something to put in it.
2026-08-16 10:28:52 +03:00
yusufipek 8c62795b8b Give Windows devices an identifier, and ask ffmpeg for them once
Three things about the dshow backend, all of them found by reading rather
than by running, so all three want checking on a real Windows machine.

The device listing is parsed in both of the shapes ffmpeg has printed it in:
newer builds mark every device `(audio)` or `(video)`, older ones print a
heading and no marks, and only the first was read. Each pattern is anchored at
both ends now, so the error lines the command ends with, which quote the device
name it was told to look for, are no longer read as a device of that name.

What is stored for a device is the alternative name under it rather than the
friendly one. A laptop with a headset plugged in has two microphones called the
same thing, and `audio=Microphone` reaches the first of them whichever one was
picked; the alternative name is unique. The friendly name stays what is shown,
which is what the (id, description) pair in these lists has always been for.

An unset microphone meant "the first one listed", and the listing costs an
ffmpeg of its own, so every press of the key paid for a process before the
recording started. The last listing is remembered instead, and opening Settings
or running `dikte devices` takes a fresh one.

And a fourth thing, which is about what the interface says rather than what it
does: whether the far side of a meeting can be captured at all is now an entry
in `audio.Sound` instead of being read off an empty device list. The two are not
the same answer. An empty list on Linux means pactl is not installed, which a
user can go and fix; False on Windows means there is no such device and no
driver that would add one. The Meeting tab says so under the empty box, and
starting a meeting says it instead of sending somebody to Settings to pick from
a list that will never have anything in it.
2026-08-16 10:28:43 +03:00
yusufipek 191eef8f8d Merge master into the Windows port
Three of the four collisions were the same one: master moved the directory
rule into paths.py while this branch was adding a Windows case to the copy in
config.py and the second copy in ggml.py. The case moves to paths.py with the
rest of it, and the directories test moves to tests/test_paths.py where master
put its neighbours.

The fourth is MeetingRecorder, which now starts a process per capture device.
Windows keeps its two lines there: no console window for either process, and
a stop that terminates rather than sending a signal the platform does not have.
2026-08-16 10:14:37 +03:00
Yusuf İpek ef016d326f Merge pull request #26 from benfirad/codex/macos-audio-device-safety
Harden macOS meeting audio capture
2026-08-16 10:00:29 +03:00
yusufipek a6fab18a22 Keep the meeting a quiet microphone gave us, and read both captures at once
A recording is never deleted for being disappointing. A microphone that
handed over nothing still leaves the right channel, which is everyone
else, and an hour of them is worth more than the empty channel costs; the
one thing the user cannot get back is the half that was there. So the
exact-zero check stays and stops throwing the file away: it says what the
microphone did, in a tray warning next to the recording being written up,
and the minutes are produced from what there is.

Reading the two capture pipes in turn from one thread put the failure it
was meant to fix back in a worse place. A microphone that stops delivering
leaves that read waiting forever, and the far side is not read either
until its pipe fills and its ffmpeg stops writing into it: the meeting
freezes, the levels sit still, and nothing is said for as long as nobody
looks. Each stream now has a reader of its own and a queue, so neither can
hold the other up, and a side that has said nothing for STALL_SECONDS ends
the recording the way a dead ffmpeg already did, out loud and keeping what
was captured.

Which system needs how many processes belongs in the table with everything
else that differs, so meeting() returns the list of commands it takes:
one on PulseAudio, one per device on a Mac. meeting_commands() is the
chooser again rather than a function with a Mac inside it, and the empty
entry in COREAUDIO is gone. The two AVFoundation targets are resolved
against a single device listing, which costs one ffmpeg run instead of two
and cannot see the indexes renumber between the microphone and the far
side.
2026-08-16 09:51:02 +03:00
Can Soykan Yılmaz 44cfb76652 Give a Mac an installer, an app bundle and a menu bar icon it can see
The macOS backends were already here: CoreAudio capture through ffmpeg,
pbcopy and CoreGraphics, Carbon hotkeys, the paths under ~/Library. What
was missing was everything that installs them, so install.sh hands over to
install-mac.sh on Darwin rather than growing a branch per line: the XDG
directories, the .desktop files and the shortcut registry mean nothing
there, and an application is a bundle rather than a path. The bundle
carries a copy of the interpreter, because macOS files the microphone and
Accessibility permissions against the process that asks, and a launcher
running Homebrew's python3 would have asked as python3 and shared the
grant with everything else on that interpreter. It is signed ad-hoc so a
reinstall is the same application rather than two more dialogs, and it
says so plainly when a brew upgrade has moved the tree it needs.
uninstall.sh and update.sh follow it.

The tray icon was invisible: QIcon.fromTheme wants a freedesktop icon
theme and hands back a null icon without one, which in a menu bar is the
whole interface gone. trayicon.py draws the three shapes as template
images, so they follow the menu bar into dark mode, and the bundle's icon
comes off the same glyph rather than a binary in the repository. Linux
keeps its own icons; these are used only where the theme has nothing.

paths.py is the fix that was never about a Mac. config.py imports ggml.py,
so ggml.py could not ask it where the data goes; each worked it out for
itself and only one of them knew about macOS. Settings went to ~/Library
while several gigabytes of models went to ~/.local/share, which is not a
place a Mac user looks and not a place uninstall.sh --purge would have
deleted from.

Ctrl+Space is the input-source switch there and Cmd+Space is Spotlight, so
the default is Ctrl+Option+Space, and hotkey.default_combo is the one
place that difference lives. The first paste asks for Accessibility with
kAXTrustedCheckOptionPrompt, which is what creates the row to switch on;
asking the other way opens a pane Dikte is not listed in. `dikte shortcut
status` asks the running instance, since the combination is held by that
process and by nothing else.

Local speech to text is the one piece a Mac builds by hand. whisper.cpp
publishes no macOS binary and Homebrew's is configured with
WHISPER_BUILD_SERVER=OFF, so it installs whisper-cli and not the server
Dikte talks to; program_path already takes a whisper-server off the PATH
or out of Settings, so the answer is the one a Linux distribution gets,
and the README carries the cmake line. CI grows a macOS job on 3.11 and
3.13, the only place the Carbon and CoreGraphics libraries have to be
there to be opened.

Written and tested on macOS 27.0 arm64. Two things are still unverified on
a Mac: the paste end to end, which waits on the Accessibility toggle, and
a meeting recording, which needs a loopback driver.
2026-08-15 15:56:27 +03:00
huseyin-emre-tigciandClaude Fable 5 6b86696648 Spell the Windows paths with forward slashes in the directories test
A backslash only separates on Windows, and this test also runs on the
Linux that checks the Windows half.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 18:09:24 +03:00
huseyin-emre-tigciandClaude Fable 5 3436e6b426 Add Windows support
Windows joins the three systems as its own entry in each table: DirectShow
through ffmpeg for capture, the Win32 clipboard and SendInput for the paste,
RegisterHotKey for the global shortcut, and the whisper.cpp and llama.cpp
Windows zips (the OpenBLAS whisper build, which transcribes about twice as
fast on a plain CPU). Settings go to APPDATA, data to LOCALAPPDATA, and
install.ps1 adds the Start Menu entry, the dikte command and an optional
autostart. Meetings are not supported yet: Windows offers nothing to record
the far side from.

Porting surfaced three fixes that were not Windows specific:

- A stopped or overlong download tried to delete its .part file while still
  holding it open, which Windows refuses. The unlinks now wait for the handle.
- The CLI transcribed files without handing the local servers their settings
  first, so a local provider failed with "no model downloaded" wherever the
  GUI had not run in the same process.
- The audio content types are pinned instead of asked of the registry, which
  answers differently machine to machine.

One fix is Windows specific but sits in shared code: shutdown() does not end
a blocked recv there, so stopping a request also closes the socket handle.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 16:53:21 +03:00
yusufipk 77b26e76be Stop the indicator from swallowing clicks in its corner
WA_TransparentForMouseEvents does nothing for a top-level window: Qt
takes the click and then drops it, so it still never reaches whatever is
underneath. Since the window stays mapped while idle, that turned its
corner of the screen into a dead zone for good. Qt::WindowTransparentForInput
is the one that leaves the window without an input region at all.

The dismissable one has to keep taking clicks, and the flag is read once
when the window is created, so it shrinks to a point while concealed
instead. Resizing keeps the surface alive, which is the whole reason
concealing does not simply hide it.
2026-08-10 15:59:35 +03:00
firat 2cd561da77 Harden macOS meeting audio capture 2026-08-09 00:33:06 +02:00
Yusuf İpek d506e0d3d1 Merge pull request #16 from hakanekin/fix/pw-record-raw-compat
Handle pw-record versions without --raw support
2026-08-05 18:42:58 +03:00
yusufipk f6754ebf43 Name the PipeWire the check is for, and keep it out of the tests
--raw arrived in 1.4, the same release that stopped reading a bare "-" as
raw on its own. The split is there rather than at 1.0: Ubuntu 24.10 and
anything else on 1.2 refuses the option too, and 1.4 onwards writes a
container around the stream without it.

Asking the installed binary meant the command tests ran pw-record --help
four times for real, on a machine whose answer decides what they see, which
the module docstring promises they never do. They pin the answer in setUp,
and the reading itself gets its own class, the empty help nobody covered
included.
2026-08-05 18:41:23 +03:00
yusufipk 49b940bcb9 Merge master into the pw-record raw check
recording_command became a table of sound systems while this branch was
open, so the pw-record command it patched now lives in _pulse_record. The
check moves there with it, and nothing else about it changes.
2026-08-05 18:40:54 +03:00
Yusuf İpek a1420b703b Merge pull request #12 from Murqin/fedora-install
Write down what installing on Fedora takes
2026-08-05 16:48:12 +03:00
yusufipk 993eb6511b Keep the Fedora section to what you have to type
The section ran longer than Ubuntu and macOS put together, and most of it
answered questions nobody asked while installing: why ffmpeg-free is enough
in detail, why the runtime directory settles after login, and why the local
models need nothing added. That last one says "do nothing", which is what
the reader would have done anyway. The reasoning stays in the pull request,
where it belongs; the README keeps the package line and the drop-in.

install.sh printed the drop-in the README already carries, six lines where
every other warning there is two. It now points at the README.

Its readiness check also went from asking whether the unit was up to asking
whether the socket is writable, which reads a stopped daemon as ready: the
socket file outlives the process. It now wants both, with pgrep for the
process so a ydotoold started by hand still counts. All five paths were
exercised with systemctl and pgrep shims, and on this Arch box the real
user service still reports ready.
2026-08-05 16:46:28 +03:00
Yusuf İpek 10d7ea08c4 Merge pull request #8 from yusufipk/macos-backend
Add a Mac as the third system, beside Wayland and X11
2026-08-05 15:10:23 +03:00
yusufipk d63f2f4a5a Say in the README that a Mac is one of the three
The first line still named KDE and Wayland only, and Install stopped at
Ubuntu. A Mac has nothing for install.sh to install into, so what it needs
is ffmpeg, PyQt6 and python dikte.py, plus BlackHole for a meeting. The
settings also sit somewhere else there.
2026-08-05 15:09:16 +03:00
yusufipk 41aec83a34 Say which Python the suite needs
A Mac tester ran the suite on the 3.9 that ships with macOS and got 167
errors, all of them TestCase.enterContext, which arrived in 3.11. The
workflow already runs 3.11, 3.12 and 3.13; nothing said so out loud.
2026-08-05 15:04:36 +03:00
yusufipk 9a3284c504 Merge master: the audio goes up as mp3 2026-08-05 15:03:42 +03:00
Yusuf İpek 0d8411502b Merge pull request #20 from benfirad/codex/macos-model-builds
Select native macOS local-model builds
2026-08-05 15:03:20 +03:00
Yusuf İpek 53ab60c451 Merge pull request #19 from benfirad/codex/macos-clipboard-native
Preserve every native macOS clipboard format
2026-08-05 15:02:48 +03:00
firat fa14ec90f4 Select native macOS model builds 2026-08-03 18:57:55 +02:00
firat ca6444c011 Preserve native macOS clipboard contents 2026-08-03 18:57:43 +02:00
Hakan Ekin 4e19d7279f Handle pw-record versions without raw support 2026-08-02 20:47:44 +03:00
yusufipk 395b52d685 Send the audio as mp3, and stop cutting a file that fits in one request
Whisper hears in thirty second windows and decides for itself where one cue
ends and the next begins. A chunk that starts in the middle of a sentence can
answer with one cue per window, twenty seconds of text at a time, for the whole
rest of the chunk: a twenty five minute recording was fine until 20:00, which
was where the second cut fell, and ran on in paragraphs from there. Sending the
same audio in one request instead of three gives cues of two and a half seconds
throughout.

The cuts were only ever there for the upload limit, and we were the ones
walking into it: ffmpeg opened a 24 MB m4a into 48 MB of uncompressed WAV, over
the 25 MB the APIs take, so the file had to be cut every ten minutes. As mp3 it
is 9 MB, and an hour of speech goes in one request. A server on this machine is
still handed the WAV, where nothing is uploaded and the encoder would only cost
quality. How long a chunk may be is now measured from the encoded file rather
than assumed from a bitrate.

Where a file still has to be cut, the chunks overlap by a whisper window and
stitch() drops the telling that was cut short, keeping the one that heard the
sentence whole. Meetings, which upload the WAV itself and so still cut every
ten minutes, get the same stitching.
2026-08-02 12:20:26 +03:00
yusufipk 822cf3b2a3 Merge master: the shortcut table, local models, and a Mac still in them
Four files disagreed, and all four the same way: master had turned things the
Mac branch wrote out by hand into one list to read from.

Shortcuts are the whole of it. master gave every binding a row in
hotkey.SHORTCUTS, so the Mac's DESKTOP_IDS is gone and CarbonHotkey reads the
desktop id off that row, which also gives the new cancel key a status line on a
Mac. Settings builds its four rows through master's _shortcut_row, and that one
now asks _install_buttons for Install and Remove, so macOS gets a combination
box and nothing to press, and everywhere else the button says the desktop's own
name. dikte.py starts the listener from the same table, on macOS whatever the
setting says: there is nothing installed for it to be a fallback to.

The rest is two imports and a paste list that lives in paste.Desktop now.
2026-08-02 09:36:22 +03:00
yusufipk 7a507c52eb Make the audio file tab remember, and its Stop stop
The two switches were written to disk by the Save button at the far end of
the window, so a file transcribed with timestamps and cleanup was
transcribed without either the next time. They belong to the run rather than
to the form: they go to disk as they are ticked now, and the folder the last
file came from goes with them.

Stop only set a flag that was looked at between chunks, and a file under ten
minutes is one chunk, so for most files it was looked at after the work it
was meant to stop had already finished. Nothing that blocks is reached by a
flag. The request is inside urlopen, ffmpeg is inside communicate, and a
whisper on this machine is a process of ours that would grind on to the end
of the chunk with nobody left to hand the answer to. So the socket is shut
down under the read, ffmpeg is killed, and a local server is stopped and
left for the next run to start again.

Shutting the socket down rather than closing it is the point: close() alone
leaves a thread already inside recv() waiting for bytes that are never
coming now. The connection is registered before it has a socket, so a stop
landing in the few lines between making a connection and blocking on it
refuses the connection rather than missing it and letting urllib quietly
open another.
2026-08-02 09:29:12 +03:00
Icarus Murqin b5ef0040b4 Write down what installing on Fedora takes
The README has Arch and Ubuntu on it, and the two things that differ on Fedora
are the two that decide whether anything works at all. The package names are
its own, and `systemctl --user enable --now ydotool` has nothing to enable
there: Fedora ships ydotool as a system service only, and its ydotoold holds a
root-owned socket at mode 600. The unit is green, the daemon is up, and every
paste is refused. The drop-in the README now spells out points ydotoold at
$XDG_RUNTIME_DIR/.ydotool_socket and hands it over by uid, which is the path
the client already looks at, so nothing has to carry YDOTOOL_SOCKET. Red Hat
closed the same report as NOTABUG in 2023, so this stays a step the user takes
rather than one a later package will take for them.

install.sh was asking whether the unit was up, which is the question that
answers wrongly here. It now asks whether the socket is one this user may write
to, which is what auto-paste actually needs, and it names which of the three
ways it failed: a socket handed to nobody, a daemon that put its socket
somewhere else, or no daemon at all. On Arch and everywhere else the answer is
the same one as before.

ffmpeg-free out of Fedora's own repositories is enough, against the usual
advice to reach for RPM Fusion. That build disables four video decoders, h264,
hevc, vc1 and vvc, and keeps every audio decoder it has, AAC among them, while
Dikte only ever asks a video file for its audio track.

The models that run on this machine ask for nothing either, which is worth
saying because it is now the way Dikte starts. The Ubuntu builds run on Fedora
unchanged; whisper.cpp transcribed a clip here without a package being added,
and llama.cpp came down in its Vulkan build and found the card. That last part
is not luck: kwin-libs requires libvulkan.so.1, so the loader is on every
Plasma desktop by way of the compositor, and it recommends the Mesa drivers
alongside itself.
2026-08-01 23:06:11 +03:00
yusufipk fc333fcb8c Reshoot every tab, and put the local models first
The settings window has four tabs it did not have when these were taken:
Agent, Meeting, Minutes and Shortcuts. Replace the four screenshots with
seven, at the same size and lossless as before. History goes without a
replacement rather than showing an old tab bar beside the new ones.

Running here is what the program does by default, and it was buried in a
paragraph under Install. Make it the first thing the feature list says,
and cut that paragraph back so the two do not repeat each other.
2026-08-01 21:05:46 +03:00
Yusuf İpek 74ca6621ef Merge pull request #11 from yusufipk/local-models
Transcribe and clean up on this machine, fetching both programs
2026-08-01 20:45:20 +03:00
yusufipk 97396d4734 Refuse a download that was published without a checksum
The hash was checked when there was one and skipped when there was not,
which is the wrong way round: everything fetched here is either run or
parsed by something written in C++, and the case with no hash is the one
where that matters most and says least.

Not hypothetical. GitHub publishes a digest for whisper.cpp v1.9.1 and for
llama.cpp today, and publishes none for whisper.cpp v1.8.0 and older, so
anything reaching for an older tag would have installed an executable
nobody checked.
2026-08-01 20:42:47 +03:00
yusufipk 785ceba02a Merge master: Groq, and cleanup on a subscription
Three of master's changes land on the same lines as this branch, so most of
this is picking the newer shape and putting the local half back into it.

cleanup.py arrived while this was being written and is the right place for
a fourth provider, so the Target refactor of api.cleanup goes away: llama.cpp
becomes a name in cleanup.PROVIDERS next to OpenRouter, Claude Code and
Codex, and worker.py, meeting.py and filetranscribe.py go back to master's.

The settings window keeps master's one row per provider, hidden with
setRowVisible, rather than the two wrapper widgets this branch had. So does
speech to text, which was doing the same thing its own way.

The transcriber table has no room for a provider with no key and no base
URL, so the local one is answered before the lookup rather than added to
it, and an unknown name now falls back to openai by name: the shipped
default is no longer a key of that table.

The minutes stay on OpenRouter, which master already decided by routing
only the transcript through cleanup.run, so meeting_provider goes.
2026-08-01 20:34:24 +03:00
yusufipk 593d307ecc Show a model name whole in the list it is chosen from
A combo box hands its own width to the list under it and elides whatever
does not fit, in the middle of the word: "ggml-org/Qwen....7B-Base-GGUF"
is not something anybody can choose between. The list is now as wide as its
longest row while the box stays the width the form gave it.
2026-08-01 20:16:38 +03:00
yusufipk f3573908d0 Count a model bigger than two gigabytes upwards
Qt's int is C++'s 32-bit one, so the progress of a 2.3 GB download arrived
at the other end of the signal as -1805209440, which is the real figure
less 2^32. The file was landing correctly; only the line under it was
wrong, reading "1.0 MB of -1805209440 B (-1%)".
2026-08-01 20:14:50 +03:00
yusufipk 634b46fcb0 Take the process with a start that goes wrong
_wait_ready can raise rather than return, and the process it was waiting on
is ours with nothing else holding a reference to it. Leaving it running
leaks a loaded model with nobody left to ask it anything, which is the
whole failure this class is careful about everywhere else. Found by two
stand-in servers still running after a test run.
2026-08-01 20:09:00 +03:00
yusufipk 2cfbbb2d99 Transcribe and clean up on this machine, without installing anything first
whisper-server is started on --inference-path /v1/audio/transcriptions,
which is exactly the path api.py already builds for the hosted providers,
and llama-server answers /chat/completions the way OpenRouter does. So the
local half is one more base URL rather than a second code path: worker.py,
filetranscribe.py and meeting.py are untouched, and dictation, subtitles
and meetings all work here on the first try.

Three findings worth naming, none of them in the new code:

whisper.cpp cuts segments on tokens, which in Turkish lands inside a word
about as often as between two. Pasted raw that gives "akraba değ\niller.";
in a subtitle it gives a cue reading "değ". Whisper marks the start of a
word with a leading space, so a piece that does not begin with one
continues the word above it.

A small model will repeat the transcript until the context is full, and
every one of those tokens is a second of somebody waiting: measured at 206
seconds, and 25 with a ceiling on the reply. Hosted models are left alone,
where the same runaway is rare and a ceiling would cut the minutes short.

A server outlives SIGTERM and SIGKILL holding its model in memory. Signals
are now turned into an event Qt delivers, since Qt blocks in C where a
Python handler never runs, and a pid file lets the next start sweep up
what a SIGKILL left behind.

The minutes keep their own provider rather than following cleanup's. The
two jobs are not the same size: a 4B model here will strip the filler words
out of a dictation and will not write up an hour long meeting.

The suite runs offline now: a test that reaches the network says so instead
of quietly going there.
2026-08-01 20:00:35 +03:00
yusufipk c0b892f53c Fetch whisper.cpp and llama.cpp, and keep a server alive on each
Two programs, one treatment: ask GitHub what was released, take the build
this machine can run, check it against the sha256 the index published, then
fetch a model from Hugging Face the same way and keep one server up on a
port of its own.

Neither list is written into the source. A model catalogue in here means a
release of Dikte for every model somebody else publishes, and a pinned
version means one for every whisper.cpp release; both are somebody else's
news. The lists are read at the moment the settings window is opened and
cached for a few hours, and a cache that has gone stale still answers when
the network does not.

Nothing that lands on disk is trusted for having arrived, which matters
more here than usual because half of what is fetched is a program Dikte
then runs.
2026-08-01 20:00:21 +03:00
Yusuf İpek 37f045d308 Merge pull request #10 from yusufipk/worktree-claude-openai-support
Clean the transcript up on the subscription you already have
2026-08-01 19:36:14 +03:00
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
Yusuf İpek da8112179b Merge pull request #9 from yusufipk/cancel-shortcut-and-scripts
A key for discarding a recording, plus uninstall.sh and update.sh
2026-08-01 19:05:28 +03:00
yusufipk 698eacf33a Take the install back, and pull the next one in
install.sh has been the only half of its job that was written down. Removing
Dikte meant remembering a symlink, three desktop files, an autostart entry and
a handful of kglobalshortcutsrc keys, and updating meant a pull followed by
guessing which of those the new version had moved. Both are now scripts,
because both are lists somebody would otherwise keep in their head.

uninstall.sh removes what install.sh put down and stops there. What you have
written is yours: the settings and the dictations, the meetings and the
recordings survive a plain run, and --purge is the word that deletes them. It
prints what that would cost first, how many dictations, how many meetings, how
much audio, since a count is the thing that makes the sentence real, and
without a terminal to ask it refuses rather than assuming --yes. Only our own
symlink goes; a file of that name somebody else put there is left alone. The
shortcuts are handed back to `dikte shortcut remove` rather than unpicked from
KDE's file here, because that is the half that knows whether they went into
kglobalshortcutsrc or into GNOME's gsettings.

update.sh asks whether anything is waiting before it complains about anything
else, because an unfinished afternoon in the working tree is nobody's problem
on a day when nothing has been published. If something is waiting and you do
have edits of your own, it stops and says so rather than choosing for you. The
merge is --ff-only: an update is somebody else's commits arriving, never a
merge a script decided to make on your behalf.

Then install.sh runs again, since an update can add a dependency or move a
file, and it is told which keys you chose so that it puts those back instead of
its own defaults. They are read from the settings rather than from the
desktop's file, that being the one place they mean the same thing on KDE and on
GNOME, and a key you had turned off is passed back as the empty string rather
than quietly becoming the default again. Finally the running instance is
restarted, because it is still holding the code from before the pull.
2026-08-01 18:57:50 +03:00
yusufipk 3486892ec6 Give throwing a recording away a key of its own
Stopping a recording is the step there is no taking back. It is what sends the
audio off, and a moment later the sentence you did not mean to dictate is in
the clipboard and pasted into whatever window you were typing in. The tray menu
was the only way out, and by the time it is open the recording has already
gone. Discarding needed to be as quick as starting, which means a key.

Ctrl+Alt+Space rather than Escape: the combination the recording started with,
one modifier along, so the two are one gesture with a modifier between them.
Escape is the obvious choice and the wrong one, because it belongs to whichever
window has focus, and while you are talking something else usually has it. It
works on a dictation and on a command for the agent alike, since the one you
want to take back is the one that is running.

That made four global shortcuts, and four is the number at which three copies
of the same forty lines stop being a coincidence. The command line already had
a table of them; hotkey.py now holds it, and the settings window reads it too,
so a shortcut is a row rather than a combination box, two buttons, a status
label and three methods written out again. The window no longer takes the
commands to run as arguments either, because ipc.command_for already knows them
from the verb. Adding the fourth key is what this buys: one line in the table
and one call per row.

Both places a key can live are told about it. The listener catching the press
and the KDE shortcut arriving behind it go through the same echo guard the
toggle already had, so the two are one discard rather than two, and the tray
calls the inner method as it already did for the toggle. Ctrl+Space and
Ctrl+Alt+Space land on the same evdev key code, so there is a test for the
modifier matching that keeps them apart.

install.sh takes the second key as a second argument and refuses to register
two of the same. It hands both to `dikte shortcut install` rather than writing
kglobalshortcutsrc itself, which is what makes it work on GNOME, and what
finally puts the chosen key in the settings as well: the built-in listener
reads it from there, so a key written to only one of the two places was a key
that half worked.

Left empty in the settings window the discard key stays empty, unlike the
dictation shortcut which falls back to Ctrl+Space: a recording can always be
thrown away from the tray menu, so there is nothing to guarantee here.
2026-08-01 18:57:50 +03:00
Yusuf İpek 1e959178b4 Merge pull request #6 from muzafferemre06/feature/groq-transcription
Add Groq as a transcription provider
2026-08-01 18:32:46 +03:00
yusufipkandMuzaffer Emre d04efe235a Put the three providers in a table, and test the request rather than the list
Reworked on top of master. The Groq request itself needed nothing beyond a key,
a base URL and a model id, but two providers fit in an if and an else where
three do not, and each one was written out four times: in transcribe_target(),
in the key rows of the settings window, in save and in load.

config.TRANSCRIBERS holds them now, one row each: the name the user sees and
the three settings that keep its key, its endpoint and its model. The variable
an empty key falls back to is the name of its setting, shouted. The provider
box, save, load, `dikte models --provider` and `dikte test-key` all read that
table, so a fourth provider is a row and a default rather than a branch in five
files. The key field, its Test button and its answer line are built once and
the three signals became one that carries which key was asked about.

The tests moved into the files of the modules they cover and check what a
provider actually changes: that the request goes to api.groq.com with the right
model and fields, that the glossary now reaches everything except OpenRouter,
that a Groq error says Groq, and that the settings window carries the key and
the model there and back. GROQ_API_KEY is cleared for the test run like the
other two, so a developer who has one does not send a test to the network.

Co-authored-by: Muzaffer Emre <[email protected]>
2026-08-01 22:09:41 +07:00
yusufipk 22edd2f247 Merge master into feature/groq-transcription 2026-08-01 22:09:01 +07:00
yusufipkandfirat 3bd8c1ad27 Add a Mac as the third system, beside Wayland and X11
Dikte already chose its clipboard programs once instead of in every
function; macOS joins that table rather than adding a branch to each one.
A Mac copies through pbcopy and presses Cmd+V straight into CoreGraphics,
records through AVFoundation, and asks Carbon for its global shortcuts.

The three tables are paste.Desktop, audio.Sound, and the pair of
predicates in hotkey.py. Each reads sys.platform inside the chooser, so a
test can stand somewhere else: 697 of the 737 tests now run on any
machine, the Wayland and X11 halves included, and the suite passes whole
whichever system it is run on.

Two things a Mac does not have needed saying rather than pretending:
there is no shortcut registry to install into, so Settings offers no
Install button and the listener is the mechanism instead of a fallback;
and nothing is offered as the sound the speakers are playing, so a
meeting needs BlackHole or Loopback and says so. The KDE-only labels
around them were already wrong on GNOME, and now name whichever desktop
is there.

Co-authored-by: firat <[email protected]>
2026-08-01 21:28:58 +07:00
Yusuf İpek 676664ea74 Merge pull request #5 from muzafferemre06/feature/ubuntu-x11-gnome
Support GNOME X11 and PulseAudio without changing Wayland
2026-08-01 16:58:18 +03:00
yusufipk 5c429ab077 Ask parec for sound as it arrives, not two seconds of it at a time
Left alone, parec buffers about two seconds and then hands the lot over at
once. Measured against the chunk the level meter reads:

    parec, as it was          longest gap  2005 ms
    parec --latency-msec=64   longest gap    86 ms
    pw-record                 longest gap   129 ms

On a machine that has parec, which is every PulseAudio one and every PipeWire
one with the compatibility service, the waveform stood still and then jumped,
looking like a microphone that was not picking anything up. The recording
itself was fine, but the tail of one can be lost that way too: stop() ends the
process, and whatever is still held back goes with it.

The number asked for is the meter's own chunk, which is the unit the rest of
this file is measured in.
2026-08-01 20:52:43 +07:00
yusufipk 3ead6eb1b5 Merge remote-tracking branch 'origin/master' into feature/ubuntu-x11-gnome 2026-08-01 20:43:26 +07:00
yusufipk 45a064e545 Tell a recorder that died from one that was asked to stop
The pump now says when the capture ended with nothing captured, which is worth
saying: parec refusing the device looks like silence otherwise. But stop() ends
it the same way, so a recording shorter than 0.3 s raised that alarm first and
"Recording too short" second, sending the user after a sound server that is
fine. It follows the flag MeetingRecorder already carries for this.

test_desktop_compat.py moves into the files for the modules it covers, so a
test is where the next person looking at that module will find it.
2026-08-01 20:34:15 +07:00