Commit Graph
202 Commits
Author SHA1 Message Date
oztturkandClaude Opus 5 1ffc3cff9d Let an error body that is an array still be read
Google answers some failures with a JSON array holding the object every
other provider sends on its own. _extract_error called .get() on it and
raised AttributeError, which is not the ApiError every caller is holding,
so a 503 from Google took the whole dictation down instead of pasting the
raw transcript with the failure shown beside it.

Found by dictating against a Google AI Studio outage:

  HTTP Error 503: Service Unavailable
  AttributeError: 'list' object has no attribute 'get'

It runs while an exception is being raised, so it now ends in a string
whatever arrives.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 16:29:41 +03:00
oztturkandClaude Opus 5 1812461612 Clean up on Google AI Studio, or on Antigravity
OpenRouter's free tier rate-limits and carries no free Gemini model, and
cleaning up through Claude Code costs a fixed few seconds because it opens
a whole CLI session to drop three "uh"s. Google's own free tier suits a
short, frequent request, and its OpenAI-compatible endpoint answers
/chat/completions, so cleanup there is one request and the same code path
OpenRouter already takes.

The one thing that is not shared is the thinking level. Google reads
OpenAI's flat reasoning_effort rather than OpenRouter's object, and "none"
is how thinking is turned off, so it is sent rather than skipped: a Flash
model left to think spends exactly the second this provider was chosen to
save. Its top two rungs land on "high", which is as far as Google goes.

Speech to text stays where it was. That endpoint has no
/audio/transcriptions behind it, audio only goes in as base64 inside a
chat message, and what comes back has none of the segment times a subtitle
file or a meeting transcript is built out of.

Antigravity joins as well, on cleanup and as an agent. It is a CLI like the
other two and costs the same session, so it is here for people who already
pay for it rather than as an answer to the speed. It takes neither an empty
tool list nor a read-only sandbox, and cleanup.py now says so plainly
instead of implying parity; what it gets is a project of its own, the home
directory, and its slash commands off.

Three things were already wrong and are fixed on the way past, because the
new providers walk the same paths: doctor raised KeyError on the local
model, whose executable is ""; the history recorded Claude's model whoever
answered; and every agent row read "asked Claude".

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 16:23:04 +03:00
sudoeren 52880bd81a Document the OpenCode Go provider 2026-08-25 21:35:04 +03:00
sudoeren e6b3fcf48f Reach OpenCode Go from the command line
test-key accepts opencode and reports the model count its /models
endpoint offers, ask --provider accepts it and maps --model to its own
setting, doctor reports its key for cleanup on it, and config list
masks the key like the other two.
2026-08-25 21:05:23 +03:00
sudoeren 3c336e7178 Offer OpenCode Go in the settings window
A key row under Keys, a model box in the cleanup tab and a box in the
agent tab, each with its own model list of the models OpenCode Go
serves over /chat/completions. Fetch model list reads whichever cleanup
provider is on screen, and the Turkish strings cover the new rows.
2026-08-25 21:05:21 +03:00
sudoeren 0a6c6128a2 Add OpenCode Go as a cleanup and agent provider
OpenCode Go serves open coding models from an OpenAI-compatible endpoint. It cannot transcribe, so it joins
the two LLM jobs: transcript cleanup and the plain-chat agent. The key
falls back to OPENCODE_API_KEY, and api.chat() stops hardcoding the
OpenRouter name so the same call serves both.
2026-08-25 21:05:18 +03:00
yusufipek be09cc3ac6 Let the next dictation start while the last one is still working
Pressing the shortcut while a transcript was being transcribed or cleaned up
did nothing, and the thought you had while waiting was lost. The microphone is
free the moment a recording stops, so the next dictation can now be spoken at
once; the pipeline queues it and each one is finished, pasted and reported in
the order it was spoken. The corner indicator stays with the recording under
way rather than being wiped by the previous run's progress, and a stop that
lands behind an unfinished run says it is waiting its turn.
2026-08-25 14:52:17 +03:00
yusufipek 0b06d6d16d Say that a model can be typed in, everywhere one can
Every model box has taken a typed name all along, but nothing said so, and a
closed-looking list reads as the whole choice. The Claude Code and Codex boxes
now carry a tooltip saying the list is a starting point, not a fence. Claude
gets no fetched list of its own: its CLI has no catalog command, and the
aliases it takes (sonnet, opus, haiku, fable) already follow the newest model
of each line.
2026-08-25 14:48:59 +03:00
yusufipek e9488a51a4 Ask Codex itself which models it offers
The Codex model boxes carried a hand-written list, which was already out of
date. The settings window now asks `codex debug models` for the catalog the
CLI's own picker reads, off the interface thread, and refills both boxes with
it; the built-in list is only what is on screen until Codex answers, or when
it is not installed at all.
2026-08-25 14:42:19 +03:00
GökhanandClaude Opus 5 0f30c852ab Carry the remembered front past the Windows paste as well
The macOS press learned a third argument, the application that was in front
when the recording started, and every backend is handed it. SendInput takes
no such thing: nothing on Windows moves the front while a recording runs, so
the process id arrives and is left alone.

