Skip to content

[WIP] new: add local whisper.cpp provider + optional Wayland paste workaround - #1

Open
njeudy wants to merge 6 commits into
vaab:masterfrom
Alusage:feat/whisper-local-provider
Open

[WIP] new: add local whisper.cpp provider + optional Wayland paste workaround#1
njeudy wants to merge 6 commits into
vaab:masterfrom
Alusage:feat/whisper-local-provider

Conversation

@njeudy

@njeudy njeudy commented May 13, 2026

Copy link
Copy Markdown

Hey Valentin,

WIP draft — I've been running this locally for a few days and figured I'd share the diff in case it's useful, or in case you'd like to leave feedback on the approach.

What's in here

1. Local Whisper provider

Adds a third transcription backend that runs whisper.cpp via the whisper-rs crate, optionally CUDA-accelerated. Useful when you'd rather not send audio to a cloud provider. The user drops a GGML model file on disk, points providers.whisper_local.model_path at it, and that's it.

providers:
  whisper_local:
    model_path: ~/.local/share/talk-rs/models/ggml-large-v3.bin
    language: fr               # or "auto"
    use_gpu: true
    threads: 4
    initial_prompt: >-         # local equivalent of context_bias
      Project names, technical terms, proper nouns…

transcription:
  default_provider: whisper_local

On my RTX 4070, large-v3 loads in ~700 ms (file in page cache) and transcribes faster than realtime. Cloud providers are untouched — same config, same factory dispatch.

Realtime + diarization aren't supported by the local backend; the factory returns clear errors when those flags are passed.

2. Optional Wayland paste workaround

Under GNOME Wayland, the X11 XTEST fake Ctrl+Shift+V keystroke used by paste_text_to_target gets blocked by the compositor for Wayland-native apps. Worse: the clipboard restore at the end of the function then wipes the transcription, so even a manual Ctrl+Shift+V finds nothing.

This PR adds an opt-in paste.no_paste: true config flag that short-circuits both the XTEST call and the clipboard restoration. The transcription is left in the X11 clipboard and piped into wl-copy (best-effort) so the Wayland-native clipboard has it too. Combined with a clipboard manager (GPaste, Pano, Copyous…) this gives a reliable ""transcribe → Ctrl+V"" workflow on Wayland.

The flag is off by default — existing X11 setups are unaffected.

Caveats / things you might want to push back on

  • Rust isn't my main language — I leaned heavily on Claude to scaffold the whisper_local module and the new config types. Happy to revisit any pattern you'd write differently (error mapping, the OnceLock<WhisperContext> lazy init, the audio decoding via ogg/opus/hound, the spawn_blocking wrapping, etc.). Feedback very welcome — feel free to be blunt.
  • I'm not 100% sure the no_paste commit is needed for everyone on Wayland — I had a confusing test earlier where I was actually running talk-rs dictate (no --toggle) in a terminal, which printed everything to stdout. After fixing that I still observed the auto-paste failing on Wayland-native apps, so I think the flag is real, but feel free to drop the commit if you'd rather keep paste_text_to_target simple.
  • libssl-dev was missing from the Debian build deps in the README — I added it. The build fails without it (transitive dep of reqwest/tokio-tungstenite via openssl-sys).
  • The whisper_local Provider variant participates in every match provider { … } site (3 in dictate/models.rs, 1 in dictate/realtime.rs); I went through the exhaustive cases rather than adding a catch-all wildcard.

Commits

new: [deps] add whisper-rs, hound and tempfile for local Whisper backend
new: [config] add WhisperLocal provider variant and WhisperLocalConfig
new: [transcription] add local whisper.cpp batch backend with CUDA support
new: [dictate] support WhisperLocal in model resolver and realtime guard
new: [paste] add ``paste.no_paste`` clipboard-only mode for Wayland
doc: [readme] document local Whisper backend and Wayland paste workflow

Verification on my side

  • cargo fmt, cargo clippy --all-targets clean (0 warnings)
  • cargo test --release passes (only the pre-existing xclip integration test fails on my Wayland box; not introduced by this PR)
  • cargo build --release produces a 129 MB binary (statically linked whisper.cpp + CUDA)
  • End-to-end: talk-rs transcribe synthetic.wav --provider whisper_local validated; daemon --toggle workflow validated under GNOME Wayland with paste.no_paste: true

Cheers — and thanks for talk-rs, it's become my daily dictation setup. ❤️

njeudy added 6 commits May 13, 2026 11:08
- ``whisper-rs`` 0.16 with ``cuda`` and ``log_backend`` features for
  GPU-accelerated whisper.cpp inference.
- ``hound`` for decoding 16 kHz mono WAV input files in pure Rust.
- ``tempfile`` promoted from dev-dependencies to runtime dependencies:
  the streaming transcribe path buffers chunks to a temp file before
  decoding (no streaming OGG/Opus decoder is available yet).
- ``Provider::WhisperLocal`` variant alongside Mistral and OpenAI,
  with ``Display``, ``FromStr`` and serde aliases for
  ``whisper-local`` / ``whisperlocal``.
- ``ProvidersConfig.whisper_local: Option<WhisperLocalConfig>`` so
  configs that don't use the local backend stay untouched.
- ``WhisperLocalConfig`` fields: ``model_path`` (required GGML file),
  ``language`` (default ``auto``), ``use_gpu`` (default ``true``),
  ``initial_prompt`` (whisper.cpp equivalent of the cloud
  ``context_bias``), ``threads`` (default 4).
