10 Commits
Author SHA1 Message Date
huseyin-emre-tigciandClaude Fable 5 e8147f49f8 Say true things in every language, and stop paying twice
doctor judged the local provider by the API key it does not use, so a
fully local machine always saw a red mark, and it crashed outright with
local cleanup picked; both lines now ask readiness. config list printed
the Groq key in plaintext while masking the other two. The one subprocess
decoded with the locale codepage gets its UTF-8 back, so a Turkish
filename cannot hang a file transcription, and a redirected stdout on
Windows replaces what it cannot encode instead of failing after the work
succeeded. meeting-cancel stops advertising a --wait the server never
honoured. "you" and "bye" leave the hallucination list: people dictate
them. The minutes stage failing no longer burns the transcription
checkpoint, and an untouched meeting-length dial no longer rewrites a
value the command line set in seconds. A prompt box compared against the
wrong language's default after a switch no longer fossilizes the old
default as a custom prompt.

The 67 strings of the local-model box, the whole first-run screen of the
shipped default, get their Turkish. The hub cache moves to the platform's
cache directory instead of ~/.cache on every system; the old directory is
a few orphaned kilobytes with a six-hour shelf life. The last NO_WINDOW
spellings collapse into the constant paths already carries, one
windowed-executable lookup, one session-file reader, one install-record
reader, one download progress signal carrying its destination, and the
KDE conflict scan loses the branch its other branch already covered.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 6f79e93d53 Let no failure eat a dictation, and none strand the state machine
The recorded WAV was deleted in a finally that did not care why the run
ended, so a whisper server being down destroyed the only copy of the
user's speech; a failed run now keeps its audio in the recordings
directory and names the path in the error. The history was written only
after the paste, and a failed key press restored the previous clipboard
over the fresh transcript: text not pasted, not on the clipboard, not in
the history, audio gone, all from one refused key. The history now comes
first, a failed press is a warning the row is amended to carry, and the
clipboard keeps the text the user has to paste by hand. Kept recordings
no longer overwrite each other inside one second, the keep_audio move
failing no longer falls through to the delete, and the history row
records whether cleanup actually ran rather than what the dictation gate
implies about an ask.

In the application: a recorder that failed to start emitted its error
synchronously and start() then wrote RECORDING over the handler's IDLE,
one more key press away from a BUSY nothing would ever end; start now
checks the recorder is running, like start_meeting always has. The new
died signal ends the run properly and transcribes what was captured. A
--paste override armed by a request that no-opped stopped haunting some
later unrelated run, and it dies with a cancelled or failed one. The
toggle, ask and pause debounce timers are per action, so a pause right
after a toggle is a pause and not a duplicate. Waiters outstanding at
quit or restart are settled instead of being read back as "the instance
is too old". The listing warm-up and the one-per-press backend lookup
move off the key press.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 69194186c8 Kill the agent's whole tree, and stop reading its errors as prose
The claude and codex CLIs spawn shells to do their work, and killing only
the direct child left those shells holding the pipes: the stdout loop
never saw EOF, so the run hung forever with the watchdog already fired,
and cleanup's subprocess.run could hang inside the stdlib the same way
after its own timeout. Both now run the CLI with its output in files
rather than pipes and put the whole tree down on a timeout, taskkill /T
on Windows and a process group everywhere else. Whether a dead resume
means "start a fresh conversation" was decided by English substrings of
stderr, which a localized CLI never says; a resumed run that exits
nonzero with no answer now retries fresh once, whatever the words were.
Recognised API trouble is the exception: a spent quota, a signed-out CLI
or a dead network is not the session's fault, and is reported as itself
rather than retried into a second copy of the same failure.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 afc8ffd992 Hold the settings and the two indexes the way files are lost
history.jsonl and meetings.jsonl were rewritten read-modify-replace from
two threads with nothing between them, so a dictation landing during a
trim, or a meeting status landing during a delete, was silently gone; one
lock now wraps every touch of either file. The config rename gets a flush
and fsync in front of it, because a rename that survives a power loss
ahead of its data is an empty settings file, and a corrupt file is set
aside as config.json.broken instead of being quietly replaced by defaults
on the next save, API keys and all. Every replace retries briefly on
Windows, where an antivirus or a sync tool holds a fresh file for a beat
and one PermissionError out of a Qt slot takes the whole application
down.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 197f5385ee Never let an install or a sweep destroy what still works
install_program deleted the working install before the download had even
started, so a network failure, or the running server's own locked DLLs,
left "whisper.cpp is not installed" behind on a machine where it had
been. The order is now: download, unpack beside, stop the server whose
binary lives there, swap, so the outage is the swap and not the whole
transfer, and a download that fails never takes the server down at all.
A re-downloaded model the server still holds open no longer costs the
finished download; the .part survives and the message says who is holding
the file.