Without this the Windows backend refuses the call the shared press makes, and
the five key tests in tests.test_paste.Windows stop at a TypeError.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-25 00:29:26 +03:00
Gökhan 901e57e4d0 Merge master into the macOS front branch 2026-08-25 00:16:20 +03:00
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
github-actions[bot] 3663c7fd57 Dikte 1.0.2 v1.0.2 2026-08-22 09:33:21 +00:00
Yusuf İpek df99456cde Merge pull request #51 from yusufipk/update-check
Say when a newer release is out, and stop there
2026-08-22 12:29:43 +03:00
yusufipek 084c48151d Say when a newer release is out, and stop there
A check on the releases page once a day: at start, on a timer while Dikte
runs, from the General tab on demand, and from `dikte update` at a terminal.
What it finds goes in the tray menu and in one notification per version, and
opens the release page.

Nothing is downloaded and nothing is installed. The four downloads are
installed four different ways and three of those belong to the platform: a
Mac bundle cannot rewrite itself while it is running, the Windows setup has
an uninstall entry of its own, an AppImage is a file kept wherever its owner
keeps it, and a checkout is updated with git. Being wrong about any one of
them means an installation somebody has to repair by hand.

Versions are compared by their numbers alone. A build off master carries the
released number with its commit after it, and that build is ahead of the
release it names rather than behind it; read as a version suffix, every
nightly would be told to go back to a release it had already passed.

The clock lives in its own file rather than in the settings, since a check
runs while the settings window may be open and a background write into
config.json is what would overwrite whatever it holds.
2026-08-22 10:34:59 +03:00
github-actions[bot] 6bf5c8b349 Dikte 1.0.1 v1.0.1 2026-08-22 05:20:11 +00:00
Yusuf İpek 2927e15833 Merge pull request #49 from yusufipk/long-file-chunks
Cut a long file into chunks a hosted request can outlive
2026-08-20 12:41:18 +03:00
yusufipek f55e201785 Cut a long file into chunks a hosted request can outlive
An hour and a half of speech came back as "OpenRouter: HTTP 502: The
operation was aborted due to timeout", every time. The upload limit was
the only thing deciding where the file was cut, and mp3 at 48 kbps
reaches 24 MB after an hour, so a 90 minute file became two chunks and
the first one was 63 minutes of audio in a single request. Nothing
between here and the model stays on the line that long.

A chunk is capped at fifteen minutes now, whatever it weighs, and the
call is finally handed a timeout of its own: it was going out on the 300
second default sized for a dictation, which the same chunk would have
hit first anyway.

The other half is not throwing the run away when one request fails.
ApiError carries whether a second try can fix it, which is true of the
statuses a gateway raises itself and of a dropped connection, and false
of a rejected key. A chunk is asked for three times, waiting five then
ten seconds, with the Stop button still able to get through. If it does
fail in the end, what was already heard goes to the output box rather
than the bin, with the status line saying where it stops.
2026-08-20 12:27:42 +03:00
Yusuf İpek 0a70484d84 Merge pull request #48 from yusufipk/windows-setup
Publish a Windows setup beside the AppImage and the disk image
v1.0.0
2026-08-19 15:57:19 +03:00
yusufipek d0c93e5a17 Let a stalled apt mirror fail the job rather than hold it
Both Linux test jobs on this branch sat an hour in apt-get, on runners
where the same step takes seconds, and a job with nothing to time it out
holds the pull request red-free and unfinished until the six hour limit.
Three retries and a twenty second timeout give apt another mirror, and
five minutes on the step gives the job an end.
2026-08-19 12:36:16 +03:00
yusufipek 29643f26b3 Keep the two Windows executables from being one file
Windows matches a filename without regard to its case, so Dikte.exe and
dikte.exe were one file in the installation directory: PyInstaller wrote
the console build second and that is the only one that landed. Every
launcher then opened a console window behind the tray, and closing it
killed the application.

The console one is dikte-cli.exe now, which the setup's shim names. The
build reads the PE subsystem of both afterwards, since a filesystem that
keeps the two names apart is exactly what the check would run on
otherwise.
2026-08-19 11:11:30 +03:00
firat 0be64c2ef9 Wait for macOS focus restoration to land 2026-08-18 18:26:15 +02:00
yusufipek 23d56acfb5 Publish a Windows setup beside the AppImage and the disk image
The releases page had nothing for Windows, so the only way in was a
checkout, a Python and a pip install. What goes out now is one setup
program per release: PyInstaller's directory, the pinned ffmpeg the
disk image already uses, and Inno Setup around both. It installs for
the account alone, so no administrator is asked for.

Two executables over the one program there, because a windowed one on
Windows has no standard output at all: Dikte.exe for the Start Menu and
dikte.exe for the terminal, sharing everything they carry. The icon is
drawn by Dikte itself into an .ico, the way the Mac's .icns and Linux's
PNGs already are, so there is still no image file in the repository.

Starting at sign-in is a registry value rather than a Startup shortcut,
which is what lets the setup program, the uninstaller and
`dikte integrate` all mean the same thing: the wizard asks once, and
typing the command changes the answer later.

The three builds move into build.yml, which release.yml now calls
instead of holding its own copy, and which a pull request touching the
packaging runs on its own. A broken build is then a red pull request
rather than a failed release.
2026-08-18 16:08:47 +03:00
Yusuf İpek d24da7df52 Merge pull request #44 from benfirad/agent/remove-integration-wrapper
Remove macOS command wrapper during integration cleanup
2026-08-18 15:40:47 +03:00
Yusuf İpek e7e30a31dd Merge pull request #30 from huseyin-emre-tigci/windows-support
Add Windows support
Co-authored-by: Hüseyin Emre Tığcı <[email protected]>
2026-08-18 15:00:07 +03:00
senolsunandClaude Fable 5 c7b14ff84e Rebuild the settings window when the language changes
Saving already switched the language everywhere strings are made at the
moment they are shown: the tray is rebuilt, the indicator and the message
box translate as they speak. The settings window is the one place written
once, at construction, so the window that took the new language was the
one place still showing the old one, and a tooltip asked for a restart.

Now the window remembers the language it was built in, and a save that
changed it has the app replace the window: a fresh one comes up where the
old one stood, on the same tab. The restart tooltip goes, having nothing
left to excuse.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 00:27:09 +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
firat 45e7af474a Remove downloaded Mac command during integration cleanup 2026-08-16 17:18:46 +02:00
GökhanandClaude Opus 5 45b18722be Keep the front where the dictation started on macOS
Recording goes through ffmpeg's avfoundation input, and opening a capture
session there brings the process that did it to the front. ffmpeg is a
child of Dikte with no bundle of its own, so macOS credits the move to
Dikte: the window the user was dictating into goes inactive, its caret
stops, its title bar greys out, and the Cmd+V at the end of the run lands
somewhere other than the document it was meant for. Measured with a
TextEdit document in front:

    press the shortcut          front = TextEdit
    recorder.start returns      front = TextEdit
    89 ms later                 front = Dikte

Nothing about the capture session can be asked not to do this. Starting
ffmpeg in its own session, and clearing __CFBundleIdentifier from its
environment, were both tried and both measured to make no difference, so
it is undone instead: the application in front is noted before the
indicator goes up, and a short watch puts it back the moment Dikte takes
the front. Measured at 99 ms from the moment it is taken, against the
title bar staying grey for the whole dictation before. All three ways in
do it, dictation, agent and meeting, since all three open the same
capture.

