python-voiceio 0.3.12__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/METADATA +61 -35
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/RECORD +30 -25
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/WHEEL +1 -1
- voiceio/__init__.py +1 -1
- voiceio/app.py +158 -12
- voiceio/audit.py +409 -0
- voiceio/autocorrect.py +453 -24
- voiceio/autocorrect_state.py +184 -0
- voiceio/cli.py +447 -57
- voiceio/config.py +47 -2
- voiceio/corrections.py +37 -3
- voiceio/feedback.py +8 -5
- voiceio/history.py +27 -3
- voiceio/llm_api.py +24 -6
- voiceio/postcorrect.py +200 -0
- voiceio/postprocess.py +11 -0
- voiceio/prompt.py +10 -2
- voiceio/recorder.py +27 -0
- voiceio/retention.py +85 -0
- voiceio/service.py +71 -0
- voiceio/snapshots.py +78 -0
- voiceio/streaming.py +15 -1
- voiceio/transcriber.py +43 -3
- voiceio/vad.py +22 -3
- voiceio/vocabulary.py +143 -16
- voiceio/wizard.py +38 -5
- voiceio/worker.py +23 -3
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/entry_points.txt +0 -0
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_voiceio-0.3.12.dist-info → python_voiceio-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Speak → text, locally, instantly.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Hugo0/voiceio
|
|
8
8
|
Project-URL: Repository, https://github.com/Hugo0/voiceio
|
|
9
9
|
Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
|
|
10
10
|
Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
|
|
11
|
-
Keywords: voice,speech-to-text,whisper,linux,
|
|
11
|
+
Keywords: voice,speech-to-text,whisper,linux,dictation,wayland,ibus
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: X11 Applications
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
-
Classifier: Operating System :: MacOS
|
|
18
16
|
Classifier: Programming Language :: Python :: 3
|
|
19
17
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
20
18
|
Requires-Python: >=3.11
|
|
@@ -61,10 +59,18 @@ Dynamic: license-file
|
|
|
61
59
|
[](LICENSE)
|
|
62
60
|
[](https://pepy.tech/projects/python-voiceio)
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
Voice dictation for Linux. Local, yours, and it learns how you speak.
|
|
65
63
|
|
|
66
64
|
https://github.com/user-attachments/assets/9cf5d1ac-b4bb-4cf8-b775-7a66dc16b376
|
|
67
65
|
|
|
66
|
+
Open-source, sovereign voice input/output. Your speech is transcribed on your own machine, everything it learns about you stays in local files you own, and it gets better the more you use it.
|
|
67
|
+
|
|
68
|
+
- **Local & sovereign** — speech is transcribed on-device with [faster-whisper](https://github.com/SYSTRAN/faster-whisper); audio never leaves the machine. Your history, retained audio, corrections, and vocabulary all live in plain local files (JSONL / TOML / txt) you can read, edit, and delete. Zero telemetry. The one honest nuance: two *optional, off-by-default* features send text (never audio) to a cloud LLM you configure yourself — final-transcript polish (`[postcorrect]`) and the weekly correction-mining review.
|
|
69
|
+
- **Improves with use — automatically** — every utterance teaches it your words and names. See [How it learns](#how-it-learns).
|
|
70
|
+
- **Linux-first & hackable** — Wayland/X11, GNOME/KDE/sway/i3, chosen automatically by chain-and-probe backends. It's plain Python you can read in an afternoon and [contribute to](CLAUDE.md).
|
|
71
|
+
|
|
72
|
+
> **Linux-first.** voiceio is developed and tested daily on Linux (GNOME/Wayland). Windows and macOS ship as **experimental, untested** targets — the code paths exist but are unmaintained and likely broken. See [Experimental platforms](#experimental-platforms).
|
|
73
|
+
|
|
68
74
|
## Quick start
|
|
69
75
|
|
|
70
76
|
```bash
|
|
@@ -102,28 +108,9 @@ voiceio setup
|
|
|
102
108
|
</details>
|
|
103
109
|
|
|
104
110
|
<details>
|
|
105
|
-
<summary><strong>Windows</strong></summary>
|
|
106
|
-
|
|
107
|
-
```powershell
|
|
108
|
-
# Option A: Install with pip (requires Python 3.11+)
|
|
109
|
-
pip install python-voiceio
|
|
110
|
-
voiceio setup
|
|
111
|
-
|
|
112
|
-
# Option B: Download the installer from GitHub Releases (no Python needed)
|
|
113
|
-
# https://github.com/Hugo0/voiceio/releases
|
|
114
|
-
# Also available as a portable .zip if you prefer no installation.
|
|
115
|
-
```
|
|
111
|
+
<summary><strong>Windows / macOS (experimental)</strong></summary>
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
</details>
|
|
119
|
-
|
|
120
|
-
<details>
|
|
121
|
-
<summary><strong>macOS</strong></summary>
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
pipx install python-voiceio
|
|
125
|
-
voiceio setup
|
|
126
|
-
```
|
|
113
|
+
See [Experimental platforms](#experimental-platforms) below — these builds are untested and unmaintained.
|
|
127
114
|
</details>
|
|
128
115
|
|
|
129
116
|
<details>
|
|
@@ -152,7 +139,35 @@ hotkey → mic capture → whisper (local) → text at cursor
|
|
|
152
139
|
pre-buffered streaming IBus / clipboard
|
|
153
140
|
```
|
|
154
141
|
|
|
155
|
-
Press your hotkey to start recording (1s pre-buffer catches the first syllable). Text streams into the focused app as an underlined preview. Press again to commit. Transcription runs locally via [faster-whisper](https://github.com/SYSTRAN/faster-whisper), text is injected through IBus (any GTK/Qt app) with clipboard fallback for terminals.
|
|
142
|
+
Press your hotkey to start recording (1s pre-buffer catches the first syllable). Text streams into the focused app as an underlined preview. Press again to commit. Transcription runs locally via [faster-whisper](https://github.com/SYSTRAN/faster-whisper), text is injected through IBus (any GTK/Qt app) with clipboard fallback for terminals. It runs in real time on a modern CPU and ships with model tiers from `tiny` to `large-v3`.
|
|
143
|
+
|
|
144
|
+
## How it learns
|
|
145
|
+
|
|
146
|
+
Most dictation tools transcribe the same way on day 100 as on day 1. voiceio adapts to *you* — your jargon, names, and accent — entirely from data that never leaves your machine.
|
|
147
|
+
|
|
148
|
+
```mermaid
|
|
149
|
+
flowchart LR
|
|
150
|
+
subgraph live ["every utterance"]
|
|
151
|
+
A["🎙️ dictate"] --> B["store audio + raw text\n+ confidence (local)"]
|
|
152
|
+
A --> C["hotwords + recent context\nbias the Whisper decoder"]
|
|
153
|
+
A --> D["optional LLM pass fixes\nmisheard proper nouns"]
|
|
154
|
+
end
|
|
155
|
+
subgraph weekly ["weekly, in the background"]
|
|
156
|
+
E["mine history for recurring errors\n→ learn corrections + vocabulary\n(safety-gated, multi-vote)"] --> F["teacher model replays your audio\n→ audits what was learned"]
|
|
157
|
+
F -->|"bad rule"| G["auto-retired"]
|
|
158
|
+
F -->|"quality regressed"| H["whole week rolled back"]
|
|
159
|
+
end
|
|
160
|
+
B --> E
|
|
161
|
+
F -->|"confirmed"| C
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
1. **Capture** — every utterance stores its audio, raw text, and confidence in local files.
|
|
165
|
+
2. **Bias** — your vocabulary and recent context steer the Whisper decoder (hotwords / prompt) on every recording.
|
|
166
|
+
3. **Contextual fix** — an optional LLM pass repairs misheard proper nouns using surrounding context.
|
|
167
|
+
4. **Mine** — a weekly background job scans your history for recurring errors and auto-learns corrections and vocabulary. Multi-vote adjudication and a protected-languages guard (for bilingual users) keep it safe; it never asks you to triage.
|
|
168
|
+
5. **Audit** — a teacher model (a larger Whisper) replays your retained audio to verify what was learned. Bad rules are retired automatically, and a system-level drift metric rolls back an entire week of learning if quality regressed.
|
|
169
|
+
|
|
170
|
+
Rules are always **probationary, never tenured** — anything that stops helping is dropped. Your only touchpoint is an occasional desktop notification telling you what was learned.
|
|
156
171
|
|
|
157
172
|
## Features
|
|
158
173
|
|
|
@@ -202,7 +217,7 @@ voiceio uninstall Remove all system integrations
|
|
|
202
217
|
`voiceio setup` handles everything interactively. To tweak later, edit the config file or override at runtime:
|
|
203
218
|
|
|
204
219
|
- Linux/macOS: `~/.config/voiceio/config.toml`
|
|
205
|
-
- Windows: `%LOCALAPPDATA%\voiceio\config\config.toml`
|
|
220
|
+
- Windows: `%LOCALAPPDATA%\voiceio\config\config.toml` (see [Experimental platforms](#experimental-platforms))
|
|
206
221
|
|
|
207
222
|
```bash
|
|
208
223
|
voiceio --model large-v3 --language auto -v
|
|
@@ -224,12 +239,12 @@ voiceio logs # check debug output
|
|
|
224
239
|
| Hotkey doesn't work on Wayland | `sudo usermod -aG input $USER` then log out and back in |
|
|
225
240
|
| Transcription too slow | Use a smaller model: `voiceio --model tiny` |
|
|
226
241
|
| Want to start fresh | `voiceio uninstall` then `voiceio setup` |
|
|
227
|
-
| Windows
|
|
228
|
-
| Windows: no sound feedback | Check `voiceio logs` for audio device info |
|
|
229
|
-
| macOS issues | Experimental — consider [aquavoice.com](https://aquavoice.com/) or contribute a PR |
|
|
242
|
+
| Windows / macOS issues | These platforms are experimental and untested — see [Experimental platforms](#experimental-platforms) |
|
|
230
243
|
|
|
231
244
|
## Platform support
|
|
232
245
|
|
|
246
|
+
voiceio targets **Linux**. That's what it's developed and tested against.
|
|
247
|
+
|
|
233
248
|
| Platform | Status | Text injection | Hotkeys | Streaming preview |
|
|
234
249
|
|----------|--------|---------------|---------|-------------------|
|
|
235
250
|
| Ubuntu / Debian (GNOME, Wayland) | **Tested daily** | IBus | evdev / GNOME shortcut | Yes |
|
|
@@ -237,11 +252,22 @@ voiceio logs # check debug output
|
|
|
237
252
|
| Fedora (GNOME) | Supported | IBus | evdev / GNOME shortcut | Yes |
|
|
238
253
|
| Arch Linux | Supported | IBus | evdev | Yes |
|
|
239
254
|
| KDE / Sway / Hyprland | Should work | IBus / ydotool / wtype | evdev | Yes |
|
|
240
|
-
| Windows 10/11 | Experimental | pynput / clipboard | pynput | Type-and-correct (no preedit) |
|
|
241
|
-
| macOS | Experimental | pynput / clipboard | pynput | Type-and-correct (no preedit) |
|
|
242
255
|
|
|
243
256
|
voiceio auto-detects your platform and picks the best available backends. Run `voiceio doctor` to see what's working on your system.
|
|
244
257
|
|
|
258
|
+
## Experimental platforms
|
|
259
|
+
|
|
260
|
+
Windows and macOS code paths exist, but they are **experimental, untested, and unmaintained** — the maintainer only develops on Linux, so they may be broken at any given time. No parity with Linux is promised. Contributions are welcome, but please don't file bugs expecting a fix.
|
|
261
|
+
|
|
262
|
+
| Platform | Status | Text injection | Hotkeys | Streaming preview |
|
|
263
|
+
|----------|--------|---------------|---------|-------------------|
|
|
264
|
+
| Windows 10/11 | Experimental / untested | pynput / clipboard | pynput | Type-and-correct (no preedit) |
|
|
265
|
+
| macOS | Experimental / untested | pynput / clipboard | pynput | Type-and-correct (no preedit) |
|
|
266
|
+
|
|
267
|
+
- **Windows:** `pip install python-voiceio` then `voiceio setup` (pynput handles hotkeys + text injection; no system deps). Prebuilt installers may appear on [GitHub Releases](https://github.com/Hugo0/voiceio/releases).
|
|
268
|
+
- **macOS:** `pipx install python-voiceio` then `voiceio setup`. If it doesn't work for you, consider [aquavoice.com](https://aquavoice.com/) or contribute a PR.
|
|
269
|
+
- Config lives at `%LOCALAPPDATA%\voiceio\config\config.toml` (Windows) or `~/.config/voiceio/config.toml` (macOS).
|
|
270
|
+
|
|
245
271
|
## Uninstall
|
|
246
272
|
|
|
247
273
|
```bash
|
|
@@ -275,7 +301,7 @@ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) and [open issues](
|
|
|
275
301
|
- [x] Voice commands — "new line", "new paragraph", "scratch that", punctuation by name
|
|
276
302
|
- [x] Custom vocabulary / personal dictionary (bias Whisper via `initial_prompt`)
|
|
277
303
|
- [x] Smart punctuation & capitalization post-processing
|
|
278
|
-
- [x] Windows support
|
|
304
|
+
- [x] Windows support (experimental, untested)
|
|
279
305
|
- [x] System tray icon with animated states
|
|
280
306
|
- [x] Auto-stop on silence
|
|
281
307
|
|
|
@@ -1,35 +1,40 @@
|
|
|
1
|
-
python_voiceio-0.
|
|
2
|
-
voiceio/__init__.py,sha256=
|
|
1
|
+
python_voiceio-0.4.0.dist-info/licenses/LICENSE,sha256=Gz61o8jFTAvZUZyB3nWDB3DQQVuipjfPkVu9W8hBHM0,1072
|
|
2
|
+
voiceio/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
|
|
3
3
|
voiceio/__main__.py,sha256=xT5QCGGreYMisHO7Lh_Y-xAQ2TOzG2D7npKknGNcWSY,53
|
|
4
|
-
voiceio/app.py,sha256=
|
|
5
|
-
voiceio/
|
|
4
|
+
voiceio/app.py,sha256=V6XGwWvvB6C6detltWW4OGMKWYUZPcAFcWUL4gXdqBY,53130
|
|
5
|
+
voiceio/audit.py,sha256=vX3x-YAhmt6EZYRZd0_6wvTquZu5sxGMvH45BhSBnCk,14915
|
|
6
|
+
voiceio/autocorrect.py,sha256=jGsglEYwAHWk2uWedeEZzyAMW2LWO_8nQGZgrWYy4Dc,26552
|
|
7
|
+
voiceio/autocorrect_state.py,sha256=BNzdHP5slcOV_-OU4MlaKE7xgVJzrPncsYFK30JWTs8,6858
|
|
6
8
|
voiceio/backends.py,sha256=-rs1YhPblub4NjFb6qCD7Ih9FqBRXtPuvjVv9bQoQ1w,368
|
|
7
|
-
voiceio/cli.py,sha256=
|
|
9
|
+
voiceio/cli.py,sha256=a27O_1IIn6jaV3Q3f5w9BXtsg3zAAdNi0nDlGx2drU4,65822
|
|
8
10
|
voiceio/clipboard_read.py,sha256=266u_QoJb_n3iLuy5fbccPZJfpYpPkjsNtZxjDNmqAE,2062
|
|
9
11
|
voiceio/commands.py,sha256=Vhtn8s5G5OcCbRN44YZ-_14Fa85ISXl_3Ix-KqSHs2A,4694
|
|
10
|
-
voiceio/config.py,sha256=
|
|
11
|
-
voiceio/corrections.py,sha256=
|
|
12
|
+
voiceio/config.py,sha256=TORjFrY6VDb36CbX9W-U4uDwRaEdcUQY369-uV_EF8U,8137
|
|
13
|
+
voiceio/corrections.py,sha256=7moKhkVvo5CZU1mZobV2-MajkcHiwliKBY1dSBFh0w0,7445
|
|
12
14
|
voiceio/demo.py,sha256=lhVMdBC-9uh25-7AXl30CVrmpL53KeGrZhY4HAtuxDI,8626
|
|
13
|
-
voiceio/feedback.py,sha256=
|
|
15
|
+
voiceio/feedback.py,sha256=TyUy_wy5HNTO0u3opx58aaPXz7mAqeN98UbNdLfnoiw,5136
|
|
14
16
|
voiceio/health.py,sha256=aJcULea1F06iUjjmREGcD-uO_JF_7zar3b5CFIR_uUM,15204
|
|
15
17
|
voiceio/hints.py,sha256=dmoiMAJ0YGTWwFc9kIPvEs_BQJN7UEUiwYuLUD_GoQQ,1415
|
|
16
|
-
voiceio/history.py,sha256=
|
|
18
|
+
voiceio/history.py,sha256=bku01AmAkbpl376ud9Ldjp-F-gYKyH1RXoHV5_cxK5s,2699
|
|
17
19
|
voiceio/llm.py,sha256=_m13jodMVB1iEtze3VGkBCafkg85OssTPBEVyaRabgA,9250
|
|
18
|
-
voiceio/llm_api.py,sha256=
|
|
20
|
+
voiceio/llm_api.py,sha256=Vjs7x_qphd95TmdPELbMlUdenKdtblusZVFm2G8FQJE,6159
|
|
19
21
|
voiceio/numbers.py,sha256=MP8jag4_F2OqUU5H14KzOmR9L8gWg48yQhLoUbGg_YI,8376
|
|
20
22
|
voiceio/pidlock.py,sha256=RInlF_8H7wR0nqkdlhUfxYs7OGzHIvb5p-XDjjQSTNg,619
|
|
21
23
|
voiceio/platform.py,sha256=5Z_bUVDrU3-tuGQW6zaW_Q5EAER4_y07-wDS3d132fU,7870
|
|
22
|
-
voiceio/
|
|
23
|
-
voiceio/
|
|
24
|
-
voiceio/
|
|
25
|
-
voiceio/
|
|
26
|
-
voiceio/
|
|
27
|
-
voiceio/
|
|
28
|
-
voiceio/
|
|
29
|
-
voiceio/
|
|
30
|
-
voiceio/
|
|
24
|
+
voiceio/postcorrect.py,sha256=S4aIbMjeYny8-oo7kUQM-6YZB8IDAy7vi80-F1qjwRc,7860
|
|
25
|
+
voiceio/postprocess.py,sha256=9grkq3I6XgiQV6ZQbIw2_fOUe57tzC7GBPvLXqhhdTs,2979
|
|
26
|
+
voiceio/prompt.py,sha256=9iza8KoQxvO_U9NPMJ8Ep7j-zkIs9nW8N5sacCRpsEk,2904
|
|
27
|
+
voiceio/recorder.py,sha256=sTxkKpaGoixmnWnNmygb7dwifu5aZZAXGqRgH1pketE,12026
|
|
28
|
+
voiceio/retention.py,sha256=MhXW3BOw8m50-q6ty6zjXic95VfLjmr6nUQytRuVPK0,2838
|
|
29
|
+
voiceio/service.py,sha256=jqX1opZyQh_dOJMK4bVf8Aoei0zOuuk1Ogta9BXTWGY,12106
|
|
30
|
+
voiceio/snapshots.py,sha256=bm-lzIeYucDUDlPBZiFiWd-jyB2I6Ft1jRANZCO6go4,2351
|
|
31
|
+
voiceio/streaming.py,sha256=epPaQbJLDP8U18RR7LJaQ_KuYmlx2E1gv2lJsJNkloo,11702
|
|
32
|
+
voiceio/transcriber.py,sha256=dslLArdozpqYe2ddTh7KZCXT35TnDpv1dCnRsVZkDjQ,8109
|
|
33
|
+
voiceio/vad.py,sha256=72_ICk4jSuSPYvTHdHYtHzY8W5uaGEOz5FTdqkolJi8,5327
|
|
34
|
+
voiceio/vocabulary.py,sha256=eGV7QZs3tN6Cw5YpyoZsVEk2M5fJvie1n2XFi9CD4hw,6321
|
|
35
|
+
voiceio/wizard.py,sha256=GEG0ykw52kF81CIA9JlatA0rSTxIxapmCMbTuOzA4EM,63350
|
|
31
36
|
voiceio/wordfreq.py,sha256=2UMjW1xIFqo5EchXzsXPizUhRb9N18CVgjnXso1wsgw,2382
|
|
32
|
-
voiceio/worker.py,sha256=
|
|
37
|
+
voiceio/worker.py,sha256=M6e4s6P0-4UGQzw0j8Mq68nypvwZleOSwuRhjuOqlVE,2563
|
|
33
38
|
voiceio/hotkeys/__init__.py,sha256=rGXSGZLD2mS_Ep2HfVdRdkUKNr4xagySlL1FM00cYgk,735
|
|
34
39
|
voiceio/hotkeys/base.py,sha256=L3iBh368sjWpt64JZpocH9DibYEZ0V16-rYflLYbpZY,699
|
|
35
40
|
voiceio/hotkeys/chain.py,sha256=8pvPhipmxWwmGG9DRXy8dlEKTPFILxNzYO-adcvw5Ps,2800
|
|
@@ -64,8 +69,8 @@ voiceio/typers/pynput_type.py,sha256=DTtkT59M-EKOAlTNkvcZrRvf_Fq52OpI8S5t_8S-QAY
|
|
|
64
69
|
voiceio/typers/wtype.py,sha256=d1wG-HZdYDQxXIysV_Xvk9HixDpaJgl3VEoKbP5vhIs,1786
|
|
65
70
|
voiceio/typers/xdotool.py,sha256=dh1zhPqUT8ihJahJ7ZKm6PtfYY087UAzADx664DvQOM,1356
|
|
66
71
|
voiceio/typers/ydotool.py,sha256=OQPNmaA2DxTAaJKBYpWzh_R6JnPSfum9OpmF7KvKYD4,3728
|
|
67
|
-
python_voiceio-0.
|
|
68
|
-
python_voiceio-0.
|
|
69
|
-
python_voiceio-0.
|
|
70
|
-
python_voiceio-0.
|
|
71
|
-
python_voiceio-0.
|
|
72
|
+
python_voiceio-0.4.0.dist-info/METADATA,sha256=u1G5NlIoTB4K8BDw_phICZPzzQR9ov4WaSadS3kbMn8,15381
|
|
73
|
+
python_voiceio-0.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
74
|
+
python_voiceio-0.4.0.dist-info/entry_points.txt,sha256=U64fA65zxzyLoC8bgbn2ztQVWHLsc0o0H0qCk1J9DMc,218
|
|
75
|
+
python_voiceio-0.4.0.dist-info/top_level.txt,sha256=piwtn309lD6uexQyXdZ-efAVBJF9y6Wfr48Z-8zkNhg,8
|
|
76
|
+
python_voiceio-0.4.0.dist-info/RECORD,,
|
voiceio/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4.0"
|
voiceio/app.py
CHANGED
|
@@ -22,7 +22,7 @@ from voiceio.streaming import StreamingSession
|
|
|
22
22
|
from voiceio.transcriber import Transcriber
|
|
23
23
|
from voiceio.typers import chain as typer_chain
|
|
24
24
|
from voiceio.vad import load_vad
|
|
25
|
-
from voiceio.vocabulary import
|
|
25
|
+
from voiceio.vocabulary import VocabularyLoader
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from voiceio.typers.base import TyperBackend
|
|
@@ -92,6 +92,12 @@ def _import_graphical_env() -> None:
|
|
|
92
92
|
|
|
93
93
|
_HEALTH_CHECK_INTERVAL = 10 # seconds between health checks
|
|
94
94
|
|
|
95
|
+
# Whisper's decoder has a hard 448-token sequence budget shared by hotwords,
|
|
96
|
+
# initial_prompt AND the transcription output. Hotwords + prompt must stay
|
|
97
|
+
# well under half of it or output gets truncated mid-utterance.
|
|
98
|
+
# ~600 chars ≈ 150 tokens.
|
|
99
|
+
_HOTWORDS_MAX_CHARS = 600
|
|
100
|
+
|
|
95
101
|
|
|
96
102
|
class _State(enum.Enum):
|
|
97
103
|
IDLE = "idle"
|
|
@@ -154,19 +160,21 @@ class VoiceIO:
|
|
|
154
160
|
vad = load_vad(cfg.audio)
|
|
155
161
|
self.recorder = AudioRecorder(cfg.audio, vad=vad)
|
|
156
162
|
|
|
157
|
-
vocab = load_vocabulary(cfg.model)
|
|
158
163
|
self._corrections = CorrectionDict()
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
# mtime-cached loader so `voiceio correct` vocabulary edits are picked
|
|
165
|
+
# up per-recording without a daemon restart.
|
|
166
|
+
self._vocab_loader = VocabularyLoader(cfg.model)
|
|
167
|
+
# Vocabulary biases the decoder via hotwords; initial_prompt carries
|
|
168
|
+
# recent-transcript context (rebuilt per recording via PromptBuilder).
|
|
169
|
+
self._hotwords = ""
|
|
170
|
+
self._refresh_hotwords()
|
|
171
|
+
from voiceio.prompt import PromptBuilder
|
|
172
|
+
self._prompt_builder = PromptBuilder()
|
|
166
173
|
|
|
167
174
|
self._command_processor = CommandProcessor(enabled=cfg.commands.enabled, editing=cfg.commands.editing)
|
|
168
175
|
self._cleanup = cfg.output.punctuation_cleanup
|
|
169
176
|
self._number_conversion = cfg.output.number_conversion
|
|
177
|
+
self._voice_input_prefix = cfg.output.voice_input_prefix
|
|
170
178
|
self._streaming = cfg.output.streaming
|
|
171
179
|
|
|
172
180
|
# LLM post-processing (optional, requires Ollama)
|
|
@@ -180,11 +188,28 @@ class VoiceIO:
|
|
|
180
188
|
else:
|
|
181
189
|
log.warning("LLM enabled but Ollama not available (will retry)")
|
|
182
190
|
|
|
191
|
+
# Constrained LLM post-correction (optional, cloud API, final pass only)
|
|
192
|
+
self._postcorrect = None
|
|
193
|
+
if cfg.postcorrect.enabled:
|
|
194
|
+
from voiceio.postcorrect import PostCorrector
|
|
195
|
+
self._postcorrect = PostCorrector(cfg)
|
|
196
|
+
if self._postcorrect.is_available():
|
|
197
|
+
log.info("PostCorrect: %s", cfg.postcorrect.model or cfg.autocorrect.model)
|
|
198
|
+
else:
|
|
199
|
+
log.warning("PostCorrect enabled but no API key resolved (disabled)")
|
|
200
|
+
|
|
183
201
|
# Explicit state machine
|
|
184
202
|
self._state = _State.IDLE
|
|
185
203
|
self._generation = 0 # incremented on every stop; leaked threads check this
|
|
186
204
|
self._session: StreamingSession | None = None
|
|
187
205
|
self._record_start: float = 0
|
|
206
|
+
self._last_clip_warn: float = 0
|
|
207
|
+
self._context_title: str | None = None
|
|
208
|
+
# Prune retained recordings over the size cap (startup housekeeping)
|
|
209
|
+
from voiceio import retention
|
|
210
|
+
threading.Thread(
|
|
211
|
+
target=retention.prune, args=(cfg.data,), daemon=True,
|
|
212
|
+
).start()
|
|
188
213
|
|
|
189
214
|
# Hotkey deduplication
|
|
190
215
|
self._hotkey_lock = threading.Lock()
|
|
@@ -265,6 +290,32 @@ class VoiceIO:
|
|
|
265
290
|
# The generation counter ensures the old finalizer exits cleanly.
|
|
266
291
|
self._do_start()
|
|
267
292
|
|
|
293
|
+
def _refresh_hotwords(self) -> None:
|
|
294
|
+
"""Rebuild Whisper hotwords from vocabulary + correction targets.
|
|
295
|
+
|
|
296
|
+
The vocabulary read is mtime-cached (only a `stat` when unchanged), and
|
|
297
|
+
`set_hotwords` is called only when the merged string actually changed,
|
|
298
|
+
so this stays cheap enough to run on every recording start.
|
|
299
|
+
"""
|
|
300
|
+
vocab = self._vocab_loader.get()
|
|
301
|
+
# Only merge RARE correction targets (proper nouns, technical terms):
|
|
302
|
+
# common-word targets ("review", "company") add nothing to decoder bias
|
|
303
|
+
# and hundreds of them blow Whisper's 448-token prompt+output budget,
|
|
304
|
+
# which truncates transcriptions mid-utterance.
|
|
305
|
+
from voiceio.wordfreq import is_common
|
|
306
|
+
vocab_terms = [
|
|
307
|
+
t for t in self._corrections.vocabulary_terms()
|
|
308
|
+
if not is_common(t, self.cfg.model.language)
|
|
309
|
+
]
|
|
310
|
+
if vocab_terms:
|
|
311
|
+
extra = ", ".join(vocab_terms)
|
|
312
|
+
vocab = f"{vocab}, {extra}" if vocab else extra
|
|
313
|
+
vocab = vocab[:_HOTWORDS_MAX_CHARS]
|
|
314
|
+
if vocab != self._hotwords:
|
|
315
|
+
self._hotwords = vocab
|
|
316
|
+
if vocab:
|
|
317
|
+
self.transcriber.set_hotwords(vocab)
|
|
318
|
+
|
|
268
319
|
def _do_start(self) -> None:
|
|
269
320
|
"""Transition to RECORDING."""
|
|
270
321
|
# Pre-flight: ensure audio stream is healthy before recording
|
|
@@ -283,6 +334,12 @@ class VoiceIO:
|
|
|
283
334
|
self._state = _State.RECORDING
|
|
284
335
|
self._activate_ibus()
|
|
285
336
|
self._corrections.load() # hot-reload corrections on each recording
|
|
337
|
+
self._refresh_hotwords() # hot-reload vocabulary (mtime-cached)
|
|
338
|
+
self.transcriber.set_initial_prompt(self._prompt_builder.build())
|
|
339
|
+
if self.cfg.data.capture_context:
|
|
340
|
+
# Snapshot the dictation target now — focus may change by finalize
|
|
341
|
+
self._context_title = None
|
|
342
|
+
threading.Thread(target=self._capture_context, daemon=True).start()
|
|
286
343
|
self._record_start = time.monotonic()
|
|
287
344
|
self.recorder.start()
|
|
288
345
|
self.recorder.set_on_auto_stop(self._on_auto_stop)
|
|
@@ -297,7 +354,9 @@ class VoiceIO:
|
|
|
297
354
|
language=self.cfg.model.language,
|
|
298
355
|
commands=self._command_processor,
|
|
299
356
|
corrections=self._corrections,
|
|
357
|
+
postcorrect=self._postcorrect,
|
|
300
358
|
llm=self._llm,
|
|
359
|
+
voice_input_prefix=self._voice_input_prefix,
|
|
301
360
|
on_typer_broken=self._on_typer_broken,
|
|
302
361
|
)
|
|
303
362
|
self._session.start()
|
|
@@ -314,6 +373,16 @@ class VoiceIO:
|
|
|
314
373
|
self.recorder.set_on_auto_stop(None)
|
|
315
374
|
audio = self.recorder.stop()
|
|
316
375
|
tray.set_recording(False)
|
|
376
|
+
self._warn_if_clipping()
|
|
377
|
+
|
|
378
|
+
if self._postcorrect is not None:
|
|
379
|
+
# Freshest context for the finalize pass: this recording's window
|
|
380
|
+
# title (captured at start) and the latest transcripts/vocabulary.
|
|
381
|
+
self._postcorrect.set_context(
|
|
382
|
+
vocabulary=self._hotwords,
|
|
383
|
+
recent=self._prompt_builder.recent(3),
|
|
384
|
+
title=self._context_title,
|
|
385
|
+
)
|
|
317
386
|
|
|
318
387
|
if self._streaming and self._session is not None:
|
|
319
388
|
self._state = _State.FINALIZING
|
|
@@ -343,6 +412,61 @@ class VoiceIO:
|
|
|
343
412
|
|
|
344
413
|
self._deactivate_ibus()
|
|
345
414
|
|
|
415
|
+
def _capture_context(self) -> None:
|
|
416
|
+
from voiceio import retention
|
|
417
|
+
self._context_title = retention.active_window_title()
|
|
418
|
+
|
|
419
|
+
def _retention_extra(self, audio: np.ndarray | None) -> dict:
|
|
420
|
+
"""Save the utterance audio (before transcription, so it survives
|
|
421
|
+
worker timeouts) and collect context for the history entry."""
|
|
422
|
+
from voiceio import retention
|
|
423
|
+
audio_name = None
|
|
424
|
+
if audio is not None:
|
|
425
|
+
audio_name = retention.save_audio(audio, time.time(), self.cfg.data)
|
|
426
|
+
return {
|
|
427
|
+
"audio": audio_name,
|
|
428
|
+
"context": self._context_title if self.cfg.data.capture_context else None,
|
|
429
|
+
"model": self.cfg.model.name,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
def _strip_voice_prefix(self, text: str) -> str:
|
|
433
|
+
"""Remove the configured voice-input prefix before persisting.
|
|
434
|
+
|
|
435
|
+
The prefix is presentation for the target app, not dictated content;
|
|
436
|
+
storing it would pollute vocabulary mining and correction analysis.
|
|
437
|
+
"""
|
|
438
|
+
if self._voice_input_prefix and text.startswith(self._voice_input_prefix):
|
|
439
|
+
return text[len(self._voice_input_prefix):].lstrip()
|
|
440
|
+
return text
|
|
441
|
+
|
|
442
|
+
_CLIP_RATIO_WARN = 0.005 # >0.5% of samples in flat-top runs
|
|
443
|
+
_CLIP_WARN_INTERVAL = 300 # seconds between warnings
|
|
444
|
+
|
|
445
|
+
def _warn_if_clipping(self) -> None:
|
|
446
|
+
"""Warn (rate-limited) when the mic input is saturating the ADC.
|
|
447
|
+
|
|
448
|
+
Clipping cannot be repaired in software — the gain must come down
|
|
449
|
+
before the ADC — so tell the user how to lower it.
|
|
450
|
+
"""
|
|
451
|
+
meter = self.recorder.get_meter()
|
|
452
|
+
if meter["clip_ratio"] < self._CLIP_RATIO_WARN:
|
|
453
|
+
return
|
|
454
|
+
now = time.monotonic()
|
|
455
|
+
if now - self._last_clip_warn < self._CLIP_WARN_INTERVAL:
|
|
456
|
+
return
|
|
457
|
+
self._last_clip_warn = now
|
|
458
|
+
log.warning(
|
|
459
|
+
"Microphone is clipping (%.1f%% of samples saturated, peak %.2f) — "
|
|
460
|
+
"transcription quality suffers. Lower the input gain, e.g.: "
|
|
461
|
+
"wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 10%%-",
|
|
462
|
+
meter["clip_ratio"] * 100, meter["peak"],
|
|
463
|
+
)
|
|
464
|
+
from voiceio import feedback
|
|
465
|
+
feedback.notify(
|
|
466
|
+
"VoiceIO: microphone too loud",
|
|
467
|
+
"Input is clipping — lower your mic gain in system sound settings.",
|
|
468
|
+
)
|
|
469
|
+
|
|
346
470
|
# ── Background work ─────────────────────────────────────────────────
|
|
347
471
|
|
|
348
472
|
def _finalize_streaming(
|
|
@@ -350,14 +474,23 @@ class VoiceIO:
|
|
|
350
474
|
elapsed: float, gen: int,
|
|
351
475
|
) -> None:
|
|
352
476
|
"""Run final transcription and commit in background thread."""
|
|
477
|
+
extra = self._retention_extra(audio)
|
|
353
478
|
final_text = session.stop(audio)
|
|
354
479
|
if self._generation != gen:
|
|
355
480
|
log.debug("Finalize cancelled (gen %d, current %d)", gen, self._generation)
|
|
356
481
|
return
|
|
357
482
|
if final_text:
|
|
358
483
|
self._play_feedback(final_text)
|
|
484
|
+
stored = self._strip_voice_prefix(final_text)
|
|
485
|
+
self._prompt_builder.add_transcript(stored)
|
|
359
486
|
from voiceio import history
|
|
360
|
-
history.append(
|
|
487
|
+
history.append(
|
|
488
|
+
stored,
|
|
489
|
+
raw=session.raw_final_text,
|
|
490
|
+
segments=session.final_segments,
|
|
491
|
+
duration=elapsed,
|
|
492
|
+
extra=extra,
|
|
493
|
+
)
|
|
361
494
|
log.info("Streaming done (%.1fs): '%s'", elapsed, final_text)
|
|
362
495
|
# Transition to IDLE under lock to avoid racing with _toggle
|
|
363
496
|
with self._hotkey_lock:
|
|
@@ -370,7 +503,10 @@ class VoiceIO:
|
|
|
370
503
|
try:
|
|
371
504
|
if self._generation != gen:
|
|
372
505
|
return
|
|
373
|
-
|
|
506
|
+
extra = self._retention_extra(audio)
|
|
507
|
+
raw = self.transcriber.transcribe(audio, final=True)
|
|
508
|
+
segments = self.transcriber.last_segments
|
|
509
|
+
text = raw
|
|
374
510
|
if self._generation != gen:
|
|
375
511
|
return
|
|
376
512
|
if text:
|
|
@@ -382,7 +518,9 @@ class VoiceIO:
|
|
|
382
518
|
language=self.cfg.model.language,
|
|
383
519
|
commands=self._command_processor,
|
|
384
520
|
corrections=self._corrections,
|
|
521
|
+
postcorrect=self._postcorrect,
|
|
385
522
|
llm=self._llm,
|
|
523
|
+
voice_input_prefix=self._voice_input_prefix,
|
|
386
524
|
final=True,
|
|
387
525
|
)
|
|
388
526
|
if abort:
|
|
@@ -390,8 +528,16 @@ class VoiceIO:
|
|
|
390
528
|
if text:
|
|
391
529
|
self._type_with_fallback(text)
|
|
392
530
|
self._play_feedback(text)
|
|
531
|
+
stored = self._strip_voice_prefix(text)
|
|
532
|
+
self._prompt_builder.add_transcript(stored)
|
|
393
533
|
from voiceio import history
|
|
394
|
-
history.append(
|
|
534
|
+
history.append(
|
|
535
|
+
stored,
|
|
536
|
+
raw=raw,
|
|
537
|
+
segments=segments,
|
|
538
|
+
duration=len(audio) / self.recorder.sample_rate,
|
|
539
|
+
extra=extra,
|
|
540
|
+
)
|
|
395
541
|
log.info("Typed: '%s'", text)
|
|
396
542
|
except Exception:
|
|
397
543
|
log.exception("Processing failed")
|