sweep() forgot the pid file before verifying or killing, so one transient
error orphaned a loaded model forever; it verifies, kills, then forgets,
and a verification that could not run leaves the file for the next start.
On Windows the ownership check was the executable's basename, which a
recycled pid could satisfy with somebody else's server; it is the full
image path now, read into a buffer that grows past 260 characters.
stop() takes the launch lock, so stopping during a start kills the server
the start was making rather than missing it, and _forget only removes a
record that is still its own. The relaunch retry stopped reading English
out of the log tail: the retryable failure is a child that exited without
ever listening, and that is what is tested, along with the child still
being alive once its port answers.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 420cda9376 Keep the recorder alive through the ways a capture dies
Four holes in one class, found by a review the last one paid for. The
capture's stderr went to a pipe nobody drained, so a chatty ffmpeg could
fill it and freeze the recording; it goes to a file now, the way the
meeting recorder always did. A capture dying with audio already buffered
ended the pump in silence, the clock counting over a dead microphone;
there is a died signal now, and the application transcribes what was
caught. A pump that outlived its two-second join could write stale audio
into the next recording and kill the next recording's process; each run
now owns its objects and a token. Short pipe reads were each billed a
whole chunk in the silence math, overstating speech; the pump reads exact
chunks, as meetings do.

Smaller ones alongside: write_wav failing no longer strands the state
machine; a deliberate stop no longer reports ffmpeg's interrupt code as
"Nothing was recorded: ffmpeg -> 255"; kills are reaped so no message
says "exit code None"; the pw-record probe is paid once per process; the
RMS loop uses sumprod where Python has it; the pactl and pw-record
listings decode as the UTF-8 they are. paths gains the NO_WINDOW constant
this module re-exports, the one spelling every subprocess site after it
shares.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:17:22 +03:00
huseyin-emre-tigciandClaude Fable 5 6f601ab969 Let a second copy yield to the instance already running
listen() was the whole of the single-instance check and it cannot be one:
a Windows named pipe takes a second server on the same name rather than
refusing it, and everywhere else removeServer() first takes the live socket
away from the instance holding it. Starting Dikte over a running Dikte then
left two whole copies up, two tray icons and all, and the newer one's
sweep() killed the whisper the older one was answering dictations with. On
a machine that sleeps instead of logging out, that is one Start Menu click
away, and it cost a real dictation before it was understood.

A QLockFile in the data directory closes the race on all three systems,
taken before the QApplication is even built, and behind it the probe is the
side-effect-free status verb: the Settings window opens as the sign of life
only when a bare second start deliberately asks for it, not as a byproduct
of a probe racing a forwarded toggle. The two Windows relaunch dances
collapse into one ipc.respawn.

The checkout installer and the packaged setup each kept an autostart the
other could not see, so a machine that tried both started two copies at
every sign-in: each autostart now removes the other's entry, the silent
every-start repair backs off from a Run value whose target still exists,
and each uninstaller deletes the shared dikte.cmd only when the shim
names its own install.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 23:09:05 +03:00
huseyin-emre-tigciandClaude Fable 5 11a4058e16 Let the device listing match ffmpeg 8's bracket prefix
The dshow parser pinned the bracketed prefix to the `[dshow @ ...]` the
older builds print, and ffmpeg 8 writes `[in#0 @ ...]` there instead: on a
current install every line fell through the anchors, the device list came
back empty, and a dictation with no microphone picked could not start at
all. The prefix is now any bracketed tag, which the anchors and the quoted
name still keep apart from the error lines. Pinned with a listing taken
verbatim from ffmpeg 8.1.2 on Windows 11, third kind marker `(none)`
included.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 01:02:42 +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