The indicator had a share of the same problem and needed AppKit for it
too, so mac_window.py carries both. An NSPanel is hidden by the system
the moment its application stops being the active one, which for a
dictation indicator is immediately, and ordering one to the front brings
its application with it unless it carries the nonactivating style bit.
Neither is reachable through Qt.

The runtime is loaded in _appkit() rather than at import, the way paste.py
loads its frameworks in _macos_api(), so the module imports on a machine
with no AppKit and the tests below run there as well: 52 new tests, none
of them skipped anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 17:37:31 +03:00
yusufipek aef33585c1 Say where the Windows install comes from now that there are downloads 2026-08-16 15:55:52 +03:00
yusufipek 180ddac37e Let the Windows job read the merged suite
The merge put a Windows runner under tests master wrote for two systems,
and 21 of them fell over on it. Two were the quoting: `command_for` goes
through shlex now, so a Windows path comes back in quotes, and the two
tests that read the command as a string were reading it as a Linux one.
They ask through the same join instead.

The other nineteen are `integrate.py`, which writes the menu entry and
the login item a downloaded build installs for itself. There are two
downloads, an AppImage and a disk image, so the module has a Linux half
and a macOS half and nothing a Windows host would run: its tests hand
the fake home over in $HOME, which Windows does not read, and compare
paths that start at the root. They carry `@posix_only`, which comes off
again the day there is a Windows build to integrate.
2026-08-16 15:54:44 +03:00
yusufipek 80ff4ecade Merge master: the downloadable builds
Three files disagreed. The test workflow gained a job on either side, so
both stay: the Mac now parses the release and packaging scripts as well,
and Windows keeps its own job below.

`restart` and `launch_gui` both start Dikte again, and master moved that
argv into `ipc.launcher()` because a packaged build has no `__main__.py`
to name. Windows still cannot use execv there, so the detached start it
needs now takes what the launcher hands it rather than spelling the
interpreter and the script itself.

The test counts in CONTRIBUTING are the suite as it stands after the
merge: 1104 of 1147 run anywhere, and the 43 left are the Linux ones.
2026-08-16 15:44:40 +03:00
Yusuf İpek 569fe7cea8 Merge pull request #42 from yusufipk/more-ca-paths
Carry the two paths the RHEL family uses as well
2026-08-16 15:35:30 +03:00
yusufipek 4ca66b1e03 Carry the two paths the RHEL family uses as well
The list was four of the six that Go's crypto/x509 and curl both carry.
The missing two are the bundle under /etc/pki/ca-trust, which is where
RHEL 7 and CentOS keep it, and OpenELEC's. Neither is likely and both are
one line.

The rest was checked in containers rather than assumed: the first entry
answers Debian, Ubuntu, Arch, Gentoo, Fedora and Alpine, and openSUSE is
the only common distribution that needs a different one.
2026-08-16 15:29:29 +03:00
Yusuf İpek e7d0a37f79 Merge pull request #41 from yusufipk/system-certificates
Look up the machine's certificates rather than the build machine's
2026-08-16 15:26:26 +03:00
yusufipek 4cec05fa49 Look up the machine's certificates rather than the build machine's
A build carries the OpenSSL of the machine it was built on, and that
OpenSSL has one directory compiled into it as the only place it looks for
certificates. For an AppImage built on Ubuntu that is /usr/lib/ssl, which
Arch, Fedora and openSUSE do not have, so on any of them every HTTPS
request fails with CERTIFICATE_VERIFY_FAILED: transcription and cleanup
report it as a rejected key, and the model downloads fail too.

