Merge pull request #84 from JRufer/claude/stop-tts-when-dictation-starts #75
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Trigger: every push to master builds and (re)creates the release for the | |
| # current app version (read from src-tauri/tauri.conf.json). Bump the version | |
| # there to start a fresh release; pushes at the same version refresh it. | |
| # Can also be triggered manually with an explicit tag to (re-)build. | |
| on: | |
| push: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag to (re-)build (e.g. v0.1.4). Defaults to the app version in tauri.conf.json." | |
| required: false | |
| type: string | |
| # Only one release build per branch at a time; a newer push cancels an | |
| # in-progress one so the release reflects the latest commit. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write # needed to create/update GitHub releases | |
| env: | |
| CARGO_HTTP_MULTIPLEXING: false | |
| CARGO_HTTP_HTTP2: false | |
| # ── Version consistency gate ─────────────────────────────────────────────────── | |
| # There is no single source of truth for the app version: it's declared | |
| # separately in package.json, src-tauri/tauri.conf.json, and Cargo.toml's | |
| # [workspace.package]. They HAVE drifted before (v0.1.8 shipped with | |
| # package.json still at 0.1.7) and release tags have been cut that didn't | |
| # match tauri.conf.json at all (v0.2.7 was tagged via a manual dispatch | |
| # override while tauri.conf.json still said 0.2.6). Fail fast, before | |
| # spending build minutes, if they disagree. Always bump via | |
| # ./scripts/bump_version.sh so this can't happen. | |
| jobs: | |
| check-version-sync: | |
| name: Check version files are in sync | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Compare package.json / tauri.conf.json / Cargo.toml | |
| run: | | |
| PKG_VERSION=$(jq -r '.version' package.json) | |
| TAURI_VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) | |
| CARGO_VERSION=$(awk '/^\[workspace\.package\]/{f=1; next} /^\[/{f=0} f && /^version *=/{gsub(/"/,"",$3); print $3; exit}' Cargo.toml) | |
| echo "package.json: $PKG_VERSION" | |
| echo "tauri.conf.json: $TAURI_VERSION" | |
| echo "Cargo.toml (workspace): $CARGO_VERSION" | |
| if [ "$PKG_VERSION" != "$TAURI_VERSION" ] || [ "$PKG_VERSION" != "$CARGO_VERSION" ]; then | |
| echo "::error::Version mismatch across package.json ($PKG_VERSION), src-tauri/tauri.conf.json ($TAURI_VERSION), and Cargo.toml ($CARGO_VERSION). Run ./scripts/bump_version.sh <version> to fix, then push again." | |
| exit 1 | |
| fi | |
| # A manually-dispatched tag must match the files' version — it | |
| # exists to re-run/re-tag a build of the CURRENT version, not to | |
| # publish a release under a version the app doesn't actually report. | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then | |
| EXPECTED="v${PKG_VERSION}" | |
| if [ "${{ inputs.tag }}" != "$EXPECTED" ]; then | |
| echo "::error::Manual tag '${{ inputs.tag }}' does not match the version in the source files ($EXPECTED). Bump the version files to match the tag you want (./scripts/bump_version.sh), or dispatch with tag=$EXPECTED." | |
| exit 1 | |
| fi | |
| fi | |
| # ── Build matrix ────────────────────────────────────────────────────────────── | |
| build: | |
| needs: check-version-sync | |
| name: Build ${{ matrix.name }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false # let all variants finish even if one fails | |
| matrix: | |
| include: | |
| # ── Linux CPU ────────────────────────────────────────────────────── | |
| - name: linux-cpu | |
| os: ubuntu-22.04 | |
| features: "" | |
| artifact_label: linux-x86_64 | |
| cuda: false | |
| # ── Linux Vulkan ─────────────────────────────────────────────────── | |
| # Portable GPU AppImage: accelerates NVIDIA/AMD/Intel via the host | |
| # Vulkan driver, with CPU fallback. Far lighter to bundle than CUDA. | |
| - name: linux-vulkan | |
| os: ubuntu-22.04 | |
| features: "vulkan" | |
| artifact_label: linux-x86_64-vulkan | |
| cuda: false | |
| vulkan: true | |
| # ── Windows CPU ──────────────────────────────────────────────────── | |
| # DISABLED: the Windows build is the slowest job in the matrix and | |
| # nobody can test what it produces at the moment, so it is not worth | |
| # the wait on every push to master. | |
| # | |
| # To re-enable: uncomment the five lines below, and put the Windows | |
| # row back in the "## Downloads" table in the release body at the | |
| # bottom of this file. Every Windows-specific *step* further down | |
| # (`if: runner.os == 'Windows'`) is left in place and simply never | |
| # runs while no Windows entry exists in this matrix — nothing else | |
| # needs changing. | |
| # - name: windows-cpu | |
| # os: windows-2022 | |
| # features: "" | |
| # artifact_label: windows-x86_64 | |
| # cuda: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # ── System deps (Linux) ───────────────────────────────────────────────── | |
| # squashfs-tools (unsquashfs) is required for APPIMAGE_EXTRACT_AND_RUN=1 | |
| # which lets appimagetool/linuxdeploy run without FUSE on CI runners. | |
| - name: Install Linux system dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| libssl-dev \ | |
| libasound2-dev \ | |
| libspeechd-dev \ | |
| libxdo-dev \ | |
| patchelf \ | |
| squashfs-tools \ | |
| pkg-config \ | |
| build-essential | |
| # ── Vulkan SDK (Linux) ────────────────────────────────────────────────── | |
| # The ggml Vulkan backend compiles its compute shaders at build time with | |
| # glslc, which ships in the LunarG Vulkan SDK. End-users only need their | |
| # GPU's Vulkan driver at runtime (bundled loader finds the host ICD). | |
| - name: Install Vulkan SDK (Linux) | |
| if: runner.os == 'Linux' && matrix.vulkan | |
| run: | | |
| wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc \ | |
| | sudo tee /etc/apt/trusted.gpg.d/lunarg.asc >/dev/null | |
| sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list \ | |
| https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list | |
| sudo apt-get update -q | |
| sudo apt-get install -y vulkan-sdk | |
| glslc --version | |
| # ── Rust ──────────────────────────────────────────────────────────────── | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: release-${{ matrix.name }} | |
| # ── Node / frontend ───────────────────────────────────────────────────── | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| - run: npm install | |
| # ── Tauri build (Linux) ───────────────────────────────────────────────── | |
| # APPIMAGE_EXTRACT_AND_RUN=1 tells linuxdeploy/appimagetool to extract | |
| # and run without FUSE (uses unsquashfs from squashfs-tools instead). | |
| # QT_QPA_PLATFORM=offscreen prevents Qt platform errors in headless CI. | |
| - name: Build AppImage + deb (Linux, CPU) | |
| if: runner.os == 'Linux' && !matrix.cuda && !matrix.vulkan | |
| env: | |
| APPIMAGE_EXTRACT_AND_RUN: 1 | |
| QT_QPA_PLATFORM: offscreen | |
| # Moonshine (STT) and Inflect-Micro (TTS) are default features, so both | |
| # ONNX engines are in every build without naming them here. | |
| run: npm run tauri build | |
| # Portable GPU AppImage. whisper.cpp's Vulkan backend links only | |
| # libvulkan (lightweight, unlike CUDA), so linuxdeploy bundles it fine. | |
| - name: Build AppImage + deb (Linux, Vulkan) | |
| if: runner.os == 'Linux' && matrix.vulkan | |
| env: | |
| APPIMAGE_EXTRACT_AND_RUN: 1 | |
| QT_QPA_PLATFORM: offscreen | |
| run: npm run tauri build -- --features vulkan | |
| # linuxdeploy bundles whatever graphics/Wayland/input libraries exist on | |
| # the build host. On ubuntu-22.04 those are older than modern desktops, and | |
| # loading them instead of the host's copies breaks the Slint overlay's | |
| # Wayland frame callbacks (it renders once, then the compositor never sends | |
| # another frame) and trips "unrecognized keysym" xkbcommon errors. Strip | |
| # them so the AppImage falls through to the host's libraries at runtime — | |
| # this is what the AppImage excludelist exists for, and why a local | |
| # build_appimage.sh (which bundles the user's own newer libs) works while | |
| # the CI build did not. libvulkan is intentionally left bundled. | |
| - name: Slim AppImage to host graphics/input libraries (Linux) | |
| if: runner.os == 'Linux' && !matrix.cuda | |
| env: | |
| APPIMAGE_EXTRACT_AND_RUN: 1 | |
| run: | | |
| set -euo pipefail | |
| appimage=$(find . -path '*/bundle/appimage/*.AppImage' ! -name '*.zsync' | head -1) | |
| if [ -z "${appimage:-}" ]; then echo "No AppImage found to slim"; exit 1; fi | |
| echo "Slimming $appimage" | |
| chmod +x ./appimagetool.bin | |
| work=$(mktemp -d) | |
| cp "$appimage" "$work/in.AppImage" | |
| chmod +x "$work/in.AppImage" | |
| ( cd "$work" && ./in.AppImage --appimage-extract >/dev/null ) | |
| root="$work/squashfs-root" | |
| # Bundle WebKitGTK 4.1 helper processes (WebKitNetworkProcess, WebKitWebProcess) | |
| helper_dir="" | |
| for dir in "/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1" "/usr/lib/webkit2gtk-4.1" "/usr/lib64/webkit2gtk-4.1"; do | |
| if [ -d "$dir" ]; then | |
| helper_dir="$dir" | |
| break | |
| fi | |
| done | |
| # The bundled libwebkit2gtk has its helper-process directory compiled | |
| # in as /usr/lib/x86_64-linux-gnu/webkit2gtk-4.1, which the Tauri | |
| # bundler rewrites to ././/lib/x86_64-linux-gnu/webkit2gtk-4.1 — a | |
| # path relative to the working directory, which linuxdeploy's AppRun | |
| # sets to $APPDIR/usr. Release builds of WebKitGTK honour no | |
| # environment override for it (WEBKIT_EXEC_PATH exists only in | |
| # developer-mode builds), so the helpers must sit at exactly that | |
| # relative path. WEBKIT_INJECTED_BUNDLE_PATH is honoured, but as a | |
| # single directory, not a colon-separated search path. | |
| if [ -n "$helper_dir" ]; then | |
| echo "Found WebKitGTK helper processes in $helper_dir, bundling into AppImage..." | |
| mkdir -p "$root/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1" | |
| cp -r "$helper_dir"/* "$root/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1/" | |
| fi | |
| for script in "$root/AppRun" "$root/apprun-hooks/linuxdeploy-plugin-gtk.sh"; do | |
| [ -f "$script" ] || continue | |
| sed -i '/^export WEBKIT_EXEC_PATH=/d; /^export WEBKIT_INJECTED_BUNDLE_PATH=/d' "$script" | |
| sed -i '2i export WEBKIT_EXEC_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1"' "$script" | |
| sed -i '3i export WEBKIT_INJECTED_BUNDLE_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1/injected-bundle"' "$script" | |
| done | |
| # pocket_tts (the Pocket-TTS and Breeze-TTS-2 engines) finds its model | |
| # config at config/<variant>.yaml relative to the working directory. | |
| # Bundle it at usr/config so those engines load without changing the | |
| # process's cwd — which, per the note above, WebKitGTK depends on. | |
| mkdir -p "$root/usr/config" | |
| cp crates/voxctrl-tts/config/*.yaml "$root/usr/config/" | |
| # Some libraries can be neither stripped nor left on the library path, so | |
| # they are parked in usr/lib/fallback instead: the AppRun hook exposes each | |
| # one only when the host has no library of that soname, so a host with its | |
| # own copy always wins. | |
| # | |
| # libsystemd / libudev — the host's own libmount (Arch) links libsystemd | |
| # and needs a newer LIBSYSTEMD_* version node than the build host's | |
| # copy, while non-systemd distributions may not ship them at all and the | |
| # bundled WebKitGTK needs them. | |
| # libgstgl-1.0 / libwayland-server — the bundled WebKitGTK links both | |
| # directly, and the strip patterns below would delete them. | |
| # libgstgl-1.0.so.0 ships in libgstreamer-gl1.0-0, which | |
| # gstreamer1.0-plugins-base does not depend on, so a desktop with no | |
| # WebKit of its own can be missing it entirely and the app then dies | |
| # with "error while loading shared libraries: libgstgl-1.0.so.0". | |
| # | |
| # This has to run before the strip loop below, which skips usr/lib/fallback. | |
| # Keep this list in sync with build_appimage.sh. | |
| mkdir -p "$root/usr/lib/fallback" | |
| for pat in 'libsystemd.so*' 'libudev.so*' \ | |
| 'libgstgl-1.0.so*' 'libwayland-server.so*'; do | |
| find "$root" -name "$pat" -not -path '*/fallback/*' -print \ | |
| -exec mv -t "$root/usr/lib/fallback/" {} + 2>/dev/null || true | |
| done | |
| echo "Host-first fallback libraries:"; ls -la "$root/usr/lib/fallback" | |
| # Everything below is deleted so the AppImage falls through to the | |
| # host's copy. The rule: once a library comes from the host, every | |
| # library it links against must come from the host too, or the | |
| # host's copy resolves its symbols against our stale bundled one and | |
| # aborts at startup (seen in the wild: GStreamer needing | |
| # g_once_init_leave_pointer from GLib 2.80; GLib needing MOUNT_2_40 | |
| # from libmount; GIO's TLS module needing GNUTLS_3_8_x). A library | |
| # may only be stripped if its soname is stable across distros — | |
| # libxml2 (.so.2 vs .so.16 on Arch) and libunistring (.so.2 vs | |
| # .so.5) are deliberately kept bundled for that reason. Everything | |
| # still bundled only needs the build host's (Ubuntu 22.04) versions | |
| # of these, so any newer host satisfies them. | |
| for pat in \ | |
| 'libwayland-*.so*' 'libEGL.so*' 'libGL.so*' 'libGLX.so*' \ | |
| 'libGLdispatch.so*' 'libOpenGL.so*' 'libglapi.so*' \ | |
| 'libgbm.so*' 'libdrm.so*' \ | |
| 'libxkbcommon.so*' 'libxkbcommon-x11.so*' \ | |
| `# GLib family, and what the host's GLib/GIO link against` \ | |
| 'libglib-2.0.so*' 'libgobject-2.0.so*' 'libgio-2.0.so*' \ | |
| 'libgmodule-2.0.so*' 'libgthread-2.0.so*' \ | |
| 'libpcre2-8.so*' 'libpcre.so*' 'libffi.so*' \ | |
| 'libmount.so*' 'libblkid.so*' 'libselinux.so*' \ | |
| `# GIO loads the host's TLS module, which needs the host's gnutls stack` \ | |
| 'libgiognutls.so' 'libgnutls.so*' 'libhogweed.so*' 'libnettle.so*' \ | |
| 'libtasn1.so*' 'libp11-kit.so*' 'libidn2.so*' \ | |
| `# GStreamer, and what the host's GStreamer links against` \ | |
| 'libgstreamer-*.so*' 'libgst*.so*' 'liborc-0.4.so*' \ | |
| 'libunwind.so*' 'libdw.so*' 'libelf.so*' \ | |
| 'libbz2.so*' 'liblzma.so*' 'libzstd.so*'; do | |
| find "$root" -name "$pat" -not -path '*/fallback/*' -print -delete 2>/dev/null || true | |
| done | |
| # Install the AppRun hook that exposes usr/lib/fallback host-first. | |
| cp scripts/appimage-hooks/host-first-fallback.sh "$root/apprun-hooks/" | |
| chmod +x "$root/apprun-hooks/host-first-fallback.sh" | |
| if ! grep -q "host-first-fallback.sh" "$root/AppRun"; then | |
| sed -i '/^exec /i source "$this_dir"/apprun-hooks/host-first-fallback.sh' "$root/AppRun" | |
| fi | |
| # Repackage. appimage-pack.sh embeds a runtime that also works on | |
| # hosts without libfuse2 (Ubuntu 22.04 / Linux Mint 21 and newer ship | |
| # fuse3 only), falling back to appimagetool's own runtime if it | |
| # cannot fetch one, and verifies the result unpacks before returning. | |
| ./scripts/appimage-pack.sh "$root" "$appimage" ./appimagetool.bin | |
| echo "Repackaged $appimage ($(du -h "$appimage" | cut -f1))" | |
| # CUDA build uses --bundles deb only: the CUDA toolkit populates system | |
| # library paths with hundreds of CUDA .so files that linuxdeploy tries to | |
| # bundle into the AppImage, causing it to OOM or fail. deb packaging does | |
| # not use linuxdeploy and works reliably. CUDA users on Linux are almost | |
| # always on Debian/Ubuntu and can install the .deb directly. | |
| # ── Tauri build (Windows) ─────────────────────────────────────────────── | |
| # NSIS (setup.exe) only — the advertised Windows installer. The WiX/MSI | |
| # bundler's `light.exe` fails to harvest the voxctrl-overlay sidecar, and | |
| # the MSI is a secondary, unadvertised artifact. Call tauri directly so | |
| # `--bundles nsis` reaches tauri instead of cargo. | |
| - name: Build installer (Windows, CPU) | |
| if: runner.os == 'Windows' | |
| run: npx tauri build --bundles nsis | |
| # ── Collect and rename Linux artifacts ────────────────────────────────── | |
| # Search the whole workspace so the path is correct regardless of whether | |
| # cargo uses a workspace-level or package-level target directory. | |
| - name: Collect Linux artifacts | |
| if: runner.os == 'Linux' | |
| run: | | |
| mkdir -p upload | |
| find . \( -name "*.AppImage" -o -name "*.deb" \) \ | |
| ! -name "*.AppImage.zsync" \ | |
| ! -path "*/.cargo/*" | \ | |
| while read -r f; do | |
| base=$(basename "$f") | |
| stem="${base%.*}" | |
| ext="${base##*.}" | |
| dest="upload/${stem}-${{ matrix.artifact_label }}.${ext}" | |
| cp "$f" "$dest" | |
| echo "Collected: $dest ($(du -sh "$f" | cut -f1))" | |
| done | |
| ls -lh upload/ | |
| # ── Collect and rename Windows artifacts ──────────────────────────────── | |
| # Search the whole workspace so the path is correct regardless of where | |
| # Tauri/cargo places the bundle directory. | |
| - name: Collect Windows artifacts | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force upload | Out-Null | |
| Get-ChildItem -Path . -Recurse -Include "*.exe","*.msi" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -like "*\bundle\*" -and $_.Name -notlike "*-stub*" } | | |
| ForEach-Object { | |
| $dest = "upload\$($_.BaseName)-${{ matrix.artifact_label }}$($_.Extension)" | |
| Copy-Item $_.FullName $dest | |
| $size = "{0:N1} MB" -f ($_.Length / 1MB) | |
| Write-Host "Collected: $dest ($size)" | |
| } | |
| if ((Get-ChildItem upload).Count -eq 0) { | |
| Write-Host "ERROR: No bundle files found. Searching for any .exe/.msi to diagnose:" | |
| Get-ChildItem -Path . -Recurse -Include "*.exe","*.msi" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -notlike "*\.cargo*" } | | |
| Select-Object -ExpandProperty FullName | |
| exit 1 | |
| } | |
| # ── Upload to Actions (picked up by publish job) ───────────────────────── | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifacts-${{ matrix.name }} | |
| path: upload/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # ── Publish release ─────────────────────────────────────────────────────────── | |
| publish: | |
| name: Publish GitHub release | |
| needs: build | |
| runs-on: ubuntu-latest | |
| # Run even if some build variants failed so partial releases still publish | |
| if: always() && contains(needs.build.result, 'success') | |
| steps: | |
| # Needed to read the app version for the default tag name. | |
| - uses: actions/checkout@v4 | |
| - name: Resolve tag name | |
| id: tag | |
| run: | | |
| # Manual dispatch may override the tag; otherwise derive it from the | |
| # app version (e.g. 0.2.4 -> v0.2.4). | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ inputs.tag }}" ]]; then | |
| echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" | |
| else | |
| VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) | |
| echo "name=v${VERSION}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Download everything produced by the build matrix | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: release-artifacts | |
| merge-multiple: true | |
| - name: List release artifacts | |
| run: ls -lh release-artifacts/ | |
| - name: Create / update draft release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.name }} | |
| name: "VoxCtrl ${{ steps.tag.outputs.name }}" | |
| draft: true | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| files: release-artifacts/* | |
| # The "What's new" section below is version-specific: update it in the | |
| # same commit that bumps the version (scripts/bump_version.sh), so a | |
| # release never ships the previous version's notes. Everything under | |
| # "## Downloads" is evergreen, and GitHub appends its own generated | |
| # "What's Changed" list of merged PRs beneath all of this. | |
| # | |
| # The Downloads table lists no Windows build because the Windows job | |
| # is commented out of the build matrix above; a row for a file this | |
| # workflow does not produce would send people looking for a download | |
| # that is not there. Restore the row when the job comes back: | |
| # | Windows | `-windows-x86_64-setup.exe` | None | | |
| # Note this is a literal YAML block scalar — a `#` inside `body:` | |
| # would be published as text, which is why the row is removed rather | |
| # than commented out. | |
| body: | | |
| ## What's new in 0.4.0 | |
| The largest release so far: a new machine is walked through setup instead of | |
| dropped into a settings window, VoxCtrl can update itself, output targets are | |
| now Output Commands and say how to use them, and a settings audit removed or | |
| implemented every control that was doing nothing. | |
| ### First-run setup wizard | |
| A first launch used to open Settings on a config nobody had chosen — the | |
| default model downloading in the background, a `Super+Space` binding the user | |
| never saw, and no sign whether any of it worked. The first thing that told you | |
| something was wrong was pressing a key and getting silence. | |
| A fresh install now gets a seven-step wizard in its own window: pick a | |
| transcription engine and model size (downloaded before you continue), bind a | |
| hotkey and register it with your desktop, choose an overlay, dictate a live | |
| test, add a voice, and finish on a screen that reports anything that failed. | |
| Every choice is written to the config as it is made, so quitting halfway keeps | |
| what you picked. | |
| - The hotkey step offers only gestures your desktop's shortcut backend can | |
| actually deliver, and will not let you past until the desktop has *accepted* | |
| the binding — because the next screen is a live dictation test that would | |
| otherwise be guaranteed to fail. | |
| - The test step is genuinely end to end: the transcript is typed into the | |
| wizard's own textarea down the real delivery path. | |
| - The first binding is pointed at a "Command" output, so voice commands work | |
| the day you add a second one, with nothing to re-bind. | |
| - The window sizes itself to the display it opens on, so a 1080p desktop at | |
| 125% scaling no longer puts the Continue button past the bottom edge. | |
| Run it again any time with `voxctrl --setup`, or Settings → General → "Open | |
| setup wizard". An upgrade never triggers it: a config file written by an | |
| earlier VoxCtrl reads as already set up. | |
| ### VoxCtrl updates itself | |
| An AppImage sits where you downloaded it, so until now the only way to update | |
| was to notice the release yourself and fetch the right file by hand. | |
| Ten seconds after launch, VoxCtrl asks GitHub what the latest release is. If | |
| there is a newer one, a window shows the version, the release notes and the | |
| download size, and offers **Update and restart**, **Skip this version** or | |
| **Not now**. Confirming downloads the build matching your installation — CPU | |
| AppImage or Vulkan AppImage — verifies it against the SHA-256 checksum GitHub | |
| published, replaces the running file and restarts into it. | |
| Nothing is replaced until a complete, verified file is on disk beside the one | |
| it replaces, and the swap is a single rename. A failed download, a checksum | |
| mismatch or a directory you cannot write to each leave the working version | |
| exactly as it was, and "cannot write to" is detected *before* the download | |
| rather than after 100 MB of it. A `.deb`, a distro package or a build from | |
| source is told why it cannot self-update instead of being given a button that | |
| cannot work. | |
| **On privacy:** this is the first thing VoxCtrl does on the network without | |
| being asked. It is one unauthenticated GET to the public releases API, sending | |
| a `User-Agent` of `VoxCtrl/<version>` and nothing else — no cookie, no account, | |
| no install ID, nothing about you or your machine. Settings → General → | |
| "Check for a new version on launch" turns it off, as does one click in the | |
| update window; with it off VoxCtrl makes no request unless you press "Check | |
| now". docs/privacy.md documents exactly what is sent and how to watch it with | |
| `tcpdump`. | |
| ### Output Targets are now Output Commands | |
| "Output Targets" described where text lands, which is the implementer's view. | |
| What you actually do with one is say its name mid-dictation — and nothing in | |
| the app ever said so. | |
| The tab is now **Output Commands**, and it opens with the thing you need to | |
| know: start dictation, say *"VoxCtrl"*, then the command's name, then your | |
| text. *"VoxCtrl notes, remember to call the plumber"* sends the rest to the | |
| command named **notes**. Dictation with no such phrase in it goes wherever your | |
| hotkey already points. | |
| Nothing on disk changed: still `targets.toml`, still `[[target]]`, still | |
| `target_id` in bindings. No existing config, script or integration is touched. | |
| ### Settings that were doing nothing — implemented or removed | |
| An audit found controls that had never been wired to anything. Each was taken | |
| in the direction it deserved. | |
| **Now real:** | |
| - **Noise suppression** runs RNNoise on the capture path, and the toggle | |
| applies without a restart. | |
| - **MCP record timeout** is now the default `transcribe_voice` listens for when | |
| the caller omits one, instead of a hardcoded 15 seconds, and is advertised in | |
| the tool schema. | |
| - **Breeze-TTS-2 GPU** loads the model onto a CUDA or Metal device (opt-in at | |
| build time; falls back to CPU with a warning). | |
| - **File targets choose their timestamp format**, validated as you type, with | |
| a bad pattern falling back to the default rather than costing you a | |
| dictation. | |
| **Removed, because nothing read them:** the AT-SPI2 section, Inference mode, | |
| Silence duration, the TTS custom-vocabulary box, Breeze's temperature slider, | |
| quiet mode, the Backend "Auto-detect" entry (it always resolved to | |
| whisper.cpp), and four unwired options in the target editor. Existing | |
| `config.json` and `targets.toml` files keep loading — the removed keys are | |
| simply ignored. | |
| ### Fixes and smaller changes | |
| - **Dictation no longer appends a newline.** It appends a single trailing space | |
| instead, so consecutive utterances do not run their words together; text that | |
| already ends in whitespace is left alone. | |
| - **The delivery-type dropdown is no longer clipped** by the editor's scrolling | |
| body — it could previously cut off mid-list with the rest unreachable. | |
| - **Clicking the tray now actually raises Settings** when it is open behind | |
| another window, instead of blinking the taskbar entry. | |
| - **Windows close when you close them**, and the app carries on in the tray | |
| rather than treating the last window as a quit. | |
| - **The HuggingFace token is asked for once** in the wizard and stored once; an | |
| exported `HF_TOKEN` wins and is never written to the config. | |
| - The settings sidebar sizes itself to its longest label instead of clipping it | |
| on wide system fonts. | |
| - The transcript history feature is gone. | |
| ### Windows builds are paused | |
| **This release has no Windows download.** The Windows job is the slowest leg of | |
| the build and nothing it produced was being tested, so it is commented out of | |
| the release workflow rather than removed — restoring it is uncommenting five | |
| lines. Windows support in the code is untouched; only the automated build is | |
| off. Build from source in the meantime. | |
| ### Requirements | |
| glibc 2.35 or newer and a GCC 12 libstdc++ — Ubuntu 22.04+, Linux | |
| Mint 21+, Debian 12+, Fedora 36+, Arch, openSUSE Tumbleweed. Ubuntu | |
| 20.04, Mint 20, Debian 11 and RHEL 9 are below that baseline; build | |
| from source there. | |
| --- | |
| ## Downloads | |
| Pick the build for your platform and GPU: | |
| | Platform | File suffix | GPU required | | |
| |---|---|---| | |
| | Linux | `-linux-x86_64.AppImage` | None | | |
| | Linux (GPU) | `-linux-x86_64-vulkan.AppImage` | Any Vulkan GPU | | |
| **The Vulkan AppImage** is a portable GPU build: it accelerates any | |
| NVIDIA/AMD/Intel GPU via the host Vulkan driver, and falls back to | |
| CPU when no GPU is present. | |
| **CPU builds** run on any hardware, including AMD, Intel, and ARM. | |
| ### Linux CPU / GPU — first run | |
| Needs glibc 2.35 or newer (Ubuntu 22.04+, Linux Mint 21+, Debian | |
| 12+, Fedora 36+, Arch). Nothing has to be installed first — not | |
| even `libfuse2`. | |
| ```bash | |
| chmod +x VoxCtrl_*-linux-x86_64*.AppImage | |
| # Optional: installs the desktop entry and text-injection helpers. | |
| # Global shortcuts need no permissions — your desktop handles them. | |
| ./VoxCtrl_*-linux-x86_64*.AppImage --install | |
| # Launch the application | |
| ./VoxCtrl_*-linux-x86_64*.AppImage | |
| ``` |