diff --git a/CMakeLists.txt b/CMakeLists.txt index e79039c..894307f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,8 +132,17 @@ install(TARGETS kastword RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT K install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.github.shape_machine.Kastword.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications COMPONENT Kastword) install(FILES data/io.github.shape_machine.Kastword.metainfo.xml DESTINATION share/metainfo COMPONENT Kastword) -install(FILES README.md LICENSES/GPL-3.0-or-later.txt - DESTINATION share/doc/kastword COMPONENT Kastword) +install(FILES README.md DESTINATION share/doc/kastword COMPONENT Kastword) +install(FILES docs/DEVELOPMENT.md docs/AUTOMATIC_PASTE.md + DESTINATION share/doc/kastword/docs COMPONENT Kastword) +install(FILES + screenshots/01-offline-dictation.png + screenshots/02-speech-models.png + screenshots/03-audio-input.png + screenshots/04-settings.png + DESTINATION share/doc/kastword/screenshots COMPONENT Kastword) +install(FILES LICENSES/GPL-3.0-or-later.txt + DESTINATION share/doc/kastword/LICENSES COMPONENT Kastword) if(KASTWORD_FETCH_DEFAULT_MODEL) install(FILES "${KASTWORD_DEFAULT_MODEL}" DESTINATION share/kastword/models COMPONENT Kastword) endif() @@ -248,6 +257,10 @@ if(BUILD_TESTING) kastword_enable_sanitizers(kastword_screenshot_platform_test) add_test(NAME screenshot-platform COMMAND kastword_screenshot_platform_test) + add_test(NAME documentation-links + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check-documentation-links.sh + ${CMAKE_CURRENT_SOURCE_DIR}) + add_test(NAME translation-catalog COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-translations.sh) set_tests_properties(translation-catalog PROPERTIES diff --git a/Makefile b/Makefile index 345ed9d..de5b019 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,17 @@ install-smoke: ensure-configured "$$smoke_prefix/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml"; \ test -f "$$smoke_prefix/share/locale/x-test/LC_MESSAGES/kastword.mo"; \ test ! -e "$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \ + test -f "$$smoke_prefix/share/doc/kastword/README.md"; \ + test -f "$$smoke_prefix/share/doc/kastword/docs/DEVELOPMENT.md"; \ + test -f "$$smoke_prefix/share/doc/kastword/docs/AUTOMATIC_PASTE.md"; \ + test -f "$$smoke_prefix/share/doc/kastword/screenshots/01-offline-dictation.png"; \ + test -f "$$smoke_prefix/share/doc/kastword/screenshots/02-speech-models.png"; \ + test -f "$$smoke_prefix/share/doc/kastword/screenshots/03-audio-input.png"; \ + test -f "$$smoke_prefix/share/doc/kastword/screenshots/04-settings.png"; \ + test -f "$$smoke_prefix/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt"; \ + ./tools/check-documentation-links.sh "$$smoke_prefix/share/doc/kastword"; \ + legacy_license="$$smoke_prefix/share/doc/kastword/GPL-3.0-or-later.txt"; \ + cmake -E touch "$$legacy_license"; \ find "$$smoke_prefix" -type f -print > "$$installed_files"; \ user_model="$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \ cmake -E make_directory "$$(dirname "$$user_model")"; \ @@ -97,7 +108,14 @@ uninstall: "$(PREFIX)/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml" \ "$(PREFIX)/share/locale/x-test/LC_MESSAGES/kastword.mo" \ "$(PREFIX)/share/doc/kastword/README.md" \ - "$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt" + "$(PREFIX)/share/doc/kastword/docs/DEVELOPMENT.md" \ + "$(PREFIX)/share/doc/kastword/docs/AUTOMATIC_PASTE.md" \ + "$(PREFIX)/share/doc/kastword/screenshots/01-offline-dictation.png" \ + "$(PREFIX)/share/doc/kastword/screenshots/02-speech-models.png" \ + "$(PREFIX)/share/doc/kastword/screenshots/03-audio-input.png" \ + "$(PREFIX)/share/doc/kastword/screenshots/04-settings.png" \ + "$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt" \ + "$(PREFIX)/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt" @if command -v update-desktop-database >/dev/null 2>&1; then \ update-desktop-database "$(PREFIX)/share/applications"; \ fi diff --git a/README.md b/README.md index 089399d..132e1f0 100644 --- a/README.md +++ b/README.md @@ -9,30 +9,82 @@ SPDX-License-Identifier: GPL-3.0-or-later **Private, offline dictation built for KDE Plasma.** -> [!WARNING] -> Kastword is early alpha software. This repository currently provides source code only—there -> are no supported binary releases or stable compatibility guarantees yet. - -Kastword records speech, transcribes it locally with `whisper.cpp`, and pastes the result into the -application you were using. No dictated audio or text is sent to a cloud service. After first-run -setup, Kastword stays out of the way in the system tray until you press the configurable global -shortcut, **Meta+Z** by default. +Kastword records your voice, transcribes it locally with Whisper, and places the result on your +clipboard. It can optionally paste the text into the application you were using. Dictated audio and +text are never sent to a cloud transcription service. -Kastword is an independent project built with KDE technology; it is not currently an official KDE -project or endorsed by KDE e.V. +> [!WARNING] +> Kastword is early alpha software. This repository currently provides source code only—there are +> no supported binary releases or stable compatibility guarantees yet. + + + + + + + + + + +
Offline dictation viewSpeech models view
Audio input viewSettings view
+ +## Why Kastword? + +- **Local transcription:** speech recognition runs on your computer using a model you choose. +- **Tray-first workflow:** start and stop dictation with a configurable KDE global shortcut, + **Meta+Z** by default. +- **Your microphone, your choice:** follow the system default, select a specific device, or disable + microphone access from the application. Kastword handles downmixing, resampling, and device + hot-plug events without silently replacing a disconnected specific device. +- **Manage models in the app:** download, verify, resume, switch, inspect, or remove supported + English-only and multilingual Whisper models. +- **Useful anywhere:** copy text to the clipboard, then paste manually or enable optional automatic + paste on Plasma Wayland or X11. +- **Configurable limits and output:** choose the transcription language, recording limit, paste + shortcuts, and global keyboard shortcut. +- **No history or telemetry:** Kastword does not save recordings, retain a transcription history, + or include usage tracking. + +## How dictation works + +1. Start Kastword and explicitly download a recommended Whisper model, or select a compatible + model already on your computer. +2. Focus the text field or terminal where you want the result. +3. Press **Meta+Z**, speak, then press it again. +4. Kastword transcribes the recording locally and updates the clipboard. +5. Paste manually, or let Kastword send your configured paste shortcut when automatic paste is + enabled and its helper is available. + +The window can be opened or hidden from the tray icon. The tray menu can also start or stop +dictation and quit the application. -## What works today +Dictation remains disabled until both a valid model and an available audio input are selected. The +Speech Models view shows model disk usage and can resume or retry interrupted downloads. If a +specifically selected microphone disconnects, Kastword waits for it to return or for you to choose +another input instead of silently switching devices. -- configurable KDE global dictation shortcut -- tray-first operation with recording, transcription, and success indicators -- selectable Qt Multimedia audio input with hot-plug recovery, downmixing, and resampling -- completely local transcription using a downloaded or user-selected compatible Whisper model -- clipboard output with optional automatic paste -- X11 paste through `xdotool` -- Plasma Wayland paste through `ydotool` -- no saved recordings, transcription history, or telemetry +## Privacy and trust -## Current status +- Microphone samples remain in process memory only until transcription completes. Recordings stop + at the configured duration limit—five minutes by default—or a 256 MiB raw-audio ceiling. +- Raw audio and transcription history are not written to disk. +- Transcribed text is placed on the desktop clipboard and primary selection. Clipboard managers, + target applications, desktop services, crash dumps, and the operating system may retain it + independently of Kastword. +- Runtime network access occurs only when you explicitly request a model download. Downloads use + immutable HTTPS URLs and are activated only after size, format, and SHA-256 verification. + Transcription itself remains offline. +- Downloaded models remain in per-user local storage under `~/.local/share/kastword/models/` by + default. Custom model files are trusted input parsed inside Kastword, so use models from sources + you trust. +- Automatic paste is disabled by default and relies on separately installed desktop helpers. See + [Automatic paste setup and security](docs/AUTOMATIC_PASTE.md). +- Kastword refuses to run with elevated privileges and contains no telemetry. + +The clear-transcription action removes Kastword's retained text and clears matching current +clipboard selections. It cannot erase entries already retained by clipboard-manager history. + +## Platform status | Environment | Status | | --- | --- | @@ -42,239 +94,37 @@ project or endorsed by KDE e.V. | Plasma X11 | Implemented but needs broader testing | | Other distributions and desktops | Community testing needed | -## Install dependencies on CachyOS or Arch Linux - -```sh -sudo pacman -S --needed \ - base-devel cmake ninja git \ - qt6-base qt6-declarative qt6-multimedia \ - extra-cmake-modules kirigami kdeclarative \ - kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \ - knotifications kstatusnotifieritem -``` - -Install the optional paste helper for your session: - -```sh -# Plasma Wayland -sudo pacman -S --needed ydotool - -# Plasma X11 -sudo pacman -S --needed xdotool -``` - -### Enable automatic paste on Plasma Wayland - -Installing `ydotool` provides both the command-line client and the `ydotoold` user service. Enable -and start the service for the current user: - -```sh -systemctl --user enable --now ydotool.service -systemctl --user is-active ydotool.service -``` - -The second command must print `active`. The daemon creates a virtual keyboard through -`/dev/uinput` and listens on a socket in the user's runtime directory. Verify both are present and -that the client can connect: - -```sh -ls -l /dev/uinput "$XDG_RUNTIME_DIR/.ydotool_socket" -ydotool debug -``` - -Finally, test actual input delivery. Run the following command, immediately focus an editable text -field, and wait one second; `Kastword ydotool test` should appear there: - -```sh -sleep 1 && ydotool type 'Kastword ydotool test' -``` - -If the service is inactive or the socket is missing, inspect its log: - -```sh -systemctl --user status ydotool.service -journalctl --user -u ydotool.service -b -``` - -Errors mentioning `/dev/uinput`, the daemon socket, or permission denied mean the helper is not -usable by the logged-in user. Check that the CachyOS/Arch package is current, restart the user -service, and log out and back in after changing device or group permissions. Avoid running Kastword -or `ydotool` with `sudo`. - -Kastword never requests elevated permissions and does not start or configure `ydotoold` itself. If -the helper is unavailable, transcription is still copied to the clipboard for manual pasting. - -## Build and run +Kastword is an independent project built with KDE technology; it is not currently an official KDE +project or endorsed by KDE e.V. -The first default build downloads one immutable, pinned dependency: +## Build from source -- `whisper.cpp` source at commit `a91dd3be72f70dd1b3cb6e252f35fa17b93f596c` +There are no supported binary packages yet. On a supported development system, install the +[documented dependencies](docs/DEVELOPMENT.md#dependencies), then run: ```sh make -make run -``` - -`make run` shows the application window immediately for local development. A normal installed -launch continues to start Kastword in the system tray. - -No speech model is downloaded or packaged during a normal build. The build needs network access -only when the pinned Whisper.cpp source is not already available. - -To install for the current user: - -```sh make install ``` -This installs the application and desktop launcher below `~/.local`, then refreshes Plasma's -application database. Launch Kastword from the application menu; `make run` is not needed. Set -`PREFIX` explicitly to install somewhere else. - -To uninstall: - -```sh -make uninstall -``` - -Uninstalling removes the installed application files but preserves downloaded models and user -settings. - -The application ID is `io.github.shape_machine.Kastword`; the underscore follows D-Bus guidance -for the hyphen in the `Shape-Machine` organization name. - -### Distribution builds - -Distribution packagers can provide the Whisper.cpp dependency without any build-time downloads: - -```sh -cmake -S . -B build -G Ninja \ - -DKASTWORD_FETCH_WHISPER=OFF -``` - -This requires a compatible system `whisper` CMake package. Kastword packages must not include a -speech model; users choose models after installation. The legacy -`KASTWORD_FETCH_DEFAULT_MODEL=ON` option remains available only for development compatibility. - -## Using Kastword - -1. Start Kastword. On first run, choose an English-only or multilingual speech model. -2. Explicitly download a recommended model or select an existing compatible `.bin` file. -3. Focus the text field or terminal where the result should go. -4. Press **Meta+Z** (or your configured shortcut) and speak, then press it again. -5. Kastword transcribes locally and updates the clipboard. If automatic paste is enabled and its - helper is available, Kastword also sends the configured paste shortcut. - -Downloaded models are checksum-verified and stored per user under -`~/.local/share/kastword/models/`. Speech Models can switch models, show their disk usage, resume or -retry downloads, and remove managed models. Audio Input can disable microphone use with None, follow -the system default microphone, or use a specific device. A specific selection is never silently -replaced when disconnected; dictation remains disabled until that device returns or another input is -chosen. Settings controls the transcription language, recording limit, automatic-paste behavior, -paste shortcuts, and global keyboard shortcut. Dictation remains disabled whenever no valid model or -audio input is available. - -Click the tray icon to open or hide the Kastword window. The tray menu can start or stop -dictation and quit the application. - -## Privacy and security model - -- Microphone samples live in process memory only until transcription completes. -- Recordings stop at the configured duration limit (five minutes by default) or a 256 MiB raw - audio ceiling, whichever comes first. -- Raw audio and transcription history are not written to disk. -- Transcribed text is placed on the desktop clipboard and primary selection. -- The selected model remains in per-user local storage. -- No telemetry is included. At runtime, network access is used only after an explicit model-download - action. -- Model downloads use immutable HTTPS URLs and are activated only after size, format, and SHA-256 - verification. Transcription remains offline. -- Automatic Wayland paste relies on the separately installed `ydotool`/`ydotoold` service. -- Custom model files are trusted input parsed inside Kastword; use models from sources you trust. -- Paste helpers are resolved from the inherited `PATH`; ensure every directory in `PATH` is - trusted. Kastword refuses to run with elevated privileges. - -Clipboard managers, target applications, desktop services, crash dumps, and the operating system -may retain data independently of Kastword. Review their settings when dictating sensitive text. -The clear-transcription action clears Kastword's retained text and any matching current clipboard -or primary selection; it does not erase entries already retained by clipboard-manager history. -Automatic paste is disabled by default. X11 focus is checked again before keys are sent; Wayland -does not expose an equivalent global focus check, so focus can change during the short delay. +Launch Kastword from the application menu. Detailed build, run, packaging, uninstall, and +contributor instructions are in the [development guide](docs/DEVELOPMENT.md). ## Known limitations - Full-size Large models require substantial disk space and memory. -- Paste reliability depends on the session, helper, and target application. -- `ydotool` requires privileged input-device access configured outside Kastword. +- Paste reliability depends on the desktop session, helper, focus, and target application. +- Plasma Wayland automatic paste requires privileged input-device access configured outside + Kastword. +- Plasma X11 and environments outside the tested Plasma setup need broader community testing. - There are no supported binary packages or release builds yet. -## Architecture - -```text -Global shortcut / tray - │ - ▼ - AppController state machine - ├── AudioCapture ─── Qt Multimedia - ├── WhisperEngine ── worker thread / whisper.cpp - ├── ModelManager ─── explicit verified downloads + per-user storage - └── TextOutput ───── clipboard + optional paste helper -``` - -Inference runs away from the UI thread. Recording and transcription status is reported through the -application window, system tray, and desktop notifications without opening the main window during a -normal dictation. - -## Development and validation - -```sh -make test -make coverage BUILD_DIR=build-coverage CMAKE_ARGS=-DKASTWORD_FETCH_DEFAULT_MODEL=OFF -make screenshots -make lint -make install-smoke -make format -make validate -``` - -`make test` builds and runs the deterministic test suites. `make coverage` enables instrumentation, -enforces the repository's line and branch thresholds, and writes a browsable report to -`build-coverage/coverage/index.html`, a text summary, and Cobertura XML. It uses an installed -`gcovr`, or runs it through `uvx` when available. The default gates require at least 68% line and -55% branch coverage and can be raised explicitly with `COVERAGE_MIN_LINE` and -`COVERAGE_MIN_BRANCH`. `make lint` checks C++ formatting without changing files, while `make format` -applies it. -`make screenshots` renders the four top-level application views in a 760×520 logical window with -deterministic fake data and safely replaces the PNG files in `screenshots/`. It requires an -active graphical session so the output inherits the current Plasma theme, icons, fonts, and display -scale. It does not access the microphone or network and preserves the previous complete set if -generation fails. Generated screenshots are covered by the repository-level REUSE annotation in -`REUSE.toml`. -`make install-smoke` installs into a temporary prefix, resolves and executes the application through -the installed desktop entry, validates the metadata, and verifies that uninstall removes every -installed file. `make validate` runs the build, tests, formatting check, REUSE license validation, -desktop metadata validation, and QML linting. QML linting uses Qt's CMake target, so it follows the -configured Qt toolchain and `BUILD_DIR` instead of assuming a distribution-specific executable -path. Coverage and license checks use installed `gcovr` and `reuse` commands, or fetch temporary -tools through `uvx`. The remaining checks require `appstreamcli`, `desktop-file-validate`, and -`clang-format`. CI invokes the same Make targets without downloading the model, installs packages -from a dated Arch Linux Archive snapshot, and also runs the tests under AddressSanitizer and -UndefinedBehaviorSanitizer. Every CI run publishes the exact coverage summary on its job page and -uploads the complete HTML, text, and XML reports as the `coverage-report` artifact. - -Kate users can open the repository directory and enable the Project, Build, and LSP Client -plugins. `.kateproject` provides Build, Run, Test, and Clean targets, while CMake generates -`build/compile_commands.json` for clangd. - -## Roadmap - -- broader Plasma Wayland/X11 and application compatibility testing -- broader audio-device and hot-plug compatibility testing -- reproducible distribution packages and signed binary releases - -Roadmap items are intentions, not promised dates. +## Help and project information -## License +- [Automatic paste setup and troubleshooting](docs/AUTOMATIC_PASTE.md) +- [Source installation and development](docs/DEVELOPMENT.md) +- [Report a bug or request a feature](https://github.com/Shape-Machine/Kastword/issues) +- [License](LICENSES/GPL-3.0-or-later.txt) -Kastword is licensed under `GPL-3.0-or-later`. See `LICENSES/GPL-3.0-or-later.txt`. -AppStream metadata is provided under `CC0-1.0`. Third-party components retain their own licenses. +Kastword is licensed under `GPL-3.0-or-later`. AppStream metadata is provided under `CC0-1.0`. +Third-party components retain their own licenses. diff --git a/docs/AUTOMATIC_PASTE.md b/docs/AUTOMATIC_PASTE.md new file mode 100644 index 0000000..1467e4c --- /dev/null +++ b/docs/AUTOMATIC_PASTE.md @@ -0,0 +1,82 @@ + + +# Automatic paste setup and troubleshooting + +Kastword always places completed transcription on the clipboard. Automatic paste is optional, +disabled by default, and uses a separately installed helper to send your configured paste shortcut. +If no helper is available, copy and transcription still work for manual pasting. + +## Install the session helper + +On CachyOS or Arch Linux: + +```sh +# Plasma Wayland +sudo pacman -S --needed ydotool + +# Plasma X11 +sudo pacman -S --needed xdotool +``` + +X11 paste uses `xdotool`. Kastword checks that the focused window is unchanged immediately before +sending keys, although focus can still change afterward. + +Wayland does not expose an equivalent global focus check. Kastword uses `ydotool`, whose +`ydotoold` service creates a virtual keyboard through `/dev/uinput`. Focus can change during the +short delay before keys are sent. + +## Enable automatic paste on Plasma Wayland + +Enable and start the packaged user service: + +```sh +systemctl --user enable --now ydotool.service +systemctl --user is-active ydotool.service +``` + +The second command must print `active`. Verify that the input device and daemon socket exist and +that the client can connect: + +```sh +ls -l /dev/uinput "$XDG_RUNTIME_DIR/.ydotool_socket" +ydotool debug +``` + +Test actual input delivery by running this command, immediately focusing an editable field, and +waiting one second. `Kastword ydotool test` should appear: + +```sh +sleep 1 && ydotool type 'Kastword ydotool test' +``` + +## Troubleshoot `ydotoold` + +If the service is inactive or the socket is missing, inspect its status and current-boot log: + +```sh +systemctl --user status ydotool.service +journalctl --user -u ydotool.service -b +``` + +Errors mentioning `/dev/uinput`, the daemon socket, or permission denied mean the helper is not +usable by the logged-in user. Check that the distribution package is current, restart the user +service, and log out and back in after changing device or group permissions. + +Kastword never requests elevated permissions and does not start or configure `ydotoold`. Do not run +Kastword or `ydotool` with `sudo`. + +## Security and privacy considerations + +- Paste helpers are resolved from the inherited `PATH`; ensure every directory in `PATH` is trusted. +- The target application receives the transcription and may retain it. +- Clipboard managers may save clipboard history independently of Kastword. +- The clear-transcription action clears matching current clipboard selections, not entries already + retained by a clipboard manager. +- Automatic paste sends keystrokes to whichever field has focus at delivery time. Confirm focus + before dictating sensitive text, especially on Wayland. + +Return to the [Kastword overview](../README.md) or see the [source installation and development +guide](DEVELOPMENT.md). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..3012d88 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,143 @@ + + +# Source installation and development + +Kastword is early alpha software and currently has no supported binary releases. This guide covers +source installation, packaging, architecture, and contributor tooling. See the [project +overview](../README.md) for product capabilities, privacy, and usage. + +## Dependencies + +On CachyOS or Arch Linux: + +```sh +sudo pacman -S --needed \ + base-devel cmake ninja git \ + qt6-base qt6-declarative qt6-multimedia \ + extra-cmake-modules kirigami kdeclarative \ + kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \ + knotifications kstatusnotifieritem +``` + +Automatic paste requires a separate session helper. See [Automatic paste setup and +troubleshooting](AUTOMATIC_PASTE.md). + +## Build and run + +The first default build downloads one immutable, pinned dependency: + +- `whisper.cpp` source at commit `a91dd3be72f70dd1b3cb6e252f35fa17b93f596c` + +```sh +make +make run +``` + +`make run` shows the application window immediately for local development. A normal installed +launch starts Kastword in the system tray. No speech model is downloaded or packaged during a +normal build. The build needs network access only when the pinned Whisper.cpp source is not already +available. + +## Install and uninstall + +Install for the current user: + +```sh +make install +``` + +This installs below `~/.local` and refreshes Plasma's application database. Set `PREFIX` explicitly +to install elsewhere. Launch Kastword from the application menu after installation. + +Uninstall application-owned files: + +```sh +make uninstall +``` + +Uninstalling preserves downloaded models and user settings. + +The application ID is `io.github.shape_machine.Kastword`; the underscore follows D-Bus guidance +for the hyphen in the `Shape-Machine` organization name. + +## Distribution builds + +Packagers can provide Whisper.cpp without a build-time download: + +```sh +cmake -S . -B build -G Ninja \ + -DKASTWORD_FETCH_WHISPER=OFF +``` + +This requires a compatible system `whisper` CMake package. Kastword packages must not include a +speech model; users choose models after installation. The legacy +`KASTWORD_FETCH_DEFAULT_MODEL=ON` option remains available only for development compatibility. + +## Architecture + +```text +Global shortcut / tray + │ + ▼ + AppController state machine + ├── AudioCapture ─── Qt Multimedia + ├── WhisperEngine ── worker thread / whisper.cpp + ├── ModelManager ─── explicit verified downloads + per-user storage + └── TextOutput ───── clipboard + optional paste helper +``` + +Inference runs away from the UI thread. Recording and transcription status is reported through the +application window, system tray, and desktop notifications without opening the main window during +a normal dictation. + +## Development and validation + +```sh +make test +make coverage BUILD_DIR=build-coverage CMAKE_ARGS=-DKASTWORD_FETCH_DEFAULT_MODEL=OFF +make screenshots +make lint +make install-smoke +make format +make validate +``` + +`make test` builds and runs the deterministic test suites. `make coverage` enables instrumentation, +enforces the repository's line and branch thresholds, and writes HTML, text, and Cobertura XML +reports under `build-coverage/coverage/`. The default gates require at least 68% line and 55% branch +coverage and can be raised with `COVERAGE_MIN_LINE` and `COVERAGE_MIN_BRANCH`. Coverage uses an +installed `gcovr`, or runs it through `uvx` when available. + +`make screenshots` renders all four top-level views in a 760×520 logical window with deterministic +fake data and safely replaces the permanent PNG assets in `screenshots/`. It requires an active +graphical session so output inherits the current Plasma theme, icons, fonts, and display scale. It +does not access the microphone or network and preserves the previous complete set on failure. + +`make lint` checks C++ formatting without changing files; `make format` applies it. +`make install-smoke` installs into a temporary prefix, launches through the installed desktop entry, +validates metadata, and verifies complete uninstall behavior. `make validate` runs the build, tests, +formatting check, REUSE validation, desktop metadata validation, and QML linting. Coverage and +license checks use installed `gcovr` and `reuse`, or temporary tools through `uvx`. Remaining checks +require `appstreamcli`, `desktop-file-validate`, and `clang-format`. + +CI uses the same Make targets without downloading a model, installs packages from a dated Arch +Linux Archive snapshot, and runs tests under AddressSanitizer and UndefinedBehaviorSanitizer. Each +run publishes its coverage summary and uploads the HTML, text, and XML reports as the +`coverage-report` artifact. + +## Kate + +Open the repository directory and enable the Project, Build, and LSP Client plugins. +`.kateproject` provides Build, Run, Test, and Clean targets, while CMake generates +`build/compile_commands.json` for clangd. + +## Roadmap + +- broader Plasma Wayland/X11 and application compatibility testing +- broader audio-device and hot-plug compatibility testing +- reproducible distribution packages and signed binary releases + +Roadmap items are intentions, not promised dates. diff --git a/screenshots/01-offline-dictation.png b/screenshots/01-offline-dictation.png index 4d2dc84..f0667bf 100644 Binary files a/screenshots/01-offline-dictation.png and b/screenshots/01-offline-dictation.png differ diff --git a/screenshots/02-speech-models.png b/screenshots/02-speech-models.png index d62102a..0508807 100644 Binary files a/screenshots/02-speech-models.png and b/screenshots/02-speech-models.png differ diff --git a/screenshots/03-audio-input.png b/screenshots/03-audio-input.png index 18efa57..b4f1dcb 100644 Binary files a/screenshots/03-audio-input.png and b/screenshots/03-audio-input.png differ diff --git a/screenshots/04-settings.png b/screenshots/04-settings.png index cbb285a..46ec503 100644 Binary files a/screenshots/04-settings.png and b/screenshots/04-settings.png differ diff --git a/tests/ModelManagerTest.cpp b/tests/ModelManagerTest.cpp index 221bf4b..4c8da59 100644 --- a/tests/ModelManagerTest.cpp +++ b/tests/ModelManagerTest.cpp @@ -544,7 +544,7 @@ void ModelManagerTest::invalidatesChangedManagedModel() { QTRY_VERIFY(!manager.modelReady()); QTRY_VERIFY(!manager.busy()); - QVERIFY(!QFileInfo::exists(path)); + QTRY_VERIFY(!QFileInfo::exists(path)); } void ModelManagerTest::invalidatesChangedLocalModel() { diff --git a/tools/check-documentation-links.sh b/tools/check-documentation-links.sh new file mode 100755 index 0000000..6bd79ae --- /dev/null +++ b/tools/check-documentation-links.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2026 Sri Rang +# SPDX-License-Identifier: GPL-3.0-or-later + +set -eu + +if [ "$#" -ne 1 ] || [ ! -d "$1" ]; then + echo "Usage: $0 DOCUMENTATION_ROOT" >&2 + exit 2 +fi + +documentation_root=$(CDPATH= cd -- "$1" && pwd -P) +documents=$(mktemp) +targets=$(mktemp) +trap 'cmake -E rm -f "$documents" "$targets"' EXIT HUP INT TERM + +if [ -f "$documentation_root/README.md" ]; then + printf '%s\n' "$documentation_root/README.md" > "$documents" +fi +if [ -d "$documentation_root/docs" ]; then + find "$documentation_root/docs" -type f -name '*.md' -print >> "$documents" +fi + +while IFS= read -r document; do + awk -v document="$document" ' + { text = text " " $0 } + END { + markdown = text + html = text + while (match(markdown, /\]\([^)]*\)/)) { + print document "\t" substr(markdown, RSTART + 2, RLENGTH - 3) + markdown = substr(markdown, RSTART + RLENGTH) + } + while (match(html, /src="[^"]+"/)) { + print document "\t" substr(html, RSTART + 5, RLENGTH - 6) + html = substr(html, RSTART + RLENGTH) + } + } + ' "$document" +done < "$documents" > "$targets" + +while IFS=" " read -r document destination; do + case "$destination" in + ''|'#'*|http://*|https://*|mailto:*) continue ;; + esac + + relative_path=${destination%%#*} + fragment= + if [ "$relative_path" != "$destination" ]; then + fragment=${destination#*#} + fi + resolved_directory=$(CDPATH= cd -- "$(dirname -- "$document")" && pwd -P) + resolved_path="$resolved_directory/$relative_path" + if [ ! -f "$resolved_path" ]; then + echo "$document: broken relative link: $destination" >&2 + exit 1 + fi + + if [ -n "$fragment" ] && ! awk -v wanted="$fragment" ' + /^#{1,6}[[:space:]]/ { + heading = $0 + sub(/^#+[[:space:]]+/, "", heading) + heading = tolower(heading) + gsub(/[^a-z0-9 _-]/, "", heading) + gsub(/[ _]+/, "-", heading) + if (heading == wanted) + found = 1 + } + END { exit !found } + ' "$resolved_path"; then + echo "$document: missing heading for link: $destination" >&2 + exit 1 + fi +done < "$targets"