Skip to content

Commit cedb687

Browse files
committed
Added a configurator tool and CUDA and VULKAN GPU support
1 parent 03287e0 commit cedb687

14 files changed

Lines changed: 1427 additions & 25 deletions

CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)
1515

1616
option(MUTTERKEY_ENABLE_ASAN "Enable AddressSanitizer for repo-owned code and vendored whisper.cpp" OFF)
1717
option(MUTTERKEY_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer for repo-owned code and vendored whisper.cpp" OFF)
18+
option(MUTTERKEY_ENABLE_WHISPER_CUDA "Enable whisper.cpp CUDA backend support (NVIDIA)" OFF)
19+
option(MUTTERKEY_ENABLE_WHISPER_VULKAN "Enable whisper.cpp Vulkan backend support" OFF)
20+
option(MUTTERKEY_ENABLE_WHISPER_BLAS "Enable whisper.cpp BLAS CPU acceleration" OFF)
21+
set(MUTTERKEY_WHISPER_BLAS_VENDOR "Generic" CACHE STRING "BLAS vendor passed to whisper.cpp when BLAS acceleration is enabled")
22+
set_property(CACHE MUTTERKEY_WHISPER_BLAS_VENDOR PROPERTY STRINGS "Generic;OpenBLAS;FLAME;ATLAS;FlexiBLAS;Intel;NVHPC;Apple")
1823

1924
find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia)
2025
find_package(KF6GlobalAccel CONFIG REQUIRED)
@@ -29,6 +34,8 @@ set(MUTTERKEY_APP_SOURCES
2934
src/audio/recording.h
3035
src/clipboardwriter.cpp
3136
src/clipboardwriter.h
37+
src/commanddispatch.cpp
38+
src/commanddispatch.h
3239
src/config.cpp
3340
src/config.h
3441
src/hotkeymanager.cpp
@@ -146,6 +153,10 @@ set(WHISPER_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
146153
set(WHISPER_BUILD_SERVER OFF CACHE BOOL "" FORCE)
147154
set(WHISPER_SANITIZE_ADDRESS ${MUTTERKEY_ENABLE_ASAN} CACHE BOOL "" FORCE)
148155
set(WHISPER_SANITIZE_UNDEFINED ${MUTTERKEY_ENABLE_UBSAN} CACHE BOOL "" FORCE)
156+
set(GGML_CUDA ${MUTTERKEY_ENABLE_WHISPER_CUDA} CACHE BOOL "" FORCE)
157+
set(GGML_VULKAN ${MUTTERKEY_ENABLE_WHISPER_VULKAN} CACHE BOOL "" FORCE)
158+
set(GGML_BLAS ${MUTTERKEY_ENABLE_WHISPER_BLAS} CACHE BOOL "" FORCE)
159+
set(GGML_BLAS_VENDOR ${MUTTERKEY_WHISPER_BLAS_VENDOR} CACHE STRING "" FORCE)
149160
add_subdirectory(third_party/whisper.cpp EXCLUDE_FROM_ALL)
150161

151162
# Mutterkey ships the vendored shared libraries, but it does not install their
@@ -161,6 +172,15 @@ install(TARGETS whisper ggml ggml-base
161172
if(TARGET ggml-cpu)
162173
install(TARGETS ggml-cpu LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
163174
endif()
175+
if(TARGET ggml-cuda)
176+
install(TARGETS ggml-cuda LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
177+
endif()
178+
if(TARGET ggml-vulkan)
179+
install(TARGETS ggml-vulkan LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
180+
endif()
181+
if(TARGET ggml-blas)
182+
install(TARGETS ggml-blas LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
183+
endif()
164184
install(FILES contrib/org.mutterkey.mutterkey.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
165185
install(FILES LICENSE THIRD_PARTY_NOTICES.md DESTINATION ${MUTTERKEY_LICENSE_INSTALL_DIR})
166186
install(FILES third_party/whisper.cpp/LICENSE DESTINATION ${MUTTERKEY_LICENSE_INSTALL_DIR}/third_party/whisper.cpp)

README.md

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,37 @@ This installs:
110110
- `~/.local/lib/libwhisper.so*` and the required `ggml` libraries
111111
- `~/.local/share/applications/org.mutterkey.mutterkey.desktop`
112112

113+
Optional acceleration flags:
114+
115+
```bash
116+
cmake -S . -B "$BUILD_DIR" \
117+
-DCMAKE_BUILD_TYPE=Release \
118+
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
119+
-DMUTTERKEY_ENABLE_WHISPER_CUDA=ON
120+
```
121+
122+
```bash
123+
cmake -S . -B "$BUILD_DIR" \
124+
-DCMAKE_BUILD_TYPE=Release \
125+
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
126+
-DMUTTERKEY_ENABLE_WHISPER_VULKAN=ON
127+
```
128+
129+
```bash
130+
cmake -S . -B "$BUILD_DIR" \
131+
-DCMAKE_BUILD_TYPE=Release \
132+
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
133+
-DMUTTERKEY_ENABLE_WHISPER_BLAS=ON \
134+
-DMUTTERKEY_WHISPER_BLAS_VENDOR=OpenBLAS
135+
```
136+
137+
Notes:
138+
139+
- `MUTTERKEY_ENABLE_WHISPER_CUDA=ON` is for NVIDIA GPUs and requires a working CUDA toolchain
140+
- `MUTTERKEY_ENABLE_WHISPER_VULKAN=ON` is for Vulkan-capable GPUs and requires Vulkan development headers and loader libraries
141+
- `MUTTERKEY_ENABLE_WHISPER_BLAS=ON` improves CPU inference speed rather than enabling GPU execution
142+
- these options are forwarded to the vendored `whisper.cpp` / `ggml` build and install any resulting backend libraries alongside Mutterkey
143+
113144
### 2. Put a Whisper model on disk
114145

115146
Example location:
@@ -121,11 +152,24 @@ Example location:
121152
### 3. Create the config file
122153

123154
```bash
124-
mkdir -p ~/.config/mutterkey
125-
cp config.example.json ~/.config/mutterkey/config.json
155+
mutterkey config init --model-path ~/.local/share/mutterkey/models/ggml-base.en.bin
126156
```
127157

128-
Edit `~/.config/mutterkey/config.json` and set at least:
158+
`mutterkey config init` writes the Linux config file to:
159+
160+
```text
161+
~/.config/mutterkey/config.json
162+
```
163+
164+
When run from a terminal, Mutterkey can also create this file automatically on
165+
first launch if it does not exist yet. The interactive bootstrap asks for:
166+
167+
- `transcriber.model_path`
168+
- `shortcut.sequence`
169+
170+
You can update saved values later with `mutterkey config set <key> <value>`.
171+
172+
Set at least:
129173

130174
- `shortcut.sequence`
131175
- `transcriber.model_path`
@@ -159,8 +203,11 @@ See [config.example.json](config.example.json) for the full config.
159203
Config notes:
160204

161205
- `transcriber.threads: 0` means auto-detect based on the local machine
206+
- `transcriber.language` accepts a Whisper language code such as `en` or `fi`, or `auto` for language detection
162207
- invalid numeric values fall back to safe defaults and log a warning
208+
- invalid `transcriber.language` values fall back to the default and log a warning
163209
- empty `shortcut.sequence` or `transcriber.model_path` values fall back to defaults and log a warning
210+
- runtime flags such as `--model-path`, `--shortcut`, `--language`, `--translate`, `--threads`, and `--warmup-on-start` override the saved config for the current process only
164211

165212
### 4. Sanity-check the installed binary
166213

@@ -185,7 +232,8 @@ The default service file assumes:
185232
- config file at `%h/.config/mutterkey/config.json`
186233

187234
If your paths differ, edit [contrib/mutterkey.service](contrib/mutterkey.service)
188-
before enabling it.
235+
before enabling it. If the config file does not exist, the service will fail
236+
fast and instruct you to run `mutterkey config init` from a terminal first.
189237

190238
### 6. Use the hotkey
191239

@@ -231,6 +279,14 @@ installed setup looks like:
231279
%h/.local/bin/mutterkey daemon --config %h/.config/mutterkey/config.json
232280
```
233281

282+
Useful config commands:
283+
284+
```bash
285+
~/.local/bin/mutterkey config init --model-path ~/.local/share/mutterkey/models/ggml-base.en.bin
286+
~/.local/bin/mutterkey config set shortcut.sequence Meta+F8
287+
~/.local/bin/mutterkey config set transcriber.language fi
288+
```
289+
234290
The desktop entry
235291
[contrib/org.mutterkey.mutterkey.desktop](contrib/org.mutterkey.mutterkey.desktop)
236292
is intentionally hidden from normal app menus because the project currently

RELEASE_CHECKLIST.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ BUILD_DIR="$(mktemp -d /tmp/mutterkey-build-XXXXXX)"
3333
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
3434
```
3535

36+
- If the release is intended to ship an accelerated Whisper backend, configure
37+
the build with the relevant Mutterkey options:
38+
39+
```bash
40+
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_CUDA=ON
41+
```
42+
43+
```bash
44+
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_VULKAN=ON
45+
```
46+
47+
```bash
48+
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_BLAS=ON -DMUTTERKEY_WHISPER_BLAS_VENDOR=OpenBLAS
49+
```
50+
51+
- Acceleration option notes:
52+
- `MUTTERKEY_ENABLE_WHISPER_CUDA=ON`: NVIDIA GPU build through vendored `ggml`
53+
- `MUTTERKEY_ENABLE_WHISPER_VULKAN=ON`: Vulkan GPU build through vendored `ggml`
54+
- `MUTTERKEY_ENABLE_WHISPER_BLAS=ON`: faster CPU inference, not GPU execution
55+
- choose the backend intentionally for the release artifact and record that choice in release notes or packaging docs when relevant
56+
3657
- Build:
3758

3859
```bash
@@ -104,6 +125,11 @@ cmake --install "$BUILD_DIR" --prefix "$INSTALL_DIR"
104125
- required `libwhisper` / `ggml` shared libraries
105126
- the desktop file under `share/applications`
106127
- license files under `share/licenses/mutterkey`
128+
- If acceleration was enabled for the release, also confirm the installed tree
129+
contains the expected backend library:
130+
- `libggml-cuda.so*` for `MUTTERKEY_ENABLE_WHISPER_CUDA=ON`
131+
- `libggml-vulkan.so*` for `MUTTERKEY_ENABLE_WHISPER_VULKAN=ON`
132+
- `libggml-blas.so*` for `MUTTERKEY_ENABLE_WHISPER_BLAS=ON`
107133
- Do not expect vendored upstream public headers to be installed; Mutterkey's
108134
install rules ship the runtime libraries but intentionally clear vendored
109135
`PUBLIC_HEADER` metadata to avoid upstream header-install warnings.
@@ -118,6 +144,12 @@ cmake --install "$BUILD_DIR" --prefix "$INSTALL_DIR"
118144
recommended installed-binary setup.
119145
- Confirm [contrib/org.mutterkey.mutterkey.desktop](contrib/org.mutterkey.mutterkey.desktop)
120146
still reflects the intended desktop behavior, including `NoDisplay=true`.
147+
- If the release is intended to use accelerated Whisper inference, verify the
148+
runtime logs on a representative machine show the expected backend instead of
149+
CPU-only fallback. For example:
150+
- CUDA/Vulkan releases should not log only `registered backend CPU`
151+
- CPU-accelerated BLAS releases may still be CPU-only, but should be tested
152+
against a representative Whisper model and expected performance target
121153

122154
## Vendored whisper.cpp Updates
123155

config.example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"device_id": ""
1414
},
1515
"transcriber": {
16-
"model_path": "/absolute/path/to/ggml-base.en.bin",
16+
"model_path": "/path/to/ggml-base.en.bin",
1717
"language": "en",
1818
"translate": false,
1919
"threads": 0,

src/commanddispatch.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#include "commanddispatch.h"
2+
3+
#include "config.h"
4+
5+
#include <QString>
6+
#include <QTextStream>
7+
8+
namespace {
9+
10+
bool optionConsumesValue(const QString &argument)
11+
{
12+
static const QStringList kOptionsWithValues{
13+
QStringLiteral("--config"),
14+
QStringLiteral("--log-level"),
15+
QStringLiteral("--model-path"),
16+
QStringLiteral("--shortcut"),
17+
QStringLiteral("--language"),
18+
QStringLiteral("--threads"),
19+
};
20+
21+
return kOptionsWithValues.contains(argument);
22+
}
23+
24+
bool isHelpArgument(const QString &argument)
25+
{
26+
return argument == QStringLiteral("--help") || argument == QStringLiteral("-h") || argument == QStringLiteral("--help-all");
27+
}
28+
29+
} // namespace
30+
31+
QStringList rawArguments(std::span<char *const> arguments)
32+
{
33+
QStringList parsedArguments;
34+
parsedArguments.reserve(static_cast<qsizetype>(arguments.size()));
35+
for (char *argument : arguments) {
36+
parsedArguments.append(QString::fromLocal8Bit(argument));
37+
}
38+
return parsedArguments;
39+
}
40+
41+
int commandIndexFromArguments(const QStringList &arguments)
42+
{
43+
for (int index = 1; index < arguments.size(); ++index) {
44+
const QString &argument = arguments.at(index);
45+
if (argument == QStringLiteral("--")) {
46+
return index + 1 < arguments.size() ? index + 1 : -1;
47+
}
48+
49+
if (optionConsumesValue(argument)) {
50+
++index;
51+
continue;
52+
}
53+
54+
if (argument.startsWith(QLatin1String("--")) && argument.contains(QLatin1Char('='))) {
55+
continue;
56+
}
57+
58+
if (argument.startsWith(QLatin1Char('-'))) {
59+
continue;
60+
}
61+
62+
return index;
63+
}
64+
65+
return -1;
66+
}
67+
68+
bool shouldShowConfigHelp(const QStringList &arguments, int commandIndex)
69+
{
70+
if (commandIndex < 0 || commandIndex >= arguments.size() || arguments.at(commandIndex) != QStringLiteral("config")) {
71+
return false;
72+
}
73+
74+
if (commandIndex == arguments.size() - 1) {
75+
return true;
76+
}
77+
78+
for (int index = commandIndex + 1; index < arguments.size(); ++index) {
79+
if (isHelpArgument(arguments.at(index))) {
80+
return true;
81+
}
82+
}
83+
84+
return false;
85+
}
86+
87+
QString configHelpText()
88+
{
89+
QString helpText;
90+
QTextStream output(&helpText);
91+
output << "Usage: mutterkey [options] config <subcommand> [args]" << Qt::endl;
92+
output << Qt::endl;
93+
output << "Configuration subcommands:" << Qt::endl;
94+
output << " init Create the config file, prompting on a terminal when needed" << Qt::endl;
95+
output << " set <key> <value> Persist one config value into the config file" << Qt::endl;
96+
output << Qt::endl;
97+
output << "Config options:" << Qt::endl;
98+
output << " --config <path> Path to the JSON config file" << Qt::endl;
99+
output << " --model-path <path> Set transcriber.model_path during `config init`" << Qt::endl;
100+
output << " --shortcut <sequence> Set shortcut.sequence during `config init`" << Qt::endl;
101+
output << " --language <code|auto> Set transcriber.language during `config init`" << Qt::endl;
102+
output << " --threads <count> Set transcriber.threads during `config init`" << Qt::endl;
103+
output << " --translate Set transcriber.translate=true during `config init`" << Qt::endl;
104+
output << " --no-translate Set transcriber.translate=false during `config init`" << Qt::endl;
105+
output << " --warmup-on-start Set transcriber.warmup_on_start=true during `config init`" << Qt::endl;
106+
output << " --no-warmup-on-start Set transcriber.warmup_on_start=false during `config init`" << Qt::endl;
107+
output << " --log-level <level> Set log_level during `config init`" << Qt::endl;
108+
output << Qt::endl;
109+
output << "Supported keys for `config set`:" << Qt::endl;
110+
for (const QString &key : supportedConfigKeys()) {
111+
output << " " << key << Qt::endl;
112+
}
113+
return helpText;
114+
}

src/commanddispatch.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#pragma once
2+
3+
#include <span>
4+
#include <QStringList>
5+
6+
/**
7+
* @brief Converts raw argv data into Qt strings.
8+
* @param arguments Raw argv span.
9+
* @return Raw command-line arguments as a QStringList.
10+
*/
11+
QStringList rawArguments(std::span<char *const> arguments);
12+
13+
/**
14+
* @brief Finds the first positional command after known global options.
15+
* @param arguments Raw command-line arguments.
16+
* @return Index of the command token, or `-1` when no command is present.
17+
*/
18+
int commandIndexFromArguments(const QStringList &arguments);
19+
20+
/**
21+
* @brief Returns whether the config command should print dedicated help.
22+
* @param arguments Raw command-line arguments.
23+
* @param commandIndex Index returned by commandIndexFromArguments().
24+
* @return `true` for bare `config` and `config --help` style invocations.
25+
*/
26+
bool shouldShowConfigHelp(const QStringList &arguments, int commandIndex);
27+
28+
/**
29+
* @brief Returns the dedicated help text for config subcommands.
30+
* @return Human-readable help text.
31+
*/
32+
QString configHelpText();

0 commit comments

Comments
 (0)