python-voiceio 0.3.13__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.13.dist-info → python_voiceio-0.4.0.dist-info}/METADATA +61 -35
- {python_voiceio-0.3.13.dist-info → python_voiceio-0.4.0.dist-info}/RECORD +29 -24
- {python_voiceio-0.3.13.dist-info → python_voiceio-0.4.0.dist-info}/WHEEL +1 -1
- voiceio/__init__.py +1 -1
- voiceio/app.py +155 -12
- voiceio/audit.py +409 -0
- voiceio/autocorrect.py +257 -4
- voiceio/autocorrect_state.py +184 -0
- voiceio/cli.py +319 -62
- voiceio/config.py +45 -1
- voiceio/corrections.py +37 -3
- voiceio/feedback.py +8 -5
- voiceio/history.py +27 -3
- voiceio/postcorrect.py +200 -0
- voiceio/postprocess.py +6 -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 +12 -1
- voiceio/transcriber.py +43 -3
- voiceio/vad.py +22 -3
- voiceio/vocabulary.py +143 -16
- voiceio/wizard.py +11 -5
- voiceio/worker.py +23 -3
- {python_voiceio-0.3.13.dist-info → python_voiceio-0.4.0.dist-info}/entry_points.txt +0 -0
- {python_voiceio-0.3.13.dist-info → python_voiceio-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_voiceio-0.3.13.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
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,15 +160,16 @@ 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
|
|
@@ -181,11 +188,28 @@ class VoiceIO:
|
|
|
181
188
|
else:
|
|
182
189
|
log.warning("LLM enabled but Ollama not available (will retry)")
|
|
183
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
|
+
|
|
184
201
|
# Explicit state machine
|
|
185
202
|
self._state = _State.IDLE
|
|
186
203
|
self._generation = 0 # incremented on every stop; leaked threads check this
|
|
187
204
|
self._session: StreamingSession | None = None
|
|
188
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()
|
|
189
213
|
|
|
190
214
|
# Hotkey deduplication
|
|
191
215
|
self._hotkey_lock = threading.Lock()
|
|
@@ -266,6 +290,32 @@ class VoiceIO:
|
|
|
266
290
|
# The generation counter ensures the old finalizer exits cleanly.
|
|
267
291
|
self._do_start()
|
|
268
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
|
+
|
|
269
319
|
def _do_start(self) -> None:
|
|
270
320
|
"""Transition to RECORDING."""
|
|
271
321
|
# Pre-flight: ensure audio stream is healthy before recording
|
|
@@ -284,6 +334,12 @@ class VoiceIO:
|
|
|
284
334
|
self._state = _State.RECORDING
|
|
285
335
|
self._activate_ibus()
|
|
286
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()
|
|
287
343
|
self._record_start = time.monotonic()
|
|
288
344
|
self.recorder.start()
|
|
289
345
|
self.recorder.set_on_auto_stop(self._on_auto_stop)
|
|
@@ -298,6 +354,7 @@ class VoiceIO:
|
|
|
298
354
|
language=self.cfg.model.language,
|
|
299
355
|
commands=self._command_processor,
|
|
300
356
|
corrections=self._corrections,
|
|
357
|
+
postcorrect=self._postcorrect,
|
|
301
358
|
llm=self._llm,
|
|
302
359
|
voice_input_prefix=self._voice_input_prefix,
|
|
303
360
|
on_typer_broken=self._on_typer_broken,
|
|
@@ -316,6 +373,16 @@ class VoiceIO:
|
|
|
316
373
|
self.recorder.set_on_auto_stop(None)
|
|
317
374
|
audio = self.recorder.stop()
|
|
318
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
|
+
)
|
|
319
386
|
|
|
320
387
|
if self._streaming and self._session is not None:
|
|
321
388
|
self._state = _State.FINALIZING
|
|
@@ -345,6 +412,61 @@ class VoiceIO:
|
|
|
345
412
|
|
|
346
413
|
self._deactivate_ibus()
|
|
347
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
|
+
|
|
348
470
|
# ── Background work ─────────────────────────────────────────────────
|
|
349
471
|
|
|
350
472
|
def _finalize_streaming(
|
|
@@ -352,14 +474,23 @@ class VoiceIO:
|
|
|
352
474
|
elapsed: float, gen: int,
|
|
353
475
|
) -> None:
|
|
354
476
|
"""Run final transcription and commit in background thread."""
|
|
477
|
+
extra = self._retention_extra(audio)
|
|
355
478
|
final_text = session.stop(audio)
|
|
356
479
|
if self._generation != gen:
|
|
357
480
|
log.debug("Finalize cancelled (gen %d, current %d)", gen, self._generation)
|
|
358
481
|
return
|
|
359
482
|
if final_text:
|
|
360
483
|
self._play_feedback(final_text)
|
|
484
|
+
stored = self._strip_voice_prefix(final_text)
|
|
485
|
+
self._prompt_builder.add_transcript(stored)
|
|
361
486
|
from voiceio import history
|
|
362
|
-
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
|
+
)
|
|
363
494
|
log.info("Streaming done (%.1fs): '%s'", elapsed, final_text)
|
|
364
495
|
# Transition to IDLE under lock to avoid racing with _toggle
|
|
365
496
|
with self._hotkey_lock:
|
|
@@ -372,7 +503,10 @@ class VoiceIO:
|
|
|
372
503
|
try:
|
|
373
504
|
if self._generation != gen:
|
|
374
505
|
return
|
|
375
|
-
|
|
506
|
+
extra = self._retention_extra(audio)
|
|
507
|
+
raw = self.transcriber.transcribe(audio, final=True)
|
|
508
|
+
segments = self.transcriber.last_segments
|
|
509
|
+
text = raw
|
|
376
510
|
if self._generation != gen:
|
|
377
511
|
return
|
|
378
512
|
if text:
|
|
@@ -384,6 +518,7 @@ class VoiceIO:
|
|
|
384
518
|
language=self.cfg.model.language,
|
|
385
519
|
commands=self._command_processor,
|
|
386
520
|
corrections=self._corrections,
|
|
521
|
+
postcorrect=self._postcorrect,
|
|
387
522
|
llm=self._llm,
|
|
388
523
|
voice_input_prefix=self._voice_input_prefix,
|
|
389
524
|
final=True,
|
|
@@ -393,8 +528,16 @@ class VoiceIO:
|
|
|
393
528
|
if text:
|
|
394
529
|
self._type_with_fallback(text)
|
|
395
530
|
self._play_feedback(text)
|
|
531
|
+
stored = self._strip_voice_prefix(text)
|
|
532
|
+
self._prompt_builder.add_transcript(stored)
|
|
396
533
|
from voiceio import history
|
|
397
|
-
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
|
+
)
|
|
398
541
|
log.info("Typed: '%s'", text)
|
|
399
542
|
except Exception:
|
|
400
543
|
log.exception("Processing failed")
|