aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-13 13:43:23 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-13 13:43:23 +0100
commit5c241d65ed4a6ec2bc3e5d75d6858ed6722f1b17 (patch)
tree9885503fbde0324c04e51c6cd70bce143c4a8355
parentd2da7ff900f360c0eec1e17f62df1358651ed77c (diff)
downloaddotfiles-5c241d65ed4a6ec2bc3e5d75d6858ed6722f1b17.tar.gz
dotfiles-5c241d65ed4a6ec2bc3e5d75d6858ed6722f1b17.tar.bz2
dotfiles-5c241d65ed4a6ec2bc3e5d75d6858ed6722f1b17.zip
feat(sway): add dictate (whisper.cpp) and ocr (tesseract) keybinds
Push-to-talk dictation toggle on Super+i: parecord captures 16 kHz mono WAV, whisper-cli transcribes (auto language), output is typed via wtype and copied to the clipboard. Region OCR on Super+Shift+o: slurp + grim feed tesseract (eng+por), result lands in the clipboard with a notification preview. Adds wtype to wayland.txt; tesseract (+eng/por data) and whisper.cpp + the large-v3-turbo-q5_0 model package to extra.txt.
-rw-r--r--KEYBINDS.md2
-rw-r--r--dot_config/sway/config6
-rw-r--r--dot_local/bin/executable_dictate87
-rw-r--r--dot_local/bin/executable_ocr39
-rw-r--r--meta/extra.txt9
-rw-r--r--meta/wayland.txt3
6 files changed, 146 insertions, 0 deletions
diff --git a/KEYBINDS.md b/KEYBINDS.md
index 1988f6e..3f203c1 100644
--- a/KEYBINDS.md
+++ b/KEYBINDS.md
@@ -324,6 +324,8 @@ Mod key: `Super` (Mod4). Only personal additions beyond sway defaults listed.
| `XF86RFKill` | Toggle all radios (rfkill) |
| `Print` | Region screenshot (grim+slurp) |
| `Shift+Print` | Full screenshot (grim) |
+| `Super+i` | Dictate toggle (whisper.cpp โ†’ wtype + clipboard) |
+| `Super+Shift+o` | OCR region (tesseract โ†’ clipboard) |
| `Super+Shift+s` | Lock screen + pause media |
| `Super+n` | Dismiss notification |
| `Super+Shift+n` | Dismiss all notifications |
diff --git a/dot_config/sway/config b/dot_config/sway/config
index edf1e32..c1d897f 100644
--- a/dot_config/sway/config
+++ b/dot_config/sway/config
@@ -157,6 +157,12 @@ bindsym XF86RFKill exec rfkill toggle all
bindsym Print exec sh -c 'grim -g "$(slurp)" - | tee ~/pics/screenshots/$(date +%Y-%m-%d-%H-%M-%S).png | wl-copy'
bindsym Shift+Print exec sh -c 'grim - | tee ~/pics/screenshots/$(date +%Y-%m-%d-%H-%M-%S).png | wl-copy'
+# Dictation (push-to-talk toggle, whisper.cpp)
+bindsym $mod+i exec ~/.local/bin/dictate
+
+# OCR โ€” crop a region, copy text to clipboard (tesseract)
+bindsym $mod+Shift+o exec ~/.local/bin/ocr
+
# Lock & pause
bindsym $mod+Shift+s exec "playerctl -a pause; swaylock -f -e -c 282828"
diff --git a/dot_local/bin/executable_dictate b/dot_local/bin/executable_dictate
new file mode 100644
index 0000000..2e8a361
--- /dev/null
+++ b/dot_local/bin/executable_dictate
@@ -0,0 +1,87 @@
+#!/usr/bin/env sh
+# Push-to-talk dictation. Toggle: 1st invocation starts recording;
+# 2nd stops, transcribes via whisper.cpp, types the result into the focused
+# window with wtype, and copies it to the clipboard.
+#
+# Pacman: pipewire-pulse (parecord), wtype, wl-clipboard, libnotify
+# AUR: whisper.cpp, whisper.cpp-model-large-v3-turbo-q5_0
+#
+# Override via env:
+# WHISPER_MODEL path to a ggml-*.bin model
+# WHISPER_LANG language code or 'auto' (default: auto)
+
+set -eu
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}/dictate"
+pid_file="$state_dir/pid"
+wav_file="$state_dir/audio.wav"
+log_file="$state_dir/whisper.log"
+default_model='/usr/share/whisper.cpp-model-large-v3-turbo-q5_0/ggml-large-v3-turbo-q5_0.bin'
+model="${WHISPER_MODEL:-$default_model}"
+lang="${WHISPER_LANG:-auto}"
+
+mkdir -p "$state_dir"
+
+is_recording() {
+ [ -r "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null
+}
+
+start_recording() {
+ if ! command -v whisper-cli >/dev/null; then
+ notify-send -u critical "๐ŸŽ™๏ธ dictate" \
+ "whisper-cli not found. Install whisper.cpp (AUR)."
+ exit 1
+ fi
+ if [ ! -r "$model" ]; then
+ notify-send -u critical "๐ŸŽ™๏ธ dictate" \
+ "Model missing: $model. Install whisper.cpp-model-large-v3-turbo-q5_0 (AUR)."
+ exit 1
+ fi
+ rm -f "$wav_file"
+ parecord --format=s16le --rate=16000 --channels=1 "$wav_file" \
+ >"$log_file" 2>&1 &
+ echo "$!" >"$pid_file"
+ notify-send -t 1500 "๐ŸŽ™๏ธ Recordingโ€ฆ" "Press the bind again to stop."
+}
+
+stop_and_transcribe() {
+ pid="$(cat "$pid_file")"
+ rm -f "$pid_file"
+ kill -TERM "$pid" 2>/dev/null || true
+ # Give parecord up to 2s to flush the WAV header.
+ i=0
+ while kill -0 "$pid" 2>/dev/null && [ "$i" -lt 20 ]; do
+ sleep 0.1
+ i=$((i + 1))
+ done
+ kill -KILL "$pid" 2>/dev/null || true
+
+ if [ ! -s "$wav_file" ]; then
+ notify-send -u low "๐ŸŽ™๏ธ dictate" "No audio captured."
+ exit 1
+ fi
+
+ notify-send -t 1500 "๐ŸŽ™๏ธ Transcribingโ€ฆ"
+
+ text="$(
+ whisper-cli -m "$model" -f "$wav_file" \
+ -l "$lang" -nt -np -t "$(nproc)" 2>"$log_file" \
+ | tr -s '[:space:]' ' ' \
+ | sed -e 's/^ //; s/ $//'
+ )"
+
+ if [ -z "$text" ]; then
+ notify-send -u low "๐ŸŽ™๏ธ dictate" "Empty transcription. See $log_file."
+ exit 1
+ fi
+
+ printf '%s' "$text" | wl-copy
+ wtype -- "$text"
+ notify-send -t 2500 "๐ŸŽ™๏ธ Dictated" "$text"
+}
+
+if is_recording; then
+ stop_and_transcribe
+else
+ start_recording
+fi
diff --git a/dot_local/bin/executable_ocr b/dot_local/bin/executable_ocr
new file mode 100644
index 0000000..6f6191f
--- /dev/null
+++ b/dot_local/bin/executable_ocr
@@ -0,0 +1,39 @@
+#!/usr/bin/env sh
+# OCR a screen region (default) or an image file โ†’ clipboard.
+#
+# Usage:
+# ocr # interactive: select a region with slurp, OCR it
+# ocr <file> # OCR an image file
+#
+# Requires: tesseract (+tesseract-data-eng, tesseract-data-por),
+# grim, slurp, wl-clipboard, libnotify.
+#
+# Override languages via TESSERACT_LANG (e.g. TESSERACT_LANG=eng).
+
+set -eu
+
+lang="${TESSERACT_LANG:-eng+por}"
+
+if [ "${1:-}" ]; then
+ [ -r "$1" ] || { notify-send -u critical "๐Ÿ“‹ OCR" "Cannot read: $1"; exit 1; }
+ text="$(tesseract "$1" - -l "$lang" 2>/dev/null || true)"
+else
+ region="$(slurp 2>/dev/null)" || exit 0
+ text="$(grim -g "$region" - | tesseract - - -l "$lang" 2>/dev/null || true)"
+fi
+
+# Trim trailing whitespace per line; collapse runs of blank lines; drop
+# leading blanks.
+text="$(printf '%s\n' "$text" | awk '
+ { sub(/[[:space:]]+$/, "") }
+ NF { print; blank = 0; next }
+ !blank { print; blank = 1 }
+' | sed -e '/./,$!d')"
+
+if [ -z "$text" ]; then
+ notify-send -u low "๐Ÿ“‹ OCR" "No text detected."
+ exit 1
+fi
+
+printf '%s' "$text" | wl-copy
+notify-send -t 3000 "๐Ÿ“‹ OCR copied" "$(printf '%s' "$text" | head -c 200)"
diff --git a/meta/extra.txt b/meta/extra.txt
index f6082d9..fb87c3d 100644
--- a/meta/extra.txt
+++ b/meta/extra.txt
@@ -3,3 +3,12 @@ pandoc-bin
syncthing
udisks2
autenticacao-gov-pt-bin
+
+# OCR (used by ~/.local/bin/ocr)
+tesseract
+tesseract-data-eng
+tesseract-data-por
+
+# Speech-to-text (used by ~/.local/bin/dictate)
+whisper.cpp
+whisper.cpp-model-large-v3-turbo-q5_0
diff --git a/meta/wayland.txt b/meta/wayland.txt
index fa0f26f..91d68b4 100644
--- a/meta/wayland.txt
+++ b/meta/wayland.txt
@@ -30,6 +30,9 @@ grim
slurp
wf-recorder
+# Wayland typing (used by dictate, etc)
+wtype
+
# Image viewer
imv