- ``TALK_RS_PROVIDERS_WHISPER_LOCAL_{MODEL_PATH,LANGUAGE,INITIAL_PROMPT}``
  environment overrides mirroring the existing Mistral/OpenAI pattern.
- Provider rename to ``snake_case`` serde casing so YAML keys are
  ``whisper_local`` (existing ``mistral``/``openai`` keys are
  unchanged because they have no underscore).
…pport

``WhisperLocalBatchTranscriber`` implements ``BatchTranscriber`` using
the ``whisper-rs`` bindings to whisper.cpp.  Inference runs on the
GPU when the binary is built with the ``cuda`` feature (or any other
``_gpu`` backend), or on the CPU otherwise.

Key design points:

- The ``WhisperContext`` is loaded lazily inside a ``OnceLock`` on the
  first ``transcribe_file`` call.  This keeps the daemon spawn fast:
  the multi-second model-load cost is paid only when the recording
  has already been captured, not before audio capture starts.
- Inference is dispatched to ``tokio::task::spawn_blocking`` so the
  reactor isn't stalled by the synchronous whisper.cpp call.
- Audio decoding is pure Rust using existing dependencies:
  - OGG/Opus via the ``ogg`` + ``opus`` crates (the format produced
    by ``talk-rs record``).
  - WAV via ``hound`` (16 kHz mono, 16-bit int or 32-bit float).
  - Other formats return a clear error pointing at ``ffmpeg``.
- Streaming transcription buffers OGG chunks to a temp file then
  delegates to ``transcribe_file`` — true streaming decode is not
  available yet for whisper.cpp.
- Diarization is not supported by whisper.cpp; the factory rejects
  ``--diarize`` with a helpful error.
- Realtime is not supported either; the factory returns a clear
  error explaining how to fall back to batch mode.
- ``initial_prompt`` is wired to whisper's ``set_initial_prompt`` so
  users can bias the decoder toward names and technical terms
  (the local equivalent of the cloud ``context_bias`` field).

The transcription dispatcher (``create_batch_transcriber``,
``create_realtime_transcriber``, ``is_model_error``,
``enrich_model_error``) is extended to route ``Provider::WhisperLocal``
to the new backend.
The model resolver and the retry-candidates builder gain a
``WhisperLocal`` branch:

- ``resolve_model`` returns the GGML file stem (e.g. ``ggml-large-v3``)
  as the effective "model name" when ``Provider::WhisperLocal`` is
  selected — the local backend has no API-served model catalog, so
  the file name is the closest semantic equivalent.
- ``add_known_models_with_streaming`` and ``add_known_realtime_models``
  return empty slices for ``WhisperLocal``: we can't enumerate other
  local GGML files on disk, so there is no retry catalog to offer.
- The realtime ``provider_specific`` metadata builder returns ``None``
  for ``WhisperLocal``.  This branch is unreachable in practice
  because the realtime factory rejects ``WhisperLocal`` upstream,
  but the match must stay exhaustive.
Under a Wayland compositor the X11 ``XTEST`` fake keystroke used by
the standard paste path is blocked, leaving the transcription out of
reach of the focused application.  Worse: talk-rs restores the
previous clipboard after the failed paste, so even a manual
``Ctrl+Shift+V`` finds nothing useful.

Add an opt-in mode ``paste.no_paste: true`` that skips both the
fake keystroke and the clipboard restoration.  The transcription is
written to the X11 clipboard and, when ``$WAYLAND_DISPLAY`` is set,
also piped into ``wl-copy`` (best-effort) so Wayland-native apps
have access to it.  Combined with a clipboard manager (GPaste,
Pano, Copyous, …) this yields a "transcribe → Ctrl+V" workflow that
works reliably on GNOME Wayland.

- ``PasteConfig.no_paste`` defaults to ``false`` so existing X11
  installs are unaffected.
- ``paste_text_to_target`` takes a new ``no_paste: bool`` argument
  and short-circuits before any XTest call when the flag is set.
- Both call sites (dictate batch path, picker selection path) read
  the value from ``config.paste.no_paste`` and propagate it.
- ``write_to_wayland_clipboard`` is a small ``wl-copy`` wrapper that
  fails silently with a single warn-level log when the binary is
  missing — no hard runtime dependency on ``wl-clipboard``.
Cover three topics that the README was missing:

- ``libssl-dev`` / ``openssl-devel`` is required for the reqwest
  TLS stack to compile.  Document it next to the other build deps.
- A full "Local whisper.cpp" subsection in *Prerequisites*:
  optional ``nvidia-cuda-toolkit`` for the GPU build, link to the
  GGML model catalog, and a ready-to-run ``curl`` line for
  ``ggml-large-v3.bin``.
- A "Wayland tips" subsection explaining why the default XTEST
  paste path breaks on Wayland, how ``paste.no_paste`` solves it,
  the optional ``wl-clipboard`` package, and pointers to popular
  clipboard managers (GPaste, Pano, Copyous).

The configuration tables now list ``providers.whisper_local.*`` and
``paste.no_paste``; ``config.example.yaml`` gains commented sample
sections for both with inline documentation.
@njeudy

njeudy commented May 13, 2026

Copy link
Copy Markdown
Author

@vaab I use ia for this, so you can accept or not this PR, modify it, i use it locally for now :)

@vaab

vaab commented May 29, 2026

Copy link
Copy Markdown
Owner

Many thanks for the interest. Alas, I'm currently totally underwater, and I don't have neither a local Whisper (I would recommend local voxtral BTW), nor a good graphic card, and even less so a Wayland install. I also committed a lot in between, and even had a deep look at your PR to integrate still what I could, and I'm afraid this will be too hard to just integrate as such, which is a pity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants