python-voiceio 0.2.1__tar.gz → 0.2.4__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.
- {python_voiceio-0.2.1/python_voiceio.egg-info → python_voiceio-0.2.4}/PKG-INFO +12 -33
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/README.md +8 -31
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/pyproject.toml +4 -2
- {python_voiceio-0.2.1 → python_voiceio-0.2.4/python_voiceio.egg-info}/PKG-INFO +12 -33
- python_voiceio-0.2.4/tests/test_app_wiring.py +139 -0
- python_voiceio-0.2.4/voiceio/__init__.py +1 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/app.py +17 -18
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/cli.py +41 -4
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/config.py +2 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/evdev.py +6 -2
- python_voiceio-0.2.4/voiceio/sounds/commit.wav +0 -0
- python_voiceio-0.2.4/voiceio/sounds/start.wav +0 -0
- python_voiceio-0.2.4/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/wizard.py +13 -12
- python_voiceio-0.2.1/tests/test_app_wiring.py +0 -135
- python_voiceio-0.2.1/voiceio/__init__.py +0 -1
- python_voiceio-0.2.1/voiceio/sounds/commit.wav +0 -0
- python_voiceio-0.2.1/voiceio/sounds/start.wav +0 -0
- python_voiceio-0.2.1/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/LICENSE +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/setup.cfg +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_config.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_fallback.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_health.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_platform.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_prebuffer.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_streaming.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/tests/test_transcriber.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/__main__.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/backends.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/feedback.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/health.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/platform.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/recorder.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/service.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/streaming.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/transcriber.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/tray.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-0.2.1 → python_voiceio-0.2.4}/voiceio/worker.py +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: 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
|
+
Project-URL: Repository, https://github.com/Hugo0/voiceio
|
|
8
9
|
Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
|
|
9
11
|
Keywords: voice,speech-to-text,whisper,linux,dictation,wayland,ibus
|
|
10
12
|
Classifier: Development Status :: 4 - Beta
|
|
11
13
|
Classifier: Environment :: X11 Applications
|
|
@@ -39,18 +41,7 @@ Dynamic: license-file
|
|
|
39
41
|
[](https://pypi.org/project/python-voiceio/)
|
|
40
42
|
[](LICENSE)
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
100% local and open source. No API keys, no cloud, no telemetry. Use and modify at your will.
|
|
45
|
-
|
|
46
|
-
<!-- demo video -->
|
|
47
|
-
<p align="center">
|
|
48
|
-
<a href="https://www.tella.tv/video/YOUR_VIDEO_ID">
|
|
49
|
-
<img src="https://github.com/Hugo0/voiceio/raw/main/assets/demo-thumbnail.png" alt="voiceio demo" width="600">
|
|
50
|
-
</a>
|
|
51
|
-
<br>
|
|
52
|
-
<em>Click to watch the demo</em>
|
|
53
|
-
</p>
|
|
44
|
+
Speak → text, locally, instantly.
|
|
54
45
|
|
|
55
46
|
## Quick start
|
|
56
47
|
|
|
@@ -100,7 +91,7 @@ voiceio setup
|
|
|
100
91
|
```
|
|
101
92
|
</details>
|
|
102
93
|
|
|
103
|
-
> You can also install with `uv tool install voiceio` or `pip install voiceio`.
|
|
94
|
+
> You can also install with `uv tool install python-voiceio` or `pip install python-voiceio`.
|
|
104
95
|
|
|
105
96
|
## How it works
|
|
106
97
|
|
|
@@ -145,6 +136,7 @@ voiceio setup Interactive setup wizard
|
|
|
145
136
|
voiceio doctor Health check (--fix to auto-repair)
|
|
146
137
|
voiceio test Test microphone + live transcription
|
|
147
138
|
voiceio toggle Toggle recording on a running daemon
|
|
139
|
+
voiceio update Update to latest version
|
|
148
140
|
voiceio service install Autostart on login via systemd
|
|
149
141
|
voiceio logs View recent logs
|
|
150
142
|
voiceio uninstall Remove all system integrations
|
|
@@ -196,28 +188,15 @@ voiceio uninstall # removes service, IBus, shortcuts, symlinks
|
|
|
196
188
|
pipx uninstall python-voiceio # removes the package
|
|
197
189
|
```
|
|
198
190
|
|
|
199
|
-
## TODO
|
|
200
|
-
|
|
201
|
-
**Launch**
|
|
202
|
-
- [ ] Publish to PyPI
|
|
203
|
-
- [ ] Record demo video + thumbnail
|
|
204
|
-
- [ ] Test clean install on a fresh VM/container
|
|
205
|
-
- [ ] GitHub repo: description, topics, social preview image
|
|
206
|
-
- [ ] Bump version to 0.2.0
|
|
207
|
-
|
|
208
|
-
**Code quality**
|
|
209
|
-
- [ ] IBus activation on non-GNOME desktops (KDE, Sway, Hyprland), currently GNOME-only via gsettings
|
|
210
|
-
- [ ] `voiceio doctor --json` for machine-readable output
|
|
211
|
-
- [ ] Shell completions (`voiceio completion bash/zsh/fish`)
|
|
212
|
-
- [ ] Refactor wizard.py (882 lines) into smaller, testable modules
|
|
213
|
-
- [ ] Socket protocol versioning (e.g. `v1:preedit:text`)
|
|
214
|
-
- [ ] Configurable log file path
|
|
215
|
-
|
|
216
191
|
## Wishlist
|
|
217
192
|
|
|
218
|
-
Contributions welcome! Open an issue to discuss before starting.
|
|
193
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md). Open an issue to discuss before starting.
|
|
219
194
|
|
|
220
195
|
**High impact**
|
|
196
|
+
- [ ] **macOS support**: test and polish pynput hotkey + typer backends
|
|
197
|
+
- [ ] **Silence filtering**: VAD-based trimming to prevent Whisper hallucinations on silence
|
|
198
|
+
- [ ] **distil-whisper models**: better speed/accuracy tradeoffs
|
|
199
|
+
- [ ] **IBus on non-GNOME desktops**: KDE, Sway, Hyprland activation (currently GNOME-only via gsettings)
|
|
221
200
|
- [ ] **Text-to-speech (voice output)**: select text, press a hotkey, hear it spoken aloud. Completes the "io" in voiceio. Use a local TTS engine (Piper, Coqui, espeak-ng), same philosophy: no cloud, no API keys
|
|
222
201
|
- [ ] **Wake word**: "Hey voiceio" hands-free activation (no hotkey needed). Use a small always-on keyword model (e.g. openWakeWord, Porcupine)
|
|
223
202
|
- [ ] **Custom vocabulary / hot words**: user-defined word list for names, jargon, technical terms that Whisper gets wrong. Boost via `initial_prompt` or fine-tuned logit bias
|
|
@@ -5,18 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/python-voiceio/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
100% local and open source. No API keys, no cloud, no telemetry. Use and modify at your will.
|
|
11
|
-
|
|
12
|
-
<!-- demo video -->
|
|
13
|
-
<p align="center">
|
|
14
|
-
<a href="https://www.tella.tv/video/YOUR_VIDEO_ID">
|
|
15
|
-
<img src="https://github.com/Hugo0/voiceio/raw/main/assets/demo-thumbnail.png" alt="voiceio demo" width="600">
|
|
16
|
-
</a>
|
|
17
|
-
<br>
|
|
18
|
-
<em>Click to watch the demo</em>
|
|
19
|
-
</p>
|
|
8
|
+
Speak → text, locally, instantly.
|
|
20
9
|
|
|
21
10
|
## Quick start
|
|
22
11
|
|
|
@@ -66,7 +55,7 @@ voiceio setup
|
|
|
66
55
|
```
|
|
67
56
|
</details>
|
|
68
57
|
|
|
69
|
-
> You can also install with `uv tool install voiceio` or `pip install voiceio`.
|
|
58
|
+
> You can also install with `uv tool install python-voiceio` or `pip install python-voiceio`.
|
|
70
59
|
|
|
71
60
|
## How it works
|
|
72
61
|
|
|
@@ -111,6 +100,7 @@ voiceio setup Interactive setup wizard
|
|
|
111
100
|
voiceio doctor Health check (--fix to auto-repair)
|
|
112
101
|
voiceio test Test microphone + live transcription
|
|
113
102
|
voiceio toggle Toggle recording on a running daemon
|
|
103
|
+
voiceio update Update to latest version
|
|
114
104
|
voiceio service install Autostart on login via systemd
|
|
115
105
|
voiceio logs View recent logs
|
|
116
106
|
voiceio uninstall Remove all system integrations
|
|
@@ -162,28 +152,15 @@ voiceio uninstall # removes service, IBus, shortcuts, symlinks
|
|
|
162
152
|
pipx uninstall python-voiceio # removes the package
|
|
163
153
|
```
|
|
164
154
|
|
|
165
|
-
## TODO
|
|
166
|
-
|
|
167
|
-
**Launch**
|
|
168
|
-
- [ ] Publish to PyPI
|
|
169
|
-
- [ ] Record demo video + thumbnail
|
|
170
|
-
- [ ] Test clean install on a fresh VM/container
|
|
171
|
-
- [ ] GitHub repo: description, topics, social preview image
|
|
172
|
-
- [ ] Bump version to 0.2.0
|
|
173
|
-
|
|
174
|
-
**Code quality**
|
|
175
|
-
- [ ] IBus activation on non-GNOME desktops (KDE, Sway, Hyprland), currently GNOME-only via gsettings
|
|
176
|
-
- [ ] `voiceio doctor --json` for machine-readable output
|
|
177
|
-
- [ ] Shell completions (`voiceio completion bash/zsh/fish`)
|
|
178
|
-
- [ ] Refactor wizard.py (882 lines) into smaller, testable modules
|
|
179
|
-
- [ ] Socket protocol versioning (e.g. `v1:preedit:text`)
|
|
180
|
-
- [ ] Configurable log file path
|
|
181
|
-
|
|
182
155
|
## Wishlist
|
|
183
156
|
|
|
184
|
-
Contributions welcome! Open an issue to discuss before starting.
|
|
157
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md). Open an issue to discuss before starting.
|
|
185
158
|
|
|
186
159
|
**High impact**
|
|
160
|
+
- [ ] **macOS support**: test and polish pynput hotkey + typer backends
|
|
161
|
+
- [ ] **Silence filtering**: VAD-based trimming to prevent Whisper hallucinations on silence
|
|
162
|
+
- [ ] **distil-whisper models**: better speed/accuracy tradeoffs
|
|
163
|
+
- [ ] **IBus on non-GNOME desktops**: KDE, Sway, Hyprland activation (currently GNOME-only via gsettings)
|
|
187
164
|
- [ ] **Text-to-speech (voice output)**: select text, press a hotkey, hear it spoken aloud. Completes the "io" in voiceio. Use a local TTS engine (Piper, Coqui, espeak-ng), same philosophy: no cloud, no API keys
|
|
188
165
|
- [ ] **Wake word**: "Hey voiceio" hands-free activation (no hotkey needed). Use a small always-on keyword model (e.g. openWakeWord, Porcupine)
|
|
189
166
|
- [ ] **Custom vocabulary / hot words**: user-defined word list for names, jargon, technical terms that Whisper gets wrong. Boost via `initial_prompt` or fine-tuned logit bias
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-voiceio"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.4"
|
|
8
|
+
description = "Speak → text, locally, instantly."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -34,7 +34,9 @@ dev = ["pytest>=7.0", "pytest-mock"]
|
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
36
|
Homepage = "https://github.com/Hugo0/voiceio"
|
|
37
|
+
Repository = "https://github.com/Hugo0/voiceio"
|
|
37
38
|
Issues = "https://github.com/Hugo0/voiceio/issues"
|
|
39
|
+
Changelog = "https://github.com/Hugo0/voiceio/releases"
|
|
38
40
|
|
|
39
41
|
[project.scripts]
|
|
40
42
|
voiceio = "voiceio.cli:main"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: 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
|
+
Project-URL: Repository, https://github.com/Hugo0/voiceio
|
|
8
9
|
Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
|
|
9
11
|
Keywords: voice,speech-to-text,whisper,linux,dictation,wayland,ibus
|
|
10
12
|
Classifier: Development Status :: 4 - Beta
|
|
11
13
|
Classifier: Environment :: X11 Applications
|
|
@@ -39,18 +41,7 @@ Dynamic: license-file
|
|
|
39
41
|
[](https://pypi.org/project/python-voiceio/)
|
|
40
42
|
[](LICENSE)
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
100% local and open source. No API keys, no cloud, no telemetry. Use and modify at your will.
|
|
45
|
-
|
|
46
|
-
<!-- demo video -->
|
|
47
|
-
<p align="center">
|
|
48
|
-
<a href="https://www.tella.tv/video/YOUR_VIDEO_ID">
|
|
49
|
-
<img src="https://github.com/Hugo0/voiceio/raw/main/assets/demo-thumbnail.png" alt="voiceio demo" width="600">
|
|
50
|
-
</a>
|
|
51
|
-
<br>
|
|
52
|
-
<em>Click to watch the demo</em>
|
|
53
|
-
</p>
|
|
44
|
+
Speak → text, locally, instantly.
|
|
54
45
|
|
|
55
46
|
## Quick start
|
|
56
47
|
|
|
@@ -100,7 +91,7 @@ voiceio setup
|
|
|
100
91
|
```
|
|
101
92
|
</details>
|
|
102
93
|
|
|
103
|
-
> You can also install with `uv tool install voiceio` or `pip install voiceio`.
|
|
94
|
+
> You can also install with `uv tool install python-voiceio` or `pip install python-voiceio`.
|
|
104
95
|
|
|
105
96
|
## How it works
|
|
106
97
|
|
|
@@ -145,6 +136,7 @@ voiceio setup Interactive setup wizard
|
|
|
145
136
|
voiceio doctor Health check (--fix to auto-repair)
|
|
146
137
|
voiceio test Test microphone + live transcription
|
|
147
138
|
voiceio toggle Toggle recording on a running daemon
|
|
139
|
+
voiceio update Update to latest version
|
|
148
140
|
voiceio service install Autostart on login via systemd
|
|
149
141
|
voiceio logs View recent logs
|
|
150
142
|
voiceio uninstall Remove all system integrations
|
|
@@ -196,28 +188,15 @@ voiceio uninstall # removes service, IBus, shortcuts, symlinks
|
|
|
196
188
|
pipx uninstall python-voiceio # removes the package
|
|
197
189
|
```
|
|
198
190
|
|
|
199
|
-
## TODO
|
|
200
|
-
|
|
201
|
-
**Launch**
|
|
202
|
-
- [ ] Publish to PyPI
|
|
203
|
-
- [ ] Record demo video + thumbnail
|
|
204
|
-
- [ ] Test clean install on a fresh VM/container
|
|
205
|
-
- [ ] GitHub repo: description, topics, social preview image
|
|
206
|
-
- [ ] Bump version to 0.2.0
|
|
207
|
-
|
|
208
|
-
**Code quality**
|
|
209
|
-
- [ ] IBus activation on non-GNOME desktops (KDE, Sway, Hyprland), currently GNOME-only via gsettings
|
|
210
|
-
- [ ] `voiceio doctor --json` for machine-readable output
|
|
211
|
-
- [ ] Shell completions (`voiceio completion bash/zsh/fish`)
|
|
212
|
-
- [ ] Refactor wizard.py (882 lines) into smaller, testable modules
|
|
213
|
-
- [ ] Socket protocol versioning (e.g. `v1:preedit:text`)
|
|
214
|
-
- [ ] Configurable log file path
|
|
215
|
-
|
|
216
191
|
## Wishlist
|
|
217
192
|
|
|
218
|
-
Contributions welcome! Open an issue to discuss before starting.
|
|
193
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md). Open an issue to discuss before starting.
|
|
219
194
|
|
|
220
195
|
**High impact**
|
|
196
|
+
- [ ] **macOS support**: test and polish pynput hotkey + typer backends
|
|
197
|
+
- [ ] **Silence filtering**: VAD-based trimming to prevent Whisper hallucinations on silence
|
|
198
|
+
- [ ] **distil-whisper models**: better speed/accuracy tradeoffs
|
|
199
|
+
- [ ] **IBus on non-GNOME desktops**: KDE, Sway, Hyprland activation (currently GNOME-only via gsettings)
|
|
221
200
|
- [ ] **Text-to-speech (voice output)**: select text, press a hotkey, hear it spoken aloud. Completes the "io" in voiceio. Use a local TTS engine (Piper, Coqui, espeak-ng), same philosophy: no cloud, no API keys
|
|
222
201
|
- [ ] **Wake word**: "Hey voiceio" hands-free activation (no hotkey needed). Use a small always-on keyword model (e.g. openWakeWord, Porcupine)
|
|
223
202
|
- [ ] **Custom vocabulary / hot words**: user-defined word list for names, jargon, technical terms that Whisper gets wrong. Boost via `initial_prompt` or fine-tuned logit bias
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Test that VoiceIO app wires up correctly with mocked backends."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from voiceio.config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_vio(mock_transcriber=None):
|
|
15
|
+
"""Create a VoiceIO instance with mocked backends."""
|
|
16
|
+
mock_hotkey = MagicMock()
|
|
17
|
+
mock_hotkey.name = "socket"
|
|
18
|
+
mock_typer = MagicMock()
|
|
19
|
+
mock_typer.name = "clipboard"
|
|
20
|
+
if mock_transcriber is None:
|
|
21
|
+
mock_transcriber = MagicMock()
|
|
22
|
+
|
|
23
|
+
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
24
|
+
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
25
|
+
patch("voiceio.app.Transcriber", return_value=mock_transcriber), \
|
|
26
|
+
patch("voiceio.app.plat.detect") as mock_detect:
|
|
27
|
+
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
28
|
+
|
|
29
|
+
from voiceio.app import VoiceIO
|
|
30
|
+
vio = VoiceIO(Config())
|
|
31
|
+
vio.recorder._stream = MagicMock() # skip real audio
|
|
32
|
+
return vio, mock_typer, mock_transcriber
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestVoiceIOInit:
|
|
36
|
+
def test_init_with_mocked_backends(self):
|
|
37
|
+
vio, mock_typer, _ = _make_vio()
|
|
38
|
+
assert vio._typer is mock_typer
|
|
39
|
+
|
|
40
|
+
def test_on_hotkey_toggle_cycle(self):
|
|
41
|
+
mock_trans = MagicMock()
|
|
42
|
+
mock_trans.transcribe.return_value = "hello world"
|
|
43
|
+
vio, _, _ = _make_vio(mock_trans)
|
|
44
|
+
|
|
45
|
+
# Start recording
|
|
46
|
+
vio.on_hotkey()
|
|
47
|
+
assert vio.recorder.is_recording
|
|
48
|
+
|
|
49
|
+
# Feed some audio
|
|
50
|
+
for _ in range(20):
|
|
51
|
+
data = np.full((1024, 1), 0.3, dtype=np.float32)
|
|
52
|
+
vio.recorder._callback(data, 1024, None, None)
|
|
53
|
+
|
|
54
|
+
# Set record_start far back so debounce allows stop
|
|
55
|
+
vio._record_start = time.monotonic() - 5.0
|
|
56
|
+
vio._last_hotkey = time.monotonic() - 5.0
|
|
57
|
+
|
|
58
|
+
# Stop recording
|
|
59
|
+
vio.on_hotkey()
|
|
60
|
+
assert not vio.recorder.is_recording
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestHotkeyDebounce:
|
|
64
|
+
"""Verify that duplicate hotkey events are properly debounced."""
|
|
65
|
+
|
|
66
|
+
def test_rapid_duplicate_ignored(self):
|
|
67
|
+
"""Two on_hotkey calls within 0.3s should only trigger once."""
|
|
68
|
+
vio, _, _ = _make_vio()
|
|
69
|
+
|
|
70
|
+
vio.on_hotkey()
|
|
71
|
+
assert vio.recorder.is_recording
|
|
72
|
+
|
|
73
|
+
# Simulate duplicate from socket backend ~50ms later
|
|
74
|
+
vio.on_hotkey()
|
|
75
|
+
# Should still be recording (duplicate was debounced, not treated as stop)
|
|
76
|
+
assert vio.recorder.is_recording
|
|
77
|
+
|
|
78
|
+
def test_stop_after_debounce_window(self):
|
|
79
|
+
"""on_hotkey after debounce window should stop recording."""
|
|
80
|
+
vio, _, _ = _make_vio()
|
|
81
|
+
|
|
82
|
+
vio.on_hotkey()
|
|
83
|
+
assert vio.recorder.is_recording
|
|
84
|
+
|
|
85
|
+
# Feed audio
|
|
86
|
+
for _ in range(20):
|
|
87
|
+
data = np.full((1024, 1), 0.3, dtype=np.float32)
|
|
88
|
+
vio.recorder._callback(data, 1024, None, None)
|
|
89
|
+
|
|
90
|
+
# Move timestamps back so debounce allows through
|
|
91
|
+
vio._record_start = time.monotonic() - 2.0
|
|
92
|
+
vio._last_hotkey = time.monotonic() - 2.0
|
|
93
|
+
|
|
94
|
+
vio.on_hotkey()
|
|
95
|
+
assert not vio.recorder.is_recording
|
|
96
|
+
|
|
97
|
+
def test_concurrent_hotkey_no_phantom_recording(self):
|
|
98
|
+
"""Socket event waiting behind lock must not start phantom recording.
|
|
99
|
+
|
|
100
|
+
This is the critical race: evdev stops recording (takes time),
|
|
101
|
+
socket event waits on lock, then must be debounced when lock releases.
|
|
102
|
+
"""
|
|
103
|
+
vio, _, _ = _make_vio()
|
|
104
|
+
|
|
105
|
+
# Start recording
|
|
106
|
+
vio.on_hotkey()
|
|
107
|
+
assert vio.recorder.is_recording
|
|
108
|
+
|
|
109
|
+
# Feed audio
|
|
110
|
+
for _ in range(20):
|
|
111
|
+
data = np.full((1024, 1), 0.3, dtype=np.float32)
|
|
112
|
+
vio.recorder._callback(data, 1024, None, None)
|
|
113
|
+
|
|
114
|
+
# Allow stop
|
|
115
|
+
vio._record_start = time.monotonic() - 2.0
|
|
116
|
+
vio._last_hotkey = time.monotonic() - 2.0
|
|
117
|
+
|
|
118
|
+
# Simulate: evdev thread stops recording, socket thread waits then fires
|
|
119
|
+
results = []
|
|
120
|
+
|
|
121
|
+
def evdev_stop():
|
|
122
|
+
vio.on_hotkey()
|
|
123
|
+
results.append(("evdev", vio.recorder.is_recording))
|
|
124
|
+
|
|
125
|
+
def socket_delayed():
|
|
126
|
+
time.sleep(0.05) # socket arrives 50ms after evdev
|
|
127
|
+
vio.on_hotkey()
|
|
128
|
+
results.append(("socket", vio.recorder.is_recording))
|
|
129
|
+
|
|
130
|
+
t1 = threading.Thread(target=evdev_stop)
|
|
131
|
+
t2 = threading.Thread(target=socket_delayed)
|
|
132
|
+
t1.start()
|
|
133
|
+
t2.start()
|
|
134
|
+
t1.join(timeout=5)
|
|
135
|
+
t2.join(timeout=5)
|
|
136
|
+
|
|
137
|
+
# Recording must be stopped and NOT restarted by socket
|
|
138
|
+
assert not vio.recorder.is_recording, \
|
|
139
|
+
f"Phantom recording started! results={results}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.4"
|
|
@@ -18,7 +18,6 @@ from voiceio.recorder import AudioRecorder
|
|
|
18
18
|
from voiceio.streaming import StreamingSession
|
|
19
19
|
from voiceio.transcriber import Transcriber
|
|
20
20
|
from voiceio.typers import chain as typer_chain
|
|
21
|
-
from voiceio.typers.base import StreamingTyper
|
|
22
21
|
log = logging.getLogger("voiceio")
|
|
23
22
|
|
|
24
23
|
|
|
@@ -33,7 +32,7 @@ class VoiceIO:
|
|
|
33
32
|
self._typer = typer_chain.select(self.platform, cfg.output.method)
|
|
34
33
|
self._auto_fallback = cfg.health.auto_fallback
|
|
35
34
|
|
|
36
|
-
#
|
|
35
|
+
# Socket backend runs alongside native hotkey for extra robustness
|
|
37
36
|
self._socket: SocketHotkey | None = None
|
|
38
37
|
if self._hotkey.name != "socket":
|
|
39
38
|
self._socket = SocketHotkey()
|
|
@@ -47,6 +46,8 @@ class VoiceIO:
|
|
|
47
46
|
self._session: StreamingSession | None = None
|
|
48
47
|
self._processing = False
|
|
49
48
|
self._record_start: float = 0
|
|
49
|
+
self._hotkey_lock = threading.Lock()
|
|
50
|
+
self._last_hotkey: float = 0
|
|
50
51
|
self._prev_ibus_engine: str | None = None
|
|
51
52
|
self._engine_proc: subprocess.Popen | None = None
|
|
52
53
|
self._shutdown = threading.Event()
|
|
@@ -56,23 +57,21 @@ class VoiceIO:
|
|
|
56
57
|
self._shutdown.set()
|
|
57
58
|
|
|
58
59
|
def on_hotkey(self) -> None:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
self._session.stop()
|
|
65
|
-
self._session = None
|
|
66
|
-
self.recorder.stop()
|
|
67
|
-
if isinstance(self._typer, StreamingTyper):
|
|
68
|
-
self._typer.clear_preedit()
|
|
69
|
-
self._deactivate_ibus()
|
|
70
|
-
log.info("Recording cancelled (double-press)")
|
|
71
|
-
return
|
|
72
|
-
if elapsed < self.cfg.output.min_recording_secs:
|
|
73
|
-
log.debug("Ignoring stop, only %.1fs into recording (min %.1fs)", elapsed, self.cfg.output.min_recording_secs)
|
|
60
|
+
with self._hotkey_lock:
|
|
61
|
+
now = time.monotonic()
|
|
62
|
+
# Deduplicate: multiple backends (evdev + socket) may fire
|
|
63
|
+
# for the same physical keypress
|
|
64
|
+
if now - self._last_hotkey < 0.3:
|
|
74
65
|
return
|
|
66
|
+
self._last_hotkey = now
|
|
67
|
+
self._on_hotkey_inner()
|
|
68
|
+
# Update timestamp after completion so threads that waited
|
|
69
|
+
# behind the lock see a fresh timestamp and get debounced
|
|
70
|
+
self._last_hotkey = time.monotonic()
|
|
75
71
|
|
|
72
|
+
def _on_hotkey_inner(self) -> None:
|
|
73
|
+
if self.recorder.is_recording:
|
|
74
|
+
elapsed = time.monotonic() - self._record_start
|
|
76
75
|
self._play_record_cue(start=False)
|
|
77
76
|
if self._streaming and self._session is not None:
|
|
78
77
|
final_text = self._session.stop()
|
|
@@ -82,11 +81,11 @@ class VoiceIO:
|
|
|
82
81
|
self._play_feedback(final_text)
|
|
83
82
|
log.info("Streaming done (%.1fs): '%s'", elapsed, final_text)
|
|
84
83
|
else:
|
|
84
|
+
self._play_record_cue(start=False)
|
|
85
85
|
audio = self.recorder.stop()
|
|
86
86
|
log.info("Stopped recording (%.1fs)", elapsed)
|
|
87
87
|
if audio is not None and not self._processing:
|
|
88
88
|
threading.Thread(target=self._process, args=(audio,), daemon=True).start()
|
|
89
|
-
# Deactivate IBus engine, return keyboard to normal
|
|
90
89
|
self._deactivate_ibus()
|
|
91
90
|
elif not self._processing:
|
|
92
91
|
# Activate IBus engine so preedit/commit can reach the focused app
|
|
@@ -59,6 +59,9 @@ def main() -> None:
|
|
|
59
59
|
choices=["install", "uninstall", "start", "stop", "status"],
|
|
60
60
|
help="Action to perform (default: status)")
|
|
61
61
|
|
|
62
|
+
# ── voiceio update ──────────────────────────────────────────────────
|
|
63
|
+
sub.add_parser("update", help="Update voiceio to the latest version")
|
|
64
|
+
|
|
62
65
|
# ── voiceio uninstall ──────────────────────────────────────────────
|
|
63
66
|
sub.add_parser("uninstall", help="Remove all voiceio system integrations")
|
|
64
67
|
|
|
@@ -77,6 +80,8 @@ def main() -> None:
|
|
|
77
80
|
_cmd_test()
|
|
78
81
|
elif args.command == "service":
|
|
79
82
|
_cmd_service(args)
|
|
83
|
+
elif args.command == "update":
|
|
84
|
+
_cmd_update()
|
|
80
85
|
elif args.command == "uninstall":
|
|
81
86
|
_cmd_uninstall()
|
|
82
87
|
elif args.command == "logs":
|
|
@@ -283,6 +288,37 @@ def _cmd_service(args: argparse.Namespace) -> None:
|
|
|
283
288
|
sys.exit(1)
|
|
284
289
|
|
|
285
290
|
|
|
291
|
+
def _cmd_update() -> None:
|
|
292
|
+
"""Update voiceio to the latest PyPI version."""
|
|
293
|
+
import subprocess
|
|
294
|
+
from voiceio import __version__
|
|
295
|
+
from voiceio.config import PYPI_NAME
|
|
296
|
+
|
|
297
|
+
is_pipx = "pipx" in sys.prefix
|
|
298
|
+
if is_pipx:
|
|
299
|
+
print(f"Current version: {__version__}")
|
|
300
|
+
print("Checking for updates...")
|
|
301
|
+
try:
|
|
302
|
+
result = subprocess.run(
|
|
303
|
+
["pipx", "upgrade", PYPI_NAME],
|
|
304
|
+
capture_output=True, text=True, timeout=60,
|
|
305
|
+
)
|
|
306
|
+
print(result.stdout.strip())
|
|
307
|
+
if result.returncode != 0 and result.stderr.strip():
|
|
308
|
+
print(result.stderr.strip(), file=sys.stderr)
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
except FileNotFoundError:
|
|
311
|
+
print("pipx not found. Update manually: pipx upgrade " + PYPI_NAME, file=sys.stderr)
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
except subprocess.TimeoutExpired:
|
|
314
|
+
print("Update timed out.", file=sys.stderr)
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
else:
|
|
317
|
+
print("Not a pipx install. Update manually:")
|
|
318
|
+
print(f" pip install --upgrade {PYPI_NAME}")
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
286
322
|
def _cmd_uninstall() -> None:
|
|
287
323
|
"""Remove all voiceio system integrations."""
|
|
288
324
|
import os
|
|
@@ -429,14 +465,15 @@ def _cmd_uninstall() -> None:
|
|
|
429
465
|
print("\nNothing to remove. voiceio was not installed on this system.")
|
|
430
466
|
|
|
431
467
|
# Offer to uninstall the Python package itself
|
|
468
|
+
from voiceio.config import PYPI_NAME
|
|
432
469
|
is_pipx = "pipx" in sys.prefix
|
|
433
470
|
if is_pipx:
|
|
434
471
|
answer = input("\nAlso uninstall the voiceio Python package (pipx uninstall)? [Y/n] ").strip().lower()
|
|
435
472
|
if answer in ("y", "yes", ""):
|
|
436
473
|
try:
|
|
437
|
-
subprocess.run(["pipx", "uninstall",
|
|
474
|
+
subprocess.run(["pipx", "uninstall", PYPI_NAME], timeout=30)
|
|
438
475
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
439
|
-
print("Failed. Run manually: pipx uninstall
|
|
476
|
+
print(f"Failed. Run manually: pipx uninstall {PYPI_NAME}")
|
|
440
477
|
else:
|
|
441
478
|
# Dev install or pip install: check if voiceio is still reachable
|
|
442
479
|
voiceio_bin = shutil.which("voiceio")
|
|
@@ -444,10 +481,10 @@ def _cmd_uninstall() -> None:
|
|
|
444
481
|
print(f"\nNote: 'voiceio' is still available at {voiceio_bin}")
|
|
445
482
|
if ".venv" in str(voiceio_bin) or "site-packages" in str(voiceio_bin):
|
|
446
483
|
print("This is a development install. To fully remove:")
|
|
447
|
-
print(" pip uninstall
|
|
484
|
+
print(f" pip uninstall {PYPI_NAME}")
|
|
448
485
|
else:
|
|
449
486
|
print("To fully remove the package:")
|
|
450
|
-
print(" pip uninstall
|
|
487
|
+
print(f" pip uninstall {PYPI_NAME}")
|
|
451
488
|
else:
|
|
452
489
|
print("\nvoiceio fully removed.")
|
|
453
490
|
|
|
@@ -108,16 +108,20 @@ class EvdevHotkey:
|
|
|
108
108
|
if event.type != ecodes.EV_KEY:
|
|
109
109
|
continue
|
|
110
110
|
key_event = evdev.categorize(event)
|
|
111
|
+
should_trigger = False
|
|
111
112
|
with pressed_lock:
|
|
112
113
|
if key_event.keystate == evdev.KeyEvent.key_down:
|
|
113
114
|
pressed.add(event.code)
|
|
114
115
|
if event.code == key_code and check_mods():
|
|
115
116
|
now = time.monotonic()
|
|
116
|
-
|
|
117
|
+
since = now - last_trigger[0]
|
|
118
|
+
if since >= DEBOUNCE_SECS:
|
|
117
119
|
last_trigger[0] = now
|
|
118
|
-
|
|
120
|
+
should_trigger = True
|
|
119
121
|
elif key_event.keystate == evdev.KeyEvent.key_up:
|
|
120
122
|
pressed.discard(event.code)
|
|
123
|
+
if should_trigger:
|
|
124
|
+
on_trigger()
|
|
121
125
|
except OSError:
|
|
122
126
|
pass
|
|
123
127
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -844,22 +844,23 @@ def run_wizard() -> None:
|
|
|
844
844
|
_streaming_test(model=_get_or_load_model())
|
|
845
845
|
|
|
846
846
|
# ── Done ────────────────────────────────────────────────────────────
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
847
|
+
# Start (or restart) the service so it's immediately usable
|
|
848
|
+
if autostart_idx == 0:
|
|
849
|
+
from voiceio.service import is_running
|
|
850
|
+
action = "restart" if is_running() else "start"
|
|
851
|
+
try:
|
|
852
|
+
subprocess.run(
|
|
853
|
+
["systemctl", "--user", action, "voiceio.service"],
|
|
854
|
+
capture_output=True, timeout=5,
|
|
855
|
+
)
|
|
856
|
+
print(f" {GREEN}✓{RESET} {DIM}voiceio service started{RESET}")
|
|
857
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
858
|
+
pass
|
|
856
859
|
|
|
857
860
|
from voiceio.config import LOG_PATH
|
|
858
861
|
log_path = LOG_PATH
|
|
859
862
|
start_hint = (
|
|
860
|
-
|
|
861
|
-
f" Or start now:\n"
|
|
862
|
-
f" {CYAN}systemctl --user start voiceio{RESET}"
|
|
863
|
+
" voiceio is running and will start automatically on login."
|
|
863
864
|
if autostart_idx == 0
|
|
864
865
|
else f" Start voiceio:\n {CYAN}voiceio{RESET}"
|
|
865
866
|
)
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"""Test that VoiceIO app wires up correctly with mocked backends."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from unittest.mock import MagicMock, patch
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from voiceio.config import Config
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestVoiceIOInit:
|
|
13
|
-
"""Verify VoiceIO can be constructed without real hardware."""
|
|
14
|
-
|
|
15
|
-
def test_init_with_mocked_backends(self):
|
|
16
|
-
from voiceio.backends import ProbeResult
|
|
17
|
-
|
|
18
|
-
mock_hotkey = MagicMock()
|
|
19
|
-
mock_hotkey.name = "socket"
|
|
20
|
-
mock_hotkey.probe.return_value = ProbeResult(ok=True)
|
|
21
|
-
|
|
22
|
-
mock_typer = MagicMock()
|
|
23
|
-
mock_typer.name = "clipboard"
|
|
24
|
-
mock_typer.probe.return_value = ProbeResult(ok=True)
|
|
25
|
-
|
|
26
|
-
mock_transcriber = MagicMock()
|
|
27
|
-
|
|
28
|
-
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
29
|
-
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
30
|
-
patch("voiceio.app.Transcriber", return_value=mock_transcriber), \
|
|
31
|
-
patch("voiceio.app.plat.detect") as mock_detect:
|
|
32
|
-
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
33
|
-
|
|
34
|
-
from voiceio.app import VoiceIO
|
|
35
|
-
vio = VoiceIO(Config())
|
|
36
|
-
|
|
37
|
-
assert vio._hotkey is mock_hotkey
|
|
38
|
-
assert vio._typer is mock_typer
|
|
39
|
-
|
|
40
|
-
def test_on_hotkey_toggle_cycle(self):
|
|
41
|
-
"""Test on_hotkey start/stop without real audio."""
|
|
42
|
-
from voiceio.backends import ProbeResult
|
|
43
|
-
|
|
44
|
-
mock_hotkey = MagicMock()
|
|
45
|
-
mock_hotkey.name = "socket"
|
|
46
|
-
|
|
47
|
-
mock_typer = MagicMock()
|
|
48
|
-
mock_typer.name = "clipboard"
|
|
49
|
-
|
|
50
|
-
mock_transcriber = MagicMock()
|
|
51
|
-
mock_transcriber.transcribe.return_value = "hello world"
|
|
52
|
-
|
|
53
|
-
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
54
|
-
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
55
|
-
patch("voiceio.app.Transcriber", return_value=mock_transcriber), \
|
|
56
|
-
patch("voiceio.app.plat.detect") as mock_detect:
|
|
57
|
-
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
58
|
-
|
|
59
|
-
from voiceio.app import VoiceIO
|
|
60
|
-
vio = VoiceIO(Config())
|
|
61
|
-
vio.recorder._stream = MagicMock() # skip real audio
|
|
62
|
-
|
|
63
|
-
# First toggle: start recording
|
|
64
|
-
vio.on_hotkey()
|
|
65
|
-
assert vio.recorder.is_recording
|
|
66
|
-
|
|
67
|
-
# Simulate some audio coming in
|
|
68
|
-
for _ in range(20):
|
|
69
|
-
data = np.full((1024, 1), 0.3, dtype=np.float32)
|
|
70
|
-
vio.recorder._callback(data, 1024, None, None)
|
|
71
|
-
|
|
72
|
-
# Hack: set _record_start far enough back
|
|
73
|
-
import time
|
|
74
|
-
vio._record_start = time.monotonic() - 5.0
|
|
75
|
-
|
|
76
|
-
# Second toggle: stop recording
|
|
77
|
-
vio.on_hotkey()
|
|
78
|
-
assert not vio.recorder.is_recording
|
|
79
|
-
|
|
80
|
-
def test_double_press_cancels_recording(self):
|
|
81
|
-
"""Rapid double-press (< 0.5s) cancels without typing."""
|
|
82
|
-
mock_hotkey = MagicMock()
|
|
83
|
-
mock_hotkey.name = "socket"
|
|
84
|
-
mock_typer = MagicMock()
|
|
85
|
-
mock_typer.name = "clipboard"
|
|
86
|
-
|
|
87
|
-
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
88
|
-
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
89
|
-
patch("voiceio.app.Transcriber") as mock_trans_cls, \
|
|
90
|
-
patch("voiceio.app.plat.detect") as mock_detect:
|
|
91
|
-
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
92
|
-
|
|
93
|
-
from voiceio.app import VoiceIO
|
|
94
|
-
vio = VoiceIO(Config())
|
|
95
|
-
vio.recorder._stream = MagicMock()
|
|
96
|
-
|
|
97
|
-
# Start recording
|
|
98
|
-
vio.on_hotkey()
|
|
99
|
-
assert vio.recorder.is_recording
|
|
100
|
-
|
|
101
|
-
# Immediately double-press (< 0.5s) - cancels
|
|
102
|
-
vio.on_hotkey()
|
|
103
|
-
assert not vio.recorder.is_recording
|
|
104
|
-
mock_typer.type_text.assert_not_called()
|
|
105
|
-
|
|
106
|
-
def test_min_recording_duration_enforced(self):
|
|
107
|
-
"""Press between cancel_window and min_recording should be ignored."""
|
|
108
|
-
import time
|
|
109
|
-
mock_hotkey = MagicMock()
|
|
110
|
-
mock_hotkey.name = "socket"
|
|
111
|
-
mock_typer = MagicMock()
|
|
112
|
-
mock_typer.name = "clipboard"
|
|
113
|
-
|
|
114
|
-
cfg = Config()
|
|
115
|
-
|
|
116
|
-
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
117
|
-
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
118
|
-
patch("voiceio.app.Transcriber") as mock_trans_cls, \
|
|
119
|
-
patch("voiceio.app.plat.detect") as mock_detect:
|
|
120
|
-
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
121
|
-
|
|
122
|
-
from voiceio.app import VoiceIO
|
|
123
|
-
vio = VoiceIO(cfg)
|
|
124
|
-
vio.recorder._stream = MagicMock()
|
|
125
|
-
|
|
126
|
-
# Start recording
|
|
127
|
-
vio.on_hotkey()
|
|
128
|
-
assert vio.recorder.is_recording
|
|
129
|
-
|
|
130
|
-
# Set record_start past cancel window but before min duration
|
|
131
|
-
vio._record_start = time.monotonic() - (cfg.output.cancel_window_secs + 0.1)
|
|
132
|
-
|
|
133
|
-
# Press again - should be ignored
|
|
134
|
-
vio.on_hotkey()
|
|
135
|
-
assert vio.recorder.is_recording # still recording
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|