Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)

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

find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia)
find_package(KF6GlobalAccel CONFIG REQUIRED)
Expand All @@ -29,6 +34,8 @@ set(MUTTERKEY_APP_SOURCES
src/audio/recording.h
src/clipboardwriter.cpp
src/clipboardwriter.h
src/commanddispatch.cpp
src/commanddispatch.h
src/config.cpp
src/config.h
src/hotkeymanager.cpp
Expand Down Expand Up @@ -146,6 +153,10 @@ set(WHISPER_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(WHISPER_BUILD_SERVER OFF CACHE BOOL "" FORCE)
set(WHISPER_SANITIZE_ADDRESS ${MUTTERKEY_ENABLE_ASAN} CACHE BOOL "" FORCE)
set(WHISPER_SANITIZE_UNDEFINED ${MUTTERKEY_ENABLE_UBSAN} CACHE BOOL "" FORCE)
set(GGML_CUDA ${MUTTERKEY_ENABLE_WHISPER_CUDA} CACHE BOOL "" FORCE)
set(GGML_VULKAN ${MUTTERKEY_ENABLE_WHISPER_VULKAN} CACHE BOOL "" FORCE)
set(GGML_BLAS ${MUTTERKEY_ENABLE_WHISPER_BLAS} CACHE BOOL "" FORCE)
set(GGML_BLAS_VENDOR ${MUTTERKEY_WHISPER_BLAS_VENDOR} CACHE STRING "" FORCE)
add_subdirectory(third_party/whisper.cpp EXCLUDE_FROM_ALL)

# Mutterkey ships the vendored shared libraries, but it does not install their
Expand All @@ -161,6 +172,15 @@ install(TARGETS whisper ggml ggml-base
if(TARGET ggml-cpu)
install(TARGETS ggml-cpu LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
if(TARGET ggml-cuda)
install(TARGETS ggml-cuda LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
if(TARGET ggml-vulkan)
install(TARGETS ggml-vulkan LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
if(TARGET ggml-blas)
install(TARGETS ggml-blas LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
install(FILES contrib/org.mutterkey.mutterkey.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install(FILES LICENSE THIRD_PARTY_NOTICES.md DESTINATION ${MUTTERKEY_LICENSE_INSTALL_DIR})
install(FILES third_party/whisper.cpp/LICENSE DESTINATION ${MUTTERKEY_LICENSE_INSTALL_DIR}/third_party/whisper.cpp)
Expand Down
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,37 @@ This installs:
- `~/.local/lib/libwhisper.so*` and the required `ggml` libraries
- `~/.local/share/applications/org.mutterkey.mutterkey.desktop`

Optional acceleration flags:

```bash
cmake -S . -B "$BUILD_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
-DMUTTERKEY_ENABLE_WHISPER_CUDA=ON
```

```bash
cmake -S . -B "$BUILD_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
-DMUTTERKEY_ENABLE_WHISPER_VULKAN=ON
```

```bash
cmake -S . -B "$BUILD_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
-DMUTTERKEY_ENABLE_WHISPER_BLAS=ON \
-DMUTTERKEY_WHISPER_BLAS_VENDOR=OpenBLAS
```

Notes:

- `MUTTERKEY_ENABLE_WHISPER_CUDA=ON` is for NVIDIA GPUs and requires a working CUDA toolchain
- `MUTTERKEY_ENABLE_WHISPER_VULKAN=ON` is for Vulkan-capable GPUs and requires Vulkan development headers and loader libraries
- `MUTTERKEY_ENABLE_WHISPER_BLAS=ON` improves CPU inference speed rather than enabling GPU execution
- these options are forwarded to the vendored `whisper.cpp` / `ggml` build and install any resulting backend libraries alongside Mutterkey

### 2. Put a Whisper model on disk

Example location:
Expand All @@ -121,11 +152,24 @@ Example location:
### 3. Create the config file

```bash
mkdir -p ~/.config/mutterkey
cp config.example.json ~/.config/mutterkey/config.json
mutterkey config init --model-path ~/.local/share/mutterkey/models/ggml-base.en.bin
```

Edit `~/.config/mutterkey/config.json` and set at least:
`mutterkey config init` writes the Linux config file to:

```text
~/.config/mutterkey/config.json
```

When run from a terminal, Mutterkey can also create this file automatically on
first launch if it does not exist yet. The interactive bootstrap asks for:

- `transcriber.model_path`
- `shortcut.sequence`

You can update saved values later with `mutterkey config set <key> <value>`.

Set at least:

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

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

### 4. Sanity-check the installed binary

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

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

### 6. Use the hotkey

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

Useful config commands:

```bash
~/.local/bin/mutterkey config init --model-path ~/.local/share/mutterkey/models/ggml-base.en.bin
~/.local/bin/mutterkey config set shortcut.sequence Meta+F8
~/.local/bin/mutterkey config set transcriber.language fi
```

The desktop entry
[contrib/org.mutterkey.mutterkey.desktop](contrib/org.mutterkey.mutterkey.desktop)
is intentionally hidden from normal app menus because the project currently
Expand Down
32 changes: 32 additions & 0 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ BUILD_DIR="$(mktemp -d /tmp/mutterkey-build-XXXXXX)"
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
```

- If the release is intended to ship an accelerated Whisper backend, configure
the build with the relevant Mutterkey options:

```bash
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_CUDA=ON
```

```bash
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_VULKAN=ON
```

```bash
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug -DMUTTERKEY_ENABLE_WHISPER_BLAS=ON -DMUTTERKEY_WHISPER_BLAS_VENDOR=OpenBLAS
```

- Acceleration option notes:
- `MUTTERKEY_ENABLE_WHISPER_CUDA=ON`: NVIDIA GPU build through vendored `ggml`
- `MUTTERKEY_ENABLE_WHISPER_VULKAN=ON`: Vulkan GPU build through vendored `ggml`
- `MUTTERKEY_ENABLE_WHISPER_BLAS=ON`: faster CPU inference, not GPU execution
- choose the backend intentionally for the release artifact and record that choice in release notes or packaging docs when relevant

- Build:

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

## Vendored whisper.cpp Updates

Expand Down
2 changes: 1 addition & 1 deletion config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"device_id": ""
},
"transcriber": {
"model_path": "/absolute/path/to/ggml-base.en.bin",
"model_path": "/path/to/ggml-base.en.bin",
"language": "en",
"translate": false,
"threads": 0,
Expand Down
114 changes: 114 additions & 0 deletions src/commanddispatch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include "commanddispatch.h"

#include "config.h"

#include <QString>
#include <QTextStream>

namespace {

bool optionConsumesValue(const QString &argument)
{
static const QStringList kOptionsWithValues{
QStringLiteral("--config"),
QStringLiteral("--log-level"),
QStringLiteral("--model-path"),
QStringLiteral("--shortcut"),
QStringLiteral("--language"),
QStringLiteral("--threads"),
};

return kOptionsWithValues.contains(argument);
}

bool isHelpArgument(const QString &argument)
{
return argument == QStringLiteral("--help") || argument == QStringLiteral("-h") || argument == QStringLiteral("--help-all");
}

} // namespace

QStringList rawArguments(std::span<char *const> arguments)
{
QStringList parsedArguments;
parsedArguments.reserve(static_cast<qsizetype>(arguments.size()));
for (char *argument : arguments) {
parsedArguments.append(QString::fromLocal8Bit(argument));
}
return parsedArguments;
}

int commandIndexFromArguments(const QStringList &arguments)
{
for (int index = 1; index < arguments.size(); ++index) {
const QString &argument = arguments.at(index);
if (argument == QStringLiteral("--")) {
return index + 1 < arguments.size() ? index + 1 : -1;
}

if (optionConsumesValue(argument)) {
++index;
continue;
}

if (argument.startsWith(QLatin1String("--")) && argument.contains(QLatin1Char('='))) {
continue;
}

if (argument.startsWith(QLatin1Char('-'))) {
continue;
}

return index;
}

return -1;
}

bool shouldShowConfigHelp(const QStringList &arguments, int commandIndex)
{
if (commandIndex < 0 || commandIndex >= arguments.size() || arguments.at(commandIndex) != QStringLiteral("config")) {
return false;
}

if (commandIndex == arguments.size() - 1) {
return true;
}

for (int index = commandIndex + 1; index < arguments.size(); ++index) {
if (isHelpArgument(arguments.at(index))) {
return true;
}
}

return false;
}

QString configHelpText()
{
QString helpText;
QTextStream output(&helpText);
output << "Usage: mutterkey [options] config <subcommand> [args]" << Qt::endl;
output << Qt::endl;
output << "Configuration subcommands:" << Qt::endl;
output << " init Create the config file, prompting on a terminal when needed" << Qt::endl;
output << " set <key> <value> Persist one config value into the config file" << Qt::endl;
output << Qt::endl;
output << "Config options:" << Qt::endl;
output << " --config <path> Path to the JSON config file" << Qt::endl;
output << " --model-path <path> Set transcriber.model_path during `config init`" << Qt::endl;
output << " --shortcut <sequence> Set shortcut.sequence during `config init`" << Qt::endl;
output << " --language <code|auto> Set transcriber.language during `config init`" << Qt::endl;
output << " --threads <count> Set transcriber.threads during `config init`" << Qt::endl;
output << " --translate Set transcriber.translate=true during `config init`" << Qt::endl;
output << " --no-translate Set transcriber.translate=false during `config init`" << Qt::endl;
output << " --warmup-on-start Set transcriber.warmup_on_start=true during `config init`" << Qt::endl;
output << " --no-warmup-on-start Set transcriber.warmup_on_start=false during `config init`" << Qt::endl;
output << " --log-level <level> Set log_level during `config init`" << Qt::endl;
output << Qt::endl;
output << "Supported keys for `config set`:" << Qt::endl;
for (const QString &key : supportedConfigKeys()) {
output << " " << key << Qt::endl;
}
return helpText;
}
32 changes: 32 additions & 0 deletions src/commanddispatch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include <span>
#include <QStringList>

/**
* @brief Converts raw argv data into Qt strings.
* @param arguments Raw argv span.
* @return Raw command-line arguments as a QStringList.
*/
QStringList rawArguments(std::span<char *const> arguments);

/**
* @brief Finds the first positional command after known global options.
* @param arguments Raw command-line arguments.
* @return Index of the command token, or `-1` when no command is present.
*/
int commandIndexFromArguments(const QStringList &arguments);

/**
* @brief Returns whether the config command should print dedicated help.
* @param arguments Raw command-line arguments.
* @param commandIndex Index returned by commandIndexFromArguments().
* @return `true` for bare `config` and `config --help` style invocations.
*/
bool shouldShowConfigHelp(const QStringList &arguments, int commandIndex);

/**
* @brief Returns the dedicated help text for config subcommands.
* @return Human-readable help text.
*/
QString configHelpText();
Loading
Loading