digue 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digue-0.1.0/LICENSE +7 -0
- digue-0.1.0/PKG-INFO +460 -0
- digue-0.1.0/README.md +431 -0
- digue-0.1.0/digue/__init__.py +67 -0
- digue-0.1.0/digue/__main__.py +6 -0
- digue-0.1.0/digue/audio.py +471 -0
- digue-0.1.0/digue/benchmark.py +316 -0
- digue-0.1.0/digue/cli.py +402 -0
- digue-0.1.0/digue/config.py +443 -0
- digue-0.1.0/digue/container.py +885 -0
- digue-0.1.0/digue/convert.py +273 -0
- digue-0.1.0/digue/delivery.py +165 -0
- digue-0.1.0/digue/dictate.py +424 -0
- digue-0.1.0/digue/notify.py +106 -0
- digue-0.1.0/digue/recording.py +1000 -0
- digue-0.1.0/digue/transcribe.py +841 -0
- digue-0.1.0/digue.egg-info/PKG-INFO +460 -0
- digue-0.1.0/digue.egg-info/SOURCES.txt +35 -0
- digue-0.1.0/digue.egg-info/dependency_links.txt +1 -0
- digue-0.1.0/digue.egg-info/entry_points.txt +2 -0
- digue-0.1.0/digue.egg-info/top_level.txt +1 -0
- digue-0.1.0/pyproject.toml +85 -0
- digue-0.1.0/setup.cfg +4 -0
- digue-0.1.0/tests/test_audio.py +664 -0
- digue-0.1.0/tests/test_batch.py +288 -0
- digue-0.1.0/tests/test_benchmark.py +432 -0
- digue-0.1.0/tests/test_clean.py +247 -0
- digue-0.1.0/tests/test_cli.py +233 -0
- digue-0.1.0/tests/test_config.py +610 -0
- digue-0.1.0/tests/test_container.py +1002 -0
- digue-0.1.0/tests/test_convert.py +290 -0
- digue-0.1.0/tests/test_delivery.py +621 -0
- digue-0.1.0/tests/test_dictate.py +609 -0
- digue-0.1.0/tests/test_library.py +130 -0
- digue-0.1.0/tests/test_notify.py +200 -0
- digue-0.1.0/tests/test_recording.py +1919 -0
- digue-0.1.0/tests/test_transcribe.py +752 -0
digue-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright © 2026 Pythonic Café
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
digue-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: digue
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local speech-to-text dictation and transcription using whisper.cpp
|
|
5
|
+
Author-email: Álvaro Justen <alvarojusten@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/turicas/digue
|
|
8
|
+
Project-URL: Repository, https://github.com/turicas/digue
|
|
9
|
+
Project-URL: Issues, https://github.com/turicas/digue/issues
|
|
10
|
+
Keywords: whisper,dictation,speech-to-text,transcription,stt,cli,docker
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
23
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# digue
|
|
31
|
+
|
|
32
|
+
Local speech-to-text dictation and transcription using [whisper.cpp](https://github.com/ggml-org/whisper.cpp). A stdlib-only Python package with zero pip dependencies.
|
|
33
|
+
|
|
34
|
+
Press a keybinding to start recording, press again to stop. The transcribed text is pasted into the focused window. Also works as a CLI for transcribing audio/video files and simplifying VTT subtitles.
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
`digue` manages a whisper-server Docker container with automatic GPU detection. On first run it detects your hardware, downloads the model, pulls the right Docker image and creates the container. Subsequent runs just start/stop it.
|
|
39
|
+
|
|
40
|
+
Supported backends:
|
|
41
|
+
|
|
42
|
+
| Backend | Docker image | Acceleration | Detection |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `nvidia` | `main-cuda` | CUDA | `nvidia-smi` responds |
|
|
45
|
+
| `amd` | `main-vulkan` | Vulkan (RADV) | `/dev/kfd` exists |
|
|
46
|
+
| `intel` | `main-vulkan` | Vulkan (ANV) | Intel iGPU in `lspci` (Skylake+) |
|
|
47
|
+
| `cpu` | `main-vulkan` (no GPU device) | CPU fallback | everything else |
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Docker image compatibility
|
|
51
|
+
|
|
52
|
+
Not all Docker images work on all CPUs. `digue doctor` tests compatibility for images that are already present locally; it does not pull images. Pull an image with `docker pull IMAGE` first if you want it included in the test.
|
|
53
|
+
|
|
54
|
+
| CPU generation | `main` | `main-vulkan` | `main-cuda` |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| Kaby Lake (7th gen, 2016) | OK | SIGILL (exit 132) | n/a |
|
|
57
|
+
| Meteor Lake (Core Ultra, 2024) | AMX crash | OK | n/a |
|
|
58
|
+
| AMD Ryzen 7000+ | untested | OK | n/a |
|
|
59
|
+
| NVIDIA GPU (any CPU) | varies | varies | OK |
|
|
60
|
+
|
|
61
|
+
If the default image crashes on your CPU, override it in the config:
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[server]
|
|
65
|
+
backend = "cpu"
|
|
66
|
+
image = "ghcr.io/ggml-org/whisper.cpp:main"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
or for one run, `digue server start --image ghcr.io/ggml-org/whisper.cpp:main`. Either way, `digue server start` notices a container created from another image and recreates it (`docker start` alone would keep the old image); `digue dictate` only warns about the mismatch, since a multi-GB pull is not what a hotkey should trigger. Any image that provides `whisper-server` works, including a locally built one.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Performance
|
|
73
|
+
|
|
74
|
+
On a Ryzen 7 8745HS with Radeon 780M (`amd` backend, Vulkan/RADV), GPU transcription of the whisper.cpp JFK sample is 5-11x faster than CPU. `large-v3-turbo` and `medium` both take about 1 s on this GPU; `small` is quicker (~0.4 s) but the transcript has no punctuation or capitalization. The default (`large-v3-turbo` on `amd`) is the right choice for this machine: same cost as `medium`, larger model.
|
|
75
|
+
|
|
76
|
+
Times are wall-clock, three runs after a warm-up, `main-vulkan` image:
|
|
77
|
+
|
|
78
|
+
| Model | GPU (`amd`) | CPU | GPU speedup |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| `small` | 0.42 s | 2.22 s | 5.3x |
|
|
81
|
+
| `medium` | 1.01 s | 6.96 s | 6.9x |
|
|
82
|
+
| `large-v3-turbo` | 0.99 s | 11.02 s | 11.1x |
|
|
83
|
+
|
|
84
|
+
One machine, not a ranking of AMD iGPUs. Re-run with `digue benchmark --sample -b amd cpu -m small medium large-v3-turbo`.
|
|
85
|
+
|
|
86
|
+
### Quantized models
|
|
87
|
+
|
|
88
|
+
whisper.cpp runs integer-quantized models and `ggerganov/whisper.cpp` on Hugging Face (where `digue` downloads from) publishes them next to the f16 files. `-q8_0` is the first step worth trying: about 55% of the size, accuracy in practice unchanged on a short dictation and in one AMD iGPU run `large-v3-turbo-q8_0` was a little faster than f16 (same transcript). `-q5_0`/`-q5_1` go to about 35% and can drop proper names ("Gedit" -> "G-Edit"); they are also not always faster -- on CPU, `medium-q5_0` was slower than `medium-q8_0`. Measure with `digue benchmark` before changing a default.
|
|
89
|
+
|
|
90
|
+
`digue models` lists every name (and which files are already in `<data-dir>/models`). Any of them works wherever a model is named: `[models]`, `digue download <model>` and `digue benchmark -m`. The `.en` variants are English-only. Sizes (MiB): `large-v3-turbo` 1549, `-q8_0` 834, `-q5_0` 547; `medium` 1463, `-q8_0` 785, `-q5_0` 514; `small` 465, `-q8_0` 252, `-q5_1` 181; `large-v3` 2952, `-q5_0` 1031.
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[models]
|
|
94
|
+
amd = "large-v3-turbo-q8_0" # 834 MB instead of 1549 MB
|
|
95
|
+
|
|
96
|
+
[host.thinkpad.models]
|
|
97
|
+
cpu = "small-q8_0" # same family as the cpu default, half the file
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The model is a file in `<data-dir>/models` mounted into the container, but its name is fixed in the container's command line, so after changing `[models]` recreate the container: `digue server destroy && digue server start` (the missing file is downloaded first). To compare before committing to a change:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
digue benchmark --sample -b amd -m large-v3-turbo large-v3-turbo-q8_0 large-v3-turbo-q5_0 -r 5
|
|
104
|
+
digue benchmark --sample -b cpu -m small small-q8_0 medium-q8_0 medium-q5_0
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## System requirements
|
|
109
|
+
|
|
110
|
+
- GNU/Linux only. Tested on Debian trixie. Should work on Ubuntu 22.04+, Fedora 38+, Arch. Not compatible with macOS or Windows.
|
|
111
|
+
- Python 3.11+ (for `tomllib`). No pip packages needed at runtime.
|
|
112
|
+
- Docker is required for running the whisper-server container (`apt install docker.io && usermod -aG docker $USER`, then log out and back in)
|
|
113
|
+
- For NVIDIA GPUs, also install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html).
|
|
114
|
+
- For audio recording (dictation only), PipeWire is needed (`apt install pipewire`) or ALSA (`apt install alsa-utils`) as a fallback
|
|
115
|
+
- For desktop notifications (dictation only), `notify-send` is needed (`apt install libnotify-bin`)
|
|
116
|
+
- For clipboard and paste (dictation only), `xclip` and `xdotool` on X11 or `wl-clipboard` and `wtype` on Wayland (`apt install xclip xdotool` / `apt install wl-clipboard wtype`). See "Text output" for the `input-mode = "type"` alternative.
|
|
117
|
+
- `digue` auto-detects X11 or Wayland via `$DISPLAY` / `$WAYLAND_DISPLAY`. You can force it with `display-server` in the config.
|
|
118
|
+
- For GPU detection (optional): `apt install pciutils vulkan-tools mesa-vulkan-drivers`
|
|
119
|
+
- For audio formats the server cannot decode (optional): `apt install ffmpeg`. Natively supported: wav, flac, mp3, ogg/Vorbis, aiff (see "Audio formats").
|
|
120
|
+
|
|
121
|
+
## Installation
|
|
122
|
+
|
|
123
|
+
`digue` is on PyPI. The recommended way to install a command-line application is `pipx`, which puts it in an isolated virtual environment and exposes the `digue` command on your `PATH` without touching the Python managed by your distribution:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pipx install digue
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
On Debian/Ubuntu, `pipx` itself comes from the distribution (`sudo apt install pipx`). `pipx` places its launchers in `~/.local/bin`; if that directory is already on your `PATH` (it is on most desktop setups), nothing else is needed. If `digue` is not found after installing, either:
|
|
130
|
+
|
|
131
|
+
1. run `pipx ensurepath` once -- note that it edits your shell configuration files (`~/.bashrc`, `~/.zshrc`, ...) to add `~/.local/bin` to `PATH`; or
|
|
132
|
+
2. add `~/.local/bin` to `PATH` yourself, in whatever way your shell is configured.
|
|
133
|
+
|
|
134
|
+
Point your window manager keybinding at `~/.local/bin/digue` directly; no `bash -c` or activation script is needed. The launcher is not a standalone executable: it still uses the Python interpreter and environment managed by `pipx`.
|
|
135
|
+
|
|
136
|
+
Plain `pip install digue` also works inside a virtual environment you manage yourself. Avoid `sudo pip install` and `pip install --user` on modern Debian/Ubuntu: PEP 668 marks the distribution Python as externally managed, so pip refuses them and bypassing that protection (`--break-system-packages`) can break system tools.
|
|
137
|
+
|
|
138
|
+
### Without pipx or pip
|
|
139
|
+
|
|
140
|
+
`digue` has no runtime dependencies beyond Python 3.11+, so the package directory can simply be copied somewhere and run with `python3 -m digue`. Clone the repository into a temporary directory, copy the `digue/` package to `~/.local/opt/` and create a small launcher in `~/.local/bin/`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git clone --depth 1 https://github.com/turicas/digue.git /tmp/digue
|
|
144
|
+
mkdir -p ~/.local/opt ~/.local/bin
|
|
145
|
+
cp -r /tmp/digue/digue ~/.local/opt/digue
|
|
146
|
+
cat > ~/.local/bin/digue <<'EOF'
|
|
147
|
+
#!/bin/sh
|
|
148
|
+
PYTHONPATH="$HOME/.local/opt${PYTHONPATH:+:$PYTHONPATH}" exec python3 -m digue "$@"
|
|
149
|
+
EOF
|
|
150
|
+
chmod +x ~/.local/bin/digue
|
|
151
|
+
rm -rf /tmp/digue
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The launcher must use `python3 -m digue` (with the parent directory on `PYTHONPATH`): running `python3 ~/.local/opt/digue/cli.py` directly does not work, because the package uses absolute `digue.*` imports and Python puts the script's own directory, not its parent, on `sys.path`. To update, repeat the clone and the `cp` (remove the old `~/.local/opt/digue` first). The same `PATH` note as above applies to `~/.local/bin`.
|
|
155
|
+
|
|
156
|
+
To work on the code instead, clone it anywhere and run `python3 -m digue ...` from the checkout; `pipx install -e .` exposes that checkout as the `digue` command.
|
|
157
|
+
|
|
158
|
+
### First run
|
|
159
|
+
|
|
160
|
+
The Docker image (~1-3 GB) and model (~0.5-1.6 GB) are downloaded automatically on first use. This can take several minutes. The download progress is shown both in the terminal and as a desktop notification.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## Keybinding setup
|
|
164
|
+
|
|
165
|
+
### i3 / sway
|
|
166
|
+
|
|
167
|
+
Add to `~/.config/i3/config` or `~/.config/sway/config`:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
bindsym $mod+Shift+d exec --no-startup-id $HOME/.local/bin/digue dictate
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Note: `~` does not work in i3/sway config. Use `$HOME` or the full path.
|
|
174
|
+
|
|
175
|
+
### GNOME
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Create the shortcut
|
|
179
|
+
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings \
|
|
180
|
+
"['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/digue/']"
|
|
181
|
+
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/digue/ \
|
|
182
|
+
name 'Digue dictation'
|
|
183
|
+
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/digue/ \
|
|
184
|
+
command "$HOME/.local/bin/digue dictate"
|
|
185
|
+
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/digue/ \
|
|
186
|
+
binding '<Super><Shift>d'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or via Settings -> Keyboard -> Custom Shortcuts.
|
|
190
|
+
|
|
191
|
+
### KDE Plasma
|
|
192
|
+
|
|
193
|
+
Settings -> Shortcuts -> Custom Shortcuts -> Edit -> New -> Global Shortcut -> Command/URL:
|
|
194
|
+
|
|
195
|
+
- Trigger: `Super+Shift+D`
|
|
196
|
+
- Action: `/home/YOUR_USER/.local/bin/digue dictate`
|
|
197
|
+
|
|
198
|
+
### XFCE
|
|
199
|
+
|
|
200
|
+
Settings -> Keyboard -> Application Shortcuts -> Add:
|
|
201
|
+
|
|
202
|
+
- Command: `/home/YOUR_USER/.local/bin/digue dictate`
|
|
203
|
+
- Shortcut: `Super+Shift+D`
|
|
204
|
+
|
|
205
|
+
### Unity
|
|
206
|
+
|
|
207
|
+
System Settings -> Keyboard -> Shortcuts -> Custom Shortcuts -> `+`:
|
|
208
|
+
|
|
209
|
+
- Name: `Digue dictation`
|
|
210
|
+
- Command: `/home/YOUR_USER/.local/bin/digue dictate`
|
|
211
|
+
- Click "Disabled", press `Super+Shift+D`
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## CLI usage
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Global options
|
|
218
|
+
digue --version # print the digue version
|
|
219
|
+
digue -c ./config.toml server status # use a custom config file
|
|
220
|
+
|
|
221
|
+
# Dictation
|
|
222
|
+
digue dictate # toggle recording/transcription
|
|
223
|
+
digue dictate -p "Turicas, Pythonic" # override the shared transcription prompt
|
|
224
|
+
|
|
225
|
+
# Server management
|
|
226
|
+
digue detect # print detected backend
|
|
227
|
+
digue detect-language audio.mp3 # print detected language code
|
|
228
|
+
digue detect-language audio.mp3 --json # code, probability and all probabilities
|
|
229
|
+
digue detect-language audio.mp3 -v # show conversion progress on stderr
|
|
230
|
+
digue models # list available models (size, already downloaded)
|
|
231
|
+
digue download # download model for detected backend
|
|
232
|
+
digue download small # download a specific model
|
|
233
|
+
digue server start # start (or create) server container
|
|
234
|
+
digue server start --image ghcr.io/ggml-org/whisper.cpp:main # with another image (recreates the container)
|
|
235
|
+
digue server start -n digue-whisper.cpp # Docker container name (default: digue-whisper.cpp)
|
|
236
|
+
digue server stop # stop server container
|
|
237
|
+
digue server destroy # stop and remove container
|
|
238
|
+
digue server status # show server status
|
|
239
|
+
|
|
240
|
+
# Dictation storage
|
|
241
|
+
digue clean # list recordings/transcripts, ask, remove all
|
|
242
|
+
digue clean -f # remove without asking
|
|
243
|
+
digue clean -w recordings # remove only recordings (also: transcripts, both)
|
|
244
|
+
|
|
245
|
+
# File transcription
|
|
246
|
+
digue transcribe audio.mp3 # text to stdout (silent)
|
|
247
|
+
digue transcribe audio.mp3 -v # show progress messages
|
|
248
|
+
digue transcribe interview.mp4 -f vtt -o out.vtt # VTT from video
|
|
249
|
+
digue transcribe audio.mp3 -f srt -o out.srt # SRT to file
|
|
250
|
+
digue transcribe audio.mp3 -f timestamps -o out.txt # [00:00:12] text lines
|
|
251
|
+
digue transcribe audio.mp3 -l pt # force language
|
|
252
|
+
digue transcribe audio.mp3 -p "Turicas, Pythonic" # hint names/acronyms
|
|
253
|
+
|
|
254
|
+
# Format conversion (no server needed)
|
|
255
|
+
digue convert a.vtt b.txt # VTT -> timestamps (.txt is inferred as timestamps)
|
|
256
|
+
digue convert a.vtt b.txt -t text # VTT -> plain text
|
|
257
|
+
digue convert a.vtt # VTT -> plain text on stdout
|
|
258
|
+
digue convert -f vtt - b.txt # VTT on stdin -> timestamps in b.txt
|
|
259
|
+
digue convert -f vtt - # VTT on stdin -> plain text on stdout
|
|
260
|
+
digue convert -f vtt - -t timestamps - # VTT on stdin -> timestamps on stdout
|
|
261
|
+
digue convert a.srt b.vtt # SRT -> VTT
|
|
262
|
+
# Formats accepted by -f/--from-format and -t/--to-format: vtt, srt, timestamps, text
|
|
263
|
+
|
|
264
|
+
# Batch operations
|
|
265
|
+
digue batch-transcribe ./audios ./transcriptions # use shared output-format (text by default)
|
|
266
|
+
digue batch-transcribe ./audios ./transcriptions -f vtt # override with VTT (also: srt, text)
|
|
267
|
+
digue batch-simplify-vtt ./transcriptions ./simplified # all VTT -> text
|
|
268
|
+
|
|
269
|
+
# Diagnostics
|
|
270
|
+
digue config show # resolved config as TOML (default)
|
|
271
|
+
digue config show -f json # resolved config as JSON
|
|
272
|
+
digue config init # create the config file with commented defaults
|
|
273
|
+
digue config init -f # overwrite the config file
|
|
274
|
+
digue config init -o path # create the config file at a custom path
|
|
275
|
+
digue -c path config init # create the config at the global -c path
|
|
276
|
+
digue doctor # check dependencies, test locally present images
|
|
277
|
+
digue benchmark # quick: resolved backend + cpu, small + large-v3-turbo (records 10s from mic)
|
|
278
|
+
digue benchmark audio.wav # same, with an existing audio file
|
|
279
|
+
digue benchmark --sample # same, with the whisper.cpp JFK sample (downloaded once)
|
|
280
|
+
digue benchmark --sample -m all # every model on the default backends
|
|
281
|
+
digue benchmark -b amd cpu -m medium -r 5 --json # pick backends/models/runs; JSON results on stdout
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## Configuration
|
|
286
|
+
|
|
287
|
+
All settings have sensible defaults. The config file is optional; `digue config init` writes it with every setting documented (defaults commented out).
|
|
288
|
+
|
|
289
|
+
Unknown sections, keys, or `[models]` backends are rejected when the config is loaded -- including inside `[host.<hostname>]` tables and for every host, not just the current machine, since the file is versioned in dotfiles.
|
|
290
|
+
|
|
291
|
+
Create `~/.config/digue/config.toml` (or `$XDG_CONFIG_HOME/digue/config.toml`). Paths support `~` (expanded to home directory).
|
|
292
|
+
|
|
293
|
+
### Shared and per-host configuration
|
|
294
|
+
|
|
295
|
+
`[transcribe]` is the real shared configuration for `transcribe`, `batch-transcribe` and `dictate`: `language`, `prompt`, `output-format`, `max-line-length`, `max-lines` and `timeout` are inherited by all applicable commands. CLI options override those values. `[dictate]` contains only capture and delivery settings.
|
|
296
|
+
|
|
297
|
+
One config file can drive all your machines: version it in your dotfiles and add a `[host.<hostname>][section]` table per machine. Inside the host table, use the same section names as the top level (`server`, `transcribe`, `dictate`, `models`); keys there override the global sections when the hostname matches and global keys you did not override are still inherited. The hostname is read with `gethostname()` (an in-memory call, microseconds) - it does not delay the dictation hotkey. Run `digue config show` on each machine to confirm what was resolved. Hostnames containing dots must be quoted (`[host."minipc.local".server]`); unquoted, TOML parses each dot as a nested table and the file is rejected.
|
|
298
|
+
|
|
299
|
+
## Config command
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
digue config init # create ~/.config/digue/config.toml with commented defaults
|
|
303
|
+
digue config init -f # overwrite an existing config file
|
|
304
|
+
digue config init -o path # create the config file at a custom path
|
|
305
|
+
digue config show # resolved configuration (defaults + file + host overrides), as TOML
|
|
306
|
+
digue config show -f json # same, as JSON
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
## Audio formats
|
|
311
|
+
|
|
312
|
+
Verified against `whisper-server` (the `ghcr.io/ggml-org/whisper.cpp` images decode with miniaudio and are built without its own ffmpeg fallback): natively supported formats are **wav, flac, mp3, ogg/Vorbis and aiff**.
|
|
313
|
+
|
|
314
|
+
Formats the server rejects (HTTP 400) are converted to 16 kHz mono WAV with **ffmpeg**, entirely in memory (the converted audio is never written to disk). This covers, among others: **ogg/Opus** (WhatsApp voice notes), **m4a/AAC**, mp4, webm, mka, wma, opus. Conversion happens either upfront (extension known to be unsupported) or as a retry after an HTTP 400. ffmpeg is optional:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
sudo apt install ffmpeg # optional, only needed for formats the server cannot decode
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Without ffmpeg, unsupported formats produce a clear error instead of a raw HTTP 400.
|
|
321
|
+
|
|
322
|
+
## Recording
|
|
323
|
+
|
|
324
|
+
Dictation uses PipeWire's `pw-record` by default and falls back to ALSA's `arecord` when PipeWire is not available (`recorder = "auto"`). Set `device` to a capture source (`pw-record --target NAME`, `arecord -D NAME`); empty keeps the system default. `pw-record` has no `--list-targets` -- list sources with `pactl list sources short` or `wpctl status`; `arecord -l` lists ALSA cards (`digue doctor` prints both). Needed packages:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
sudo apt install pipewire # default recorder (pw-record)
|
|
328
|
+
sudo apt install alsa-utils # fallback recorder (arecord)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The first `digue dictate` invocation stays alive as the recording daemon. Pressing the keybinding again sends it a stop signal and returns immediately; the original process stops the recorder, transcribes, delivers the text and archives the take. When run in a terminal, Ctrl+c also stops and transcribes. There is no global recording state: each take is tracked by its own state file in `$XDG_RUNTIME_DIR` (`digue-take-<take_id>.json`), which follows the cycle `starting` (recording file reserved) -> `recording` (recorder pid and /proc starttime published) -> `recovering` (the daemon died and the next toggle claimed the take). The owner itself is tracked by the daemon file (`digue-daemon.pid`: `starting` -> `recording` -> `delivering`); while it says `delivering` the daemon ignores further Ctrl+c and SIGTERM and a new take may start. Recovery: a recorder still alive is stopped through its published identity and its audio delivered; a dead recorder's WAV goes straight to delivery, unless its transcript was already saved (the daemon died while archiving), in which case only the audio is archived and nothing is pasted again; an empty WAV is discarded with its state (a take without audio has nothing left to recover). The toggle that recovers a take delivers it and returns without starting a new recording (press again to record); while it is delivering, another press starts a new take normally. A state file that cannot be parsed is reported as unreadable and left untouched (the WAV is kept). A take whose delivery failed in a retryable way keeps its state and WAV and the next `digue dictate` retries it; only `kill -9` aborts a delivery and then the raw WAV and the take state stay in `$XDG_RUNTIME_DIR` for recovery. Because each take carries its own identity, a recovery never signals another take's recorder. A new take may start while an earlier one is still being delivered.
|
|
332
|
+
|
|
333
|
+
The recorder runs in its own process group, so it can survive a killed daemon. The daemon normally enforces `max-duration` (default 300s, set `0` for unlimited), stops the recorder and reports that the limit was reached. A detached watchdog is only a safety killer: if the daemon is killed abruptly, it stops the recorder a few seconds after the limit but does not notify or transcribe. The next `digue dictate` recovers and delivers an orphaned recording and returns without starting a new one.
|
|
334
|
+
|
|
335
|
+
The recording is saved as a backup next to the `.txt` transcript, compressed with `audio-format` (default `flac`: lossless, ~35% of the WAV size; `opus`: ~7%, lossy 24 kbit/s; `wav`: no compression). When `audio-format = "flac"` and `pw-record --list-containers` lists `flac`, the live take is already FLAC (no ffmpeg). `arecord` cannot write FLAC and a `pw-record` without libFLAC still records WAV then compresses with ffmpeg (host, then the local container); without ffmpeg digue keeps the WAV and warns. Set `save-audio = false` to keep only the transcript (a take that fails to transcribe or paste is still kept as WAV, since it was delivered nowhere). A saved `.flac` is decodable by whisper-server natively; a saved `.opus` goes through the ffmpeg fallback if you run `digue transcribe` on it.
|
|
336
|
+
|
|
337
|
+
If the recorder exits at start (unknown `--target`, missing PCM, missing binary), digue notifies and prints the recorder's own error so the device name can be fixed.
|
|
338
|
+
|
|
339
|
+
## Text output
|
|
340
|
+
|
|
341
|
+
The transcribed text is joined into a single line before being sent to the focused window. Line breaks in the server output are segment boundaries; with `token_timestamps=false` (sent by digue on every request) the server no longer wraps segments at 60 characters, which was splitting words in half (`trans` / `crevendo`).
|
|
342
|
+
|
|
343
|
+
How the text lands on screen is controlled by `input-mode` and, for `paste`, by `paste-key`:
|
|
344
|
+
|
|
345
|
+
- `input-mode = "paste"` copies the text to the clipboard and simulates `paste-key` in the focused window: instant and atomic. `input-mode = "type"` simulates keystrokes (`xdotool type` / `wtype -`): works anywhere text can be typed, but is slower (~12 ms/char) and may drop characters in slow apps.
|
|
346
|
+
- `paste-key = "ctrl+v"` (default) is what GUI apps expect; **terminals ignore it** (most treat Ctrl+V as "insert next key literally"), so a dictation into a terminal pastes nothing.
|
|
347
|
+
- `paste-key = "ctrl+shift+v"` is the terminal shortcut and browsers accept it (paste without formatting) -- but GTK/Qt apps such as gedit ignore it, LibreOffice opens Paste Special and VS Code toggles the Markdown preview.
|
|
348
|
+
- `paste-key = "shift+insert"` is the X11-wide paste: terminals (xterm, urxvt, alacritty, gnome-terminal, kitty), GTK, Qt, browsers, LibreOffice and VS Code all paste on it. digue also fills the PRIMARY selection for this key, since xterm/urxvt/alacritty paste PRIMARY rather than the clipboard on Shift+Insert. This is the choice when you dictate into both terminals and GUI apps.
|
|
349
|
+
|
|
350
|
+
Rule of thumb: `paste` with `shift+insert` if terminals are part of your day; `paste` with the default `ctrl+v` otherwise; `type` when an app accepts none of the paste keys.
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
sudo apt install xclip xdotool # X11
|
|
354
|
+
sudo apt install wl-clipboard wtype # Wayland
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Remote access via SSH tunnel
|
|
358
|
+
|
|
359
|
+
The server binds to `127.0.0.1` (see `bind-ip` in the config) and is not exposed to the network. To use a remote machine's server (e.g. offloading from a laptop to a desktop):
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
ssh -NfL 8178:127.0.0.1:8178 user@desktop
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Then set `backend = "remote"` in the client's config:
|
|
366
|
+
|
|
367
|
+
```toml
|
|
368
|
+
[server]
|
|
369
|
+
backend = "remote"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
With the tunnel active, `digue` works normally on the client. The `remote` backend also tells `digue` to never create, start, or stop a local container: `digue server start`, `server stop` and `server destroy` refuse to run, `digue server status` only checks the port and a failed transcription points you to the tunnel instead of suggesting `digue server start`. Without this setting, `digue` would try to spin up a local container if it could not reach the port.
|
|
373
|
+
|
|
374
|
+
To manage the container itself, run the commands (`digue download`, `server start`, `server destroy`) on the remote machine.
|
|
375
|
+
|
|
376
|
+
If the server is already reachable on your network (no tunnel needed), point `digue` straight at it:
|
|
377
|
+
|
|
378
|
+
```toml
|
|
379
|
+
[server]
|
|
380
|
+
backend = "remote"
|
|
381
|
+
remote-host = "desktop.lan" # or a LAN IP, e.g. 10.0.0.5
|
|
382
|
+
# port = 8178 # the port the remote server listens on
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
On the server machine, keep the container reachable from the network by setting `bind-ip` to a LAN IP (the default `127.0.0.1` only accepts local connections; the server has no authentication, so only do this on a network you trust). `digue config show` on the client tells you which host:port is being probed.
|
|
386
|
+
|
|
387
|
+
For a persistent tunnel, add to `~/.ssh/config`:
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
Host digue-remote
|
|
391
|
+
HostName <desktop-ip>
|
|
392
|
+
User <user>
|
|
393
|
+
LocalForward 8178 127.0.0.1:8178
|
|
394
|
+
ServerAliveInterval 30
|
|
395
|
+
ExitOnForwardFailure yes
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Troubleshooting
|
|
399
|
+
|
|
400
|
+
Run `digue doctor` to check dependencies and config and to test compatible Docker images that are already downloaded. Images reported as `SKIP not pulled` are not tested or downloaded.
|
|
401
|
+
|
|
402
|
+
Common issues:
|
|
403
|
+
|
|
404
|
+
- **Server crashes in a loop (exit 132)**: the Docker image uses CPU instructions your processor doesn't support (SIGILL). Override the image in your config. See "Docker image compatibility" above.
|
|
405
|
+
- **"AMX is not ready to be used!"**: the `main` image fails on Meteor Lake CPUs inside Docker. Use `main-vulkan` (the default for `cpu` backend) or the `intel` backend.
|
|
406
|
+
- **`xclip` times out**: `xclip` forks a background process that inherits pipes. This is handled internally. If it still fails, check that `$DISPLAY` is set (run from a graphical terminal, not SSH).
|
|
407
|
+
- **No desktop notifications**: install `libnotify-bin`. All messages also print to stderr.
|
|
408
|
+
- **Config syntax error**: `digue` fails with a `tomllib` parse error pointing at the line - fix `~/.config/digue/config.toml` (or run `digue config init -f` to start over).
|
|
409
|
+
- **"Recorder not found"**: install PipeWire (`apt install pipewire`, for `pw-record`) or ALSA (`apt install alsa-utils`, for `arecord`).
|
|
410
|
+
- **A dictation notification got stuck**: concurrent dictations use 32 notification slots, from ID 48271 through 48302, based on the daemon PID. Successful delivery replaces the progress popup with a 3s Pasted/Typed toast; errors replace it with a notification that expires in 5-10s. `kill -9` may leave one behind; the next toggle closes the slot of the daemon it recovers. If one still lingers, click it, or close all digue slots with:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
for id in $(seq 48271 48302); do
|
|
414
|
+
gdbus call --session \
|
|
415
|
+
--dest org.freedesktop.Notifications \
|
|
416
|
+
--object-path /org/freedesktop/Notifications \
|
|
417
|
+
--method org.freedesktop.Notifications.CloseNotification "$id" >/dev/null
|
|
418
|
+
done
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Python API
|
|
422
|
+
|
|
423
|
+
The CLI is the main interface; the same module works as a library (no daemon, no paste):
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
import digue
|
|
427
|
+
|
|
428
|
+
config = digue.load_config() # default path, or load_config("path/to/config.toml")
|
|
429
|
+
text = digue.transcribe_file("meeting.wav") # starts the server if needed
|
|
430
|
+
digue.record_to("take.flac", seconds=8, config=config)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`transcribe_file` uses `[transcribe]` (language, prompt, output-format). `record_to` uses `[dictate]` `recorder` / `device` and writes FLAC natively when the path ends in `.flac` and `pw-record` supports that container; otherwise (arecord, or a `pw-record` without the flac container) it records WAV under the same stem, says so on stderr and returns the `.wav` path -- always use the returned path. A recorder that exits at start raises `RuntimeError` with its stderr.
|
|
434
|
+
|
|
435
|
+
## Tests
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
pip install pytest pytest-cov ruff mypy
|
|
439
|
+
make test # or: pytest tests/ -v --cov=digue --cov-report=term-missing
|
|
440
|
+
make mypy # mypy --strict over digue/
|
|
441
|
+
make lint # ruff check --fix + format
|
|
442
|
+
make check # lint-check + mypy + test in one go
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Publishing to PyPI
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
make build-check # build sdist+wheel and validate with twine
|
|
449
|
+
make publish # upload (requires credentials)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Bump `__version__` in `digue/__init__.py` before building (the package version comes from it).
|
|
453
|
+
|
|
454
|
+
## Audio storage
|
|
455
|
+
|
|
456
|
+
Every dictation is saved as a `<YYYYMMDD-HHMMSS>-<take_id>.txt` transcript plus the recording (compressed per `audio-format`, default `flac`) unless `save-audio = false`, under `<data-dir>/audio/YYYY/MM/` (one folder per month). The take id (16 hex chars) keeps overlapping takes that end in the same second from overwriting each other's files; older files without the id (plain `<YYYYMMDD-HHMMSS>.<ext>`) are still recognized. The timestamp has no colons, so filenames are shell-friendly to complete. These are kept as backup and not cleaned up automatically; `digue clean` lists what there is and removes it after confirmation (`-f` skips the confirmation and empty month directories are removed too). Only files in the dictation layout are touched (`YYYY/MM/<timestamp>[-<take_id>].wav|flac|opus|txt`), so anything else living under `audio-dir` is left alone.
|
|
457
|
+
|
|
458
|
+
## License
|
|
459
|
+
|
|
460
|
+
[MIT](LICENSE)
|