Ask the machine instead, from the list curl and Go use, and only when the
build's own answer turns out not to exist. The store on the machine
rather than a copy carried along, because a copy goes stale as roots are
rotated and would ignore a certificate somebody added themselves. Anybody
who has already set SSL_CERT_FILE is left alone.
2026-08-16 15:24:33 +03:00
Yusuf İpek f247752bc1 Merge pull request #39 from yusufipk/downloadable-builds
Build an AppImage and a disk image, and publish them
2026-08-16 15:15:24 +03:00
yusufipek 01c91eb425 Resolve the test's home before comparing paths to it
A Mac keeps temporary directories under /var, which is a symlink to
/private/var, and target() resolves what it is handed because a bundle
reached through a symlink is still that bundle. So the login item named
/private/var/... while the test held /var/..., and the two spellings of
one path did not match.
2026-08-16 15:09:40 +03:00
yusufipek 7e510f8b35 Build an AppImage and a disk image, and publish them
Installing meant cloning the repository and running a shell script, which
is a fair ask of somebody who already has a terminal open and no ask at
all of anybody else. The releases page now carries an AppImage and a disk
image per Mac architecture: a push to master rebuilds a rolling "latest",
a v* tag publishes a version and leaves it there, and the Run button in
the Actions tab raises the number by running scripts/release.sh, which is
the same script and not a second copy of what it does.

Two things in the application had to give for that. A build has no
__main__.py on disk, and an AppImage is mounted somewhere new every run,
so the command a shortcut is registered with cannot go on being this
interpreter and this file; ipc.launcher() answers with the AppImage or
the bundle instead. And a build carries its own libstdc++, which every
process it starts inherits through LD_LIBRARY_PATH and none of them can
live with: ffmpeg, ydotool and wl-copy are the distribution's binaries
built against the distribution's libraries, and AppImageLauncher, which
is what starting the AppImage again goes through, refuses outright.
integrate.py puts that variable back before anything else runs.

Nothing installs itself over an installation that is already there.
install.sh's menu entry, install-mac.sh's login item and the desktop file
AppImageLauncher writes are each recognised and left alone, so trying a
download once does not quietly move the machine onto it. `dikte
integrate` is how you ask for it outright, and --remove takes it back.

The disk image carries an ffmpeg, pinned and checksummed, because macOS
records through one and ships nothing like it. It is signed ad-hoc and
not with an Apple certificate, so a first launch is refused until Open
Anyway and the permissions are asked for again after each update; both
READMEs and the release notes say so.
2026-08-16 15:03:44 +03:00
Yusuf İpek 061dc5451b Merge pull request #38 from yusufipk/cleanup-language
Keep the transcript in the language it was spoken in
2026-08-16 14:26:16 +03:00
yusufipk 5236b27cb3 Keep the transcript in the language it was spoken in
The cleanup prompt is picked by interface language, so a Turkish
interface hands a Turkish system prompt to the model and English
dictation comes back translated. The rule against that was the fifth
line of a DO NOT list, which is not where a model looks when the whole
prompt around it is written in one language.

Move it up under the job, in both cleanup prompts, and say plainly that
the language the rules are written in decides nothing.
2026-08-16 14:24:00 +03:00
yusufipek bac988e0e6 Ask for the entry point by its parts, not by a slash
The assertion spelled the path with a forward slash, which is not the separator
Windows joins with. The Windows job this branch adds is the first to run it
there.
2026-08-16 14:13:12 +03:00
yusufipek b186f7fde2 Merge master: the modules moved into a package
Every file this branch touches moved into dikte/, so the merge is mostly the
rename following the edits. What needed a hand:

hotkey.py: master replaced the _macos()/_gnome() pair with one backend()
chooser, and this branch had added _windows() to the pair. Windows is a fifth
value of the chooser now, and everything that used to ask "macOS or Windows?"
asks backend() instead. The key is held by the running process there, so
installs_shortcuts() and shortcut_needs_restart() are both false for it, and
desktop_name() says Windows.

install.ps1 and the Windows README name dikte/__main__.py, the entry point the
Linux and macOS installers were pointed at in the same commit. The Start Menu
entry, the autostart entry and the dikte.cmd shim all come off one $entry
variable.

settings_ui.py: the shortcut tab now has a Windows sentence of its own, with
the Turkish for it. Falling through to the branch master wrote for a desktop
with no registry would have told a Windows user to check /dev/input. Nothing
covers that branch: there is no Windows Settings test class, the way there is
one for macOS.

CONTRIBUTING: the chooser it names is backend() now, and the test count is the
merged one, 1067 of 1110 running anywhere.
2026-08-16 14:10:28 +03:00
Yusuf İpek ea476665b8 Merge pull request #37 from yusufipk/package-layout
Put the modules in a package and the scripts in a folder
2026-08-16 14:02:39 +03:00