python-voiceio 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_voiceio-0.4.0/python_voiceio.egg-info → python_voiceio-0.5.0}/PKG-INFO +14 -6
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/README.md +9 -5
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/pyproject.toml +5 -1
- {python_voiceio-0.4.0 → python_voiceio-0.5.0/python_voiceio.egg-info}/PKG-INFO +14 -6
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/python_voiceio.egg-info/SOURCES.txt +6 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/python_voiceio.egg-info/requires.txt +5 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_app_wiring.py +52 -1
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_clipboard_read.py +54 -0
- python_voiceio-0.5.0/tests/test_concurrency_lockdown.py +254 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_config.py +13 -0
- python_voiceio-0.5.0/tests/test_ibus_pending.py +38 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_ibus_typer.py +57 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_prebuffer.py +46 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_robustness.py +53 -0
- python_voiceio-0.5.0/tests/test_security_hardening.py +238 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_streaming.py +170 -0
- python_voiceio-0.5.0/tests/test_transcriber.py +132 -0
- python_voiceio-0.5.0/tests/test_wizard.py +230 -0
- python_voiceio-0.5.0/voiceio/__init__.py +1 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/app.py +289 -80
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/cli.py +148 -30
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/clipboard_read.py +47 -1
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/config.py +129 -0
- python_voiceio-0.5.0/voiceio/consent.py +57 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/demo.py +2 -2
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/health.py +13 -4
- python_voiceio-0.5.0/voiceio/history.py +181 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/ibus/engine.py +18 -8
- python_voiceio-0.5.0/voiceio/ibus/pending.py +43 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/llm_api.py +37 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/platform.py +121 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/postcorrect.py +7 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/recorder.py +26 -12
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/retention.py +13 -1
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/streaming.py +91 -5
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/transcriber.py +38 -4
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/ibus.py +49 -8
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/ydotool.py +40 -2
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/wizard.py +433 -80
- python_voiceio-0.4.0/tests/test_transcriber.py +0 -69
- python_voiceio-0.4.0/voiceio/__init__.py +0 -1
- python_voiceio-0.4.0/voiceio/history.py +0 -88
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/LICENSE +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/setup.cfg +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_adjudicate.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_audit.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_autocorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_autocorrect_state.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_cli.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_commands.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_correct_batch.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_corrections.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_fallback.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_health.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_hints.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_history.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_llm.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_numbers.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_platform.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_prompt.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_retention.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_snapshots.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_tts.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_vad.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/__main__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/audit.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/autocorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/autocorrect_state.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/backends.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/commands.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/corrections.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/feedback.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hints.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/llm.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/numbers.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/prompt.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/service.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/snapshots.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/vad.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/wordfreq.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.5.0}/voiceio/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -40,6 +40,10 @@ Requires-Dist: edge-tts>=6.1.0; extra == "tts-cloud"
|
|
|
40
40
|
Provides-Extra: dev
|
|
41
41
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
42
42
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
43
|
+
Provides-Extra: linux
|
|
44
|
+
Requires-Dist: pystray>=0.19; extra == "linux"
|
|
45
|
+
Requires-Dist: Pillow>=10.0; extra == "linux"
|
|
46
|
+
Requires-Dist: piper-tts>=1.2.0; extra == "linux"
|
|
43
47
|
Dynamic: license-file
|
|
44
48
|
|
|
45
49
|
# voiceio
|
|
@@ -74,8 +78,9 @@ Open-source, sovereign voice input/output. Your speech is transcribed on your ow
|
|
|
74
78
|
## Quick start
|
|
75
79
|
|
|
76
80
|
```bash
|
|
77
|
-
# 1. Install system dependencies (Ubuntu/Debian)
|
|
78
|
-
|
|
81
|
+
# 1. Install system dependencies (Ubuntu/Debian). build-essential + python3-dev
|
|
82
|
+
# are needed to compile evdev (no prebuilt wheel); portaudio for the mic.
|
|
83
|
+
sudo apt install pipx build-essential python3-dev portaudio19-dev ibus gir1.2-ibus-1.0 python3-gi
|
|
79
84
|
|
|
80
85
|
# 2. Install voiceio
|
|
81
86
|
pipx install python-voiceio
|
|
@@ -86,11 +91,14 @@ voiceio setup
|
|
|
86
91
|
|
|
87
92
|
That's it. Press **Ctrl+Alt+V** (or your chosen hotkey) to start dictating.
|
|
88
93
|
|
|
94
|
+
**Installing with an AI agent?** Point it at [INSTALL.md](INSTALL.md) — a terse,
|
|
95
|
+
copy-pasteable runbook (`voiceio setup --defaults` / `--answers '{json}'`, no TTY needed).
|
|
96
|
+
|
|
89
97
|
<details>
|
|
90
98
|
<summary><strong>Fedora</strong></summary>
|
|
91
99
|
|
|
92
100
|
```bash
|
|
93
|
-
sudo dnf install pipx
|
|
101
|
+
sudo dnf install pipx gcc gcc-c++ make python3-devel portaudio-devel ibus ibus-libs python3-gobject
|
|
94
102
|
pipx install python-voiceio
|
|
95
103
|
voiceio setup
|
|
96
104
|
```
|
|
@@ -100,8 +108,8 @@ voiceio setup
|
|
|
100
108
|
<summary><strong>Arch Linux</strong></summary>
|
|
101
109
|
|
|
102
110
|
```bash
|
|
103
|
-
sudo pacman -S python-pipx ibus python-gobject
|
|
104
|
-
#
|
|
111
|
+
sudo pacman -S python-pipx base-devel portaudio ibus python-gobject
|
|
112
|
+
# base-devel provides gcc/make; the python package ships headers.
|
|
105
113
|
pipx install python-voiceio
|
|
106
114
|
voiceio setup
|
|
107
115
|
```
|
|
@@ -30,8 +30,9 @@ Open-source, sovereign voice input/output. Your speech is transcribed on your ow
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
# 1. Install system dependencies (Ubuntu/Debian)
|
|
34
|
-
|
|
33
|
+
# 1. Install system dependencies (Ubuntu/Debian). build-essential + python3-dev
|
|
34
|
+
# are needed to compile evdev (no prebuilt wheel); portaudio for the mic.
|
|
35
|
+
sudo apt install pipx build-essential python3-dev portaudio19-dev ibus gir1.2-ibus-1.0 python3-gi
|
|
35
36
|
|
|
36
37
|
# 2. Install voiceio
|
|
37
38
|
pipx install python-voiceio
|
|
@@ -42,11 +43,14 @@ voiceio setup
|
|
|
42
43
|
|
|
43
44
|
That's it. Press **Ctrl+Alt+V** (or your chosen hotkey) to start dictating.
|
|
44
45
|
|
|
46
|
+
**Installing with an AI agent?** Point it at [INSTALL.md](INSTALL.md) — a terse,
|
|
47
|
+
copy-pasteable runbook (`voiceio setup --defaults` / `--answers '{json}'`, no TTY needed).
|
|
48
|
+
|
|
45
49
|
<details>
|
|
46
50
|
<summary><strong>Fedora</strong></summary>
|
|
47
51
|
|
|
48
52
|
```bash
|
|
49
|
-
sudo dnf install pipx
|
|
53
|
+
sudo dnf install pipx gcc gcc-c++ make python3-devel portaudio-devel ibus ibus-libs python3-gobject
|
|
50
54
|
pipx install python-voiceio
|
|
51
55
|
voiceio setup
|
|
52
56
|
```
|
|
@@ -56,8 +60,8 @@ voiceio setup
|
|
|
56
60
|
<summary><strong>Arch Linux</strong></summary>
|
|
57
61
|
|
|
58
62
|
```bash
|
|
59
|
-
sudo pacman -S python-pipx ibus python-gobject
|
|
60
|
-
#
|
|
63
|
+
sudo pacman -S python-pipx base-devel portaudio ibus python-gobject
|
|
64
|
+
# base-devel provides gcc/make; the python package ships headers.
|
|
61
65
|
pipx install python-voiceio
|
|
62
66
|
voiceio setup
|
|
63
67
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-voiceio"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Voice dictation for Linux. Speak → text, locally, instantly."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -37,6 +37,10 @@ tray = ["pystray>=0.19", "Pillow>=10.0"]
|
|
|
37
37
|
tts = ["piper-tts>=1.2.0"]
|
|
38
38
|
tts-cloud = ["edge-tts>=6.1.0"]
|
|
39
39
|
dev = ["pytest>=7.0", "pytest-mock"]
|
|
40
|
+
# Convenience bundle for a full Linux install (tray icon + local TTS). The core
|
|
41
|
+
# Linux deps (evdev, sounddevice) are already unconditional; system packages
|
|
42
|
+
# (IBus, PortAudio, a C toolchain) are installed separately — see README.
|
|
43
|
+
linux = ["pystray>=0.19", "Pillow>=10.0", "piper-tts>=1.2.0"]
|
|
40
44
|
|
|
41
45
|
[project.urls]
|
|
42
46
|
Homepage = "https://github.com/Hugo0/voiceio"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -40,6 +40,10 @@ Requires-Dist: edge-tts>=6.1.0; extra == "tts-cloud"
|
|
|
40
40
|
Provides-Extra: dev
|
|
41
41
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
42
42
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
43
|
+
Provides-Extra: linux
|
|
44
|
+
Requires-Dist: pystray>=0.19; extra == "linux"
|
|
45
|
+
Requires-Dist: Pillow>=10.0; extra == "linux"
|
|
46
|
+
Requires-Dist: piper-tts>=1.2.0; extra == "linux"
|
|
43
47
|
Dynamic: license-file
|
|
44
48
|
|
|
45
49
|
# voiceio
|
|
@@ -74,8 +78,9 @@ Open-source, sovereign voice input/output. Your speech is transcribed on your ow
|
|
|
74
78
|
## Quick start
|
|
75
79
|
|
|
76
80
|
```bash
|
|
77
|
-
# 1. Install system dependencies (Ubuntu/Debian)
|
|
78
|
-
|
|
81
|
+
# 1. Install system dependencies (Ubuntu/Debian). build-essential + python3-dev
|
|
82
|
+
# are needed to compile evdev (no prebuilt wheel); portaudio for the mic.
|
|
83
|
+
sudo apt install pipx build-essential python3-dev portaudio19-dev ibus gir1.2-ibus-1.0 python3-gi
|
|
79
84
|
|
|
80
85
|
# 2. Install voiceio
|
|
81
86
|
pipx install python-voiceio
|
|
@@ -86,11 +91,14 @@ voiceio setup
|
|
|
86
91
|
|
|
87
92
|
That's it. Press **Ctrl+Alt+V** (or your chosen hotkey) to start dictating.
|
|
88
93
|
|
|
94
|
+
**Installing with an AI agent?** Point it at [INSTALL.md](INSTALL.md) — a terse,
|
|
95
|
+
copy-pasteable runbook (`voiceio setup --defaults` / `--answers '{json}'`, no TTY needed).
|
|
96
|
+
|
|
89
97
|
<details>
|
|
90
98
|
<summary><strong>Fedora</strong></summary>
|
|
91
99
|
|
|
92
100
|
```bash
|
|
93
|
-
sudo dnf install pipx
|
|
101
|
+
sudo dnf install pipx gcc gcc-c++ make python3-devel portaudio-devel ibus ibus-libs python3-gobject
|
|
94
102
|
pipx install python-voiceio
|
|
95
103
|
voiceio setup
|
|
96
104
|
```
|
|
@@ -100,8 +108,8 @@ voiceio setup
|
|
|
100
108
|
<summary><strong>Arch Linux</strong></summary>
|
|
101
109
|
|
|
102
110
|
```bash
|
|
103
|
-
sudo pacman -S python-pipx ibus python-gobject
|
|
104
|
-
#
|
|
111
|
+
sudo pacman -S python-pipx base-devel portaudio ibus python-gobject
|
|
112
|
+
# base-devel provides gcc/make; the python package ships headers.
|
|
105
113
|
pipx install python-voiceio
|
|
106
114
|
voiceio setup
|
|
107
115
|
```
|
|
@@ -17,6 +17,7 @@ tests/test_backend_probes.py
|
|
|
17
17
|
tests/test_cli.py
|
|
18
18
|
tests/test_clipboard_read.py
|
|
19
19
|
tests/test_commands.py
|
|
20
|
+
tests/test_concurrency_lockdown.py
|
|
20
21
|
tests/test_config.py
|
|
21
22
|
tests/test_correct_batch.py
|
|
22
23
|
tests/test_corrections.py
|
|
@@ -24,6 +25,7 @@ tests/test_fallback.py
|
|
|
24
25
|
tests/test_health.py
|
|
25
26
|
tests/test_hints.py
|
|
26
27
|
tests/test_history.py
|
|
28
|
+
tests/test_ibus_pending.py
|
|
27
29
|
tests/test_ibus_typer.py
|
|
28
30
|
tests/test_llm.py
|
|
29
31
|
tests/test_llm_api.py
|
|
@@ -36,12 +38,14 @@ tests/test_prompt.py
|
|
|
36
38
|
tests/test_recorder_integration.py
|
|
37
39
|
tests/test_retention.py
|
|
38
40
|
tests/test_robustness.py
|
|
41
|
+
tests/test_security_hardening.py
|
|
39
42
|
tests/test_snapshots.py
|
|
40
43
|
tests/test_streaming.py
|
|
41
44
|
tests/test_transcriber.py
|
|
42
45
|
tests/test_tts.py
|
|
43
46
|
tests/test_vad.py
|
|
44
47
|
tests/test_vocabulary.py
|
|
48
|
+
tests/test_wizard.py
|
|
45
49
|
tests/test_wordfreq.py
|
|
46
50
|
voiceio/__init__.py
|
|
47
51
|
voiceio/__main__.py
|
|
@@ -54,6 +58,7 @@ voiceio/cli.py
|
|
|
54
58
|
voiceio/clipboard_read.py
|
|
55
59
|
voiceio/commands.py
|
|
56
60
|
voiceio/config.py
|
|
61
|
+
voiceio/consent.py
|
|
57
62
|
voiceio/corrections.py
|
|
58
63
|
voiceio/demo.py
|
|
59
64
|
voiceio/feedback.py
|
|
@@ -87,6 +92,7 @@ voiceio/hotkeys/pynput_backend.py
|
|
|
87
92
|
voiceio/hotkeys/socket_backend.py
|
|
88
93
|
voiceio/ibus/__init__.py
|
|
89
94
|
voiceio/ibus/engine.py
|
|
95
|
+
voiceio/ibus/pending.py
|
|
90
96
|
voiceio/models/__init__.py
|
|
91
97
|
voiceio/models/silero_vad.onnx
|
|
92
98
|
voiceio/sounds/__init__.py
|
|
@@ -27,7 +27,10 @@ def _make_vio(mock_transcriber=None):
|
|
|
27
27
|
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
28
28
|
|
|
29
29
|
from voiceio.app import VoiceIO
|
|
30
|
-
|
|
30
|
+
cfg = Config()
|
|
31
|
+
# Tests must never touch the real system clipboard
|
|
32
|
+
cfg.output.copy_to_clipboard = "off"
|
|
33
|
+
vio = VoiceIO(cfg)
|
|
31
34
|
mock_stream = MagicMock()
|
|
32
35
|
mock_stream.active = True
|
|
33
36
|
mock_stream.closed = False
|
|
@@ -209,3 +212,51 @@ class TestStateMachine:
|
|
|
209
212
|
vio._request_stop()
|
|
210
213
|
assert vio._state == _State.IDLE
|
|
211
214
|
assert not vio.recorder.is_recording
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestClipboardMirror:
|
|
218
|
+
"""copy_to_clipboard: interim/final text mirrored to the clipboard."""
|
|
219
|
+
|
|
220
|
+
def test_interim_copy_skipped_for_clipboard_typer(self):
|
|
221
|
+
vio, mock_typer, _ = _make_vio()
|
|
222
|
+
mock_typer.name = "clipboard"
|
|
223
|
+
vio.cfg.output.copy_to_clipboard = "live"
|
|
224
|
+
with patch("voiceio.clipboard_read.copy_text") as mock_copy:
|
|
225
|
+
vio._on_interim_text("hello")
|
|
226
|
+
mock_copy.assert_not_called()
|
|
227
|
+
|
|
228
|
+
def test_interim_copy_fires_for_other_typers(self):
|
|
229
|
+
vio, mock_typer, _ = _make_vio()
|
|
230
|
+
mock_typer.name = "ibus"
|
|
231
|
+
vio.cfg.output.copy_to_clipboard = "live"
|
|
232
|
+
with patch("voiceio.clipboard_read.copy_text") as mock_copy:
|
|
233
|
+
vio._on_interim_text("hello")
|
|
234
|
+
mock_copy.assert_called_once_with("hello")
|
|
235
|
+
|
|
236
|
+
def test_interim_copy_off_unless_live(self):
|
|
237
|
+
vio, mock_typer, _ = _make_vio()
|
|
238
|
+
mock_typer.name = "ibus"
|
|
239
|
+
vio.cfg.output.copy_to_clipboard = "final"
|
|
240
|
+
with patch("voiceio.clipboard_read.copy_text") as mock_copy:
|
|
241
|
+
vio._on_interim_text("hello")
|
|
242
|
+
mock_copy.assert_not_called()
|
|
243
|
+
|
|
244
|
+
def test_final_copy_respects_off(self):
|
|
245
|
+
vio, mock_typer, _ = _make_vio()
|
|
246
|
+
mock_typer.name = "ibus"
|
|
247
|
+
vio.cfg.output.copy_to_clipboard = "off"
|
|
248
|
+
with patch("voiceio.clipboard_read.copy_text") as mock_copy:
|
|
249
|
+
vio._copy_result_async("hello")
|
|
250
|
+
time.sleep(0.1)
|
|
251
|
+
mock_copy.assert_not_called()
|
|
252
|
+
|
|
253
|
+
def test_final_copy_runs_async(self):
|
|
254
|
+
vio, mock_typer, _ = _make_vio()
|
|
255
|
+
mock_typer.name = "ibus"
|
|
256
|
+
vio.cfg.output.copy_to_clipboard = "final"
|
|
257
|
+
done = threading.Event()
|
|
258
|
+
with patch("voiceio.clipboard_read.copy_text",
|
|
259
|
+
side_effect=lambda t: done.set()) as mock_copy:
|
|
260
|
+
vio._copy_result_async("hello world")
|
|
261
|
+
assert done.wait(timeout=2)
|
|
262
|
+
mock_copy.assert_called_once_with("hello world")
|
|
@@ -89,3 +89,57 @@ def test_read_text_no_tools():
|
|
|
89
89
|
from voiceio.clipboard_read import read_text
|
|
90
90
|
result = read_text()
|
|
91
91
|
assert result is None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# --- copy_text ---
|
|
95
|
+
|
|
96
|
+
def test_copy_text_wayland():
|
|
97
|
+
"""Wayland copies via wl-copy with text on stdin."""
|
|
98
|
+
with patch("voiceio.clipboard_read.detect", return_value=_mock_platform(display_server="wayland")), \
|
|
99
|
+
patch("shutil.which", return_value="/usr/bin/wl-copy"), \
|
|
100
|
+
patch("subprocess.run") as mock_run:
|
|
101
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
102
|
+
from voiceio.clipboard_read import copy_text
|
|
103
|
+
assert copy_text("hello") is True
|
|
104
|
+
args = mock_run.call_args[0][0]
|
|
105
|
+
assert "wl-copy" in args
|
|
106
|
+
assert mock_run.call_args.kwargs["input"] == b"hello"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_copy_text_x11_xclip():
|
|
110
|
+
"""X11 copies to the CLIPBOARD selection via xclip."""
|
|
111
|
+
with patch("voiceio.clipboard_read.detect", return_value=_mock_platform(display_server="x11")), \
|
|
112
|
+
patch("shutil.which", lambda t: "/usr/bin/xclip" if t == "xclip" else None), \
|
|
113
|
+
patch("subprocess.run") as mock_run:
|
|
114
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
115
|
+
from voiceio.clipboard_read import copy_text
|
|
116
|
+
assert copy_text("hello") is True
|
|
117
|
+
args = mock_run.call_args[0][0]
|
|
118
|
+
assert args[:1] == ["xclip"]
|
|
119
|
+
assert "clipboard" in args
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_copy_text_no_tools():
|
|
123
|
+
"""Returns False (no raise) when no clipboard tools available."""
|
|
124
|
+
with patch("voiceio.clipboard_read.detect", return_value=_mock_platform(display_server="x11")), \
|
|
125
|
+
patch("shutil.which", return_value=None):
|
|
126
|
+
from voiceio.clipboard_read import copy_text
|
|
127
|
+
assert copy_text("hello") is False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_copy_text_empty():
|
|
131
|
+
"""Empty text is never copied."""
|
|
132
|
+
with patch("subprocess.run") as mock_run:
|
|
133
|
+
from voiceio.clipboard_read import copy_text
|
|
134
|
+
assert copy_text("") is False
|
|
135
|
+
mock_run.assert_not_called()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_copy_text_command_failure():
|
|
139
|
+
"""Nonzero exit from the copy tool reports False."""
|
|
140
|
+
with patch("voiceio.clipboard_read.detect", return_value=_mock_platform(display_server="wayland")), \
|
|
141
|
+
patch("shutil.which", return_value="/usr/bin/wl-copy"), \
|
|
142
|
+
patch("subprocess.run") as mock_run:
|
|
143
|
+
mock_run.return_value = MagicMock(returncode=1)
|
|
144
|
+
from voiceio.clipboard_read import copy_text
|
|
145
|
+
assert copy_text("hello") is False
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Regression tests for the concurrency lockdown + IBus good-citizenship work.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
#1 IBus input-source is never re-forced while IDLE; mid-record switch-away
|
|
5
|
+
falls back to clipboard instead of fighting the user.
|
|
6
|
+
#2 IBus deactivation is generation-checked and restores the user's source.
|
|
7
|
+
#3 Watchdog typer/platform mutation goes through lock-guarded, IDLE-gated
|
|
8
|
+
swap helpers.
|
|
9
|
+
#8 SIGTERM runs the same shutdown path as Ctrl-C.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
from voiceio.config import Config
|
|
17
|
+
from voiceio.typers.base import TyperBackend
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_app(typer_name="clipboard"):
|
|
21
|
+
"""VoiceIO with mocked backends; typer.name configurable."""
|
|
22
|
+
mock_hotkey = MagicMock()
|
|
23
|
+
mock_hotkey.name = "socket"
|
|
24
|
+
mock_typer = MagicMock(spec=TyperBackend)
|
|
25
|
+
mock_typer.name = typer_name
|
|
26
|
+
|
|
27
|
+
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
28
|
+
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
29
|
+
patch("voiceio.app.Transcriber", return_value=MagicMock()), \
|
|
30
|
+
patch("voiceio.app.plat.detect") as mock_detect:
|
|
31
|
+
mock_detect.return_value = MagicMock(
|
|
32
|
+
display_server="wayland", desktop="gnome", is_gnome=True,
|
|
33
|
+
)
|
|
34
|
+
from voiceio.app import VoiceIO
|
|
35
|
+
vio = VoiceIO(Config())
|
|
36
|
+
mock_stream = MagicMock()
|
|
37
|
+
mock_stream.active = True
|
|
38
|
+
mock_stream.closed = False
|
|
39
|
+
mock_stream.stopped = False
|
|
40
|
+
vio.recorder._stream = mock_stream
|
|
41
|
+
vio.recorder._last_callback_time = time.monotonic()
|
|
42
|
+
return vio
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Fix #3: lock-guarded, IDLE-gated swaps ──────────────────────────────
|
|
46
|
+
|
|
47
|
+
class TestGuardedSwaps:
|
|
48
|
+
def test_swap_typer_only_when_idle(self):
|
|
49
|
+
from voiceio.app import _State
|
|
50
|
+
vio = _make_app()
|
|
51
|
+
new = MagicMock(spec=TyperBackend)
|
|
52
|
+
new.name = "ydotool"
|
|
53
|
+
|
|
54
|
+
vio._state = _State.RECORDING
|
|
55
|
+
assert vio._swap_typer(new, "test") is False
|
|
56
|
+
assert vio._typer is not new # not swapped mid-recording
|
|
57
|
+
|
|
58
|
+
vio._state = _State.IDLE
|
|
59
|
+
assert vio._swap_typer(new, "test") is True
|
|
60
|
+
assert vio._typer is new
|
|
61
|
+
|
|
62
|
+
def test_swap_platform_only_when_idle(self):
|
|
63
|
+
from voiceio.app import _State
|
|
64
|
+
vio = _make_app()
|
|
65
|
+
original = vio.platform
|
|
66
|
+
new_platform = MagicMock(desktop="kde")
|
|
67
|
+
|
|
68
|
+
vio._state = _State.FINALIZING
|
|
69
|
+
assert vio._swap_platform(new_platform) is False
|
|
70
|
+
assert vio.platform is original
|
|
71
|
+
|
|
72
|
+
vio._state = _State.IDLE
|
|
73
|
+
assert vio._swap_platform(new_platform) is True
|
|
74
|
+
assert vio.platform is new_platform
|
|
75
|
+
|
|
76
|
+
def test_health_upkeep_skipped_when_not_idle(self):
|
|
77
|
+
"""_check_health must not run typer upkeep during a live recording."""
|
|
78
|
+
from voiceio.app import _State
|
|
79
|
+
vio = _make_app()
|
|
80
|
+
vio._state = _State.RECORDING
|
|
81
|
+
vio.transcriber.is_worker_alive.return_value = True
|
|
82
|
+
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
83
|
+
vio.cfg.tray.enabled = False
|
|
84
|
+
vio._health_typer_upkeep = MagicMock()
|
|
85
|
+
vio._check_health()
|
|
86
|
+
vio._health_typer_upkeep.assert_not_called()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Fix #2: generation-checked deactivation + source restore ─────────────
|
|
90
|
+
|
|
91
|
+
class TestIBusDeactivation:
|
|
92
|
+
def test_deactivate_skipped_when_superseded(self):
|
|
93
|
+
vio = _make_app(typer_name="ibus")
|
|
94
|
+
vio._generation = 6
|
|
95
|
+
with patch("voiceio.app.threading.Thread") as MockThread:
|
|
96
|
+
vio._deactivate_ibus(gen=5) # a newer recording already started
|
|
97
|
+
MockThread.assert_not_called()
|
|
98
|
+
|
|
99
|
+
def test_deactivate_runs_when_current(self):
|
|
100
|
+
vio = _make_app(typer_name="ibus")
|
|
101
|
+
vio._generation = 6
|
|
102
|
+
with patch("voiceio.app.threading.Thread") as MockThread:
|
|
103
|
+
vio._deactivate_ibus(gen=6)
|
|
104
|
+
MockThread.assert_called_once()
|
|
105
|
+
|
|
106
|
+
def test_deactivate_noop_for_non_ibus(self):
|
|
107
|
+
vio = _make_app(typer_name="clipboard")
|
|
108
|
+
with patch("voiceio.app.threading.Thread") as MockThread:
|
|
109
|
+
vio._deactivate_ibus(gen=1)
|
|
110
|
+
MockThread.assert_not_called()
|
|
111
|
+
|
|
112
|
+
def test_restore_uses_recorded_prev_index(self):
|
|
113
|
+
vio = _make_app(typer_name="ibus")
|
|
114
|
+
vio._prev_input_source_index = 2
|
|
115
|
+
vio._voiceio_source_index = 4
|
|
116
|
+
calls = []
|
|
117
|
+
vio._set_gnome_input_source_index = lambda i: calls.append(i)
|
|
118
|
+
vio._restore_input_source()
|
|
119
|
+
assert calls == [2] # restores the user's source, not hardcoded 0
|
|
120
|
+
assert vio._prev_input_source_index is None
|
|
121
|
+
assert vio._voiceio_source_index is None
|
|
122
|
+
|
|
123
|
+
def test_restore_defaults_to_zero_when_unknown(self):
|
|
124
|
+
vio = _make_app(typer_name="ibus")
|
|
125
|
+
vio._prev_input_source_index = None
|
|
126
|
+
calls = []
|
|
127
|
+
vio._set_gnome_input_source_index = lambda i: calls.append(i)
|
|
128
|
+
vio._restore_input_source()
|
|
129
|
+
assert calls == [0]
|
|
130
|
+
|
|
131
|
+
def test_do_stop_does_not_deactivate_synchronously(self):
|
|
132
|
+
"""Fix #2: deactivation is deferred to after the final commit, not run
|
|
133
|
+
inside _do_stop (which would race the finalizer's commit)."""
|
|
134
|
+
from voiceio.app import _State
|
|
135
|
+
vio = _make_app(typer_name="ibus")
|
|
136
|
+
vio._deactivate_ibus = MagicMock()
|
|
137
|
+
vio._state = _State.RECORDING
|
|
138
|
+
vio._record_start = time.monotonic() - 2
|
|
139
|
+
# Give the recorder something to return so a finalizer is spawned.
|
|
140
|
+
vio.recorder.stop = MagicMock(return_value=None)
|
|
141
|
+
vio._session = MagicMock()
|
|
142
|
+
with patch("voiceio.app.threading.Thread"):
|
|
143
|
+
vio._do_stop()
|
|
144
|
+
# For the streaming path the finalizer owns deactivation, so _do_stop
|
|
145
|
+
# itself must not have called it.
|
|
146
|
+
vio._deactivate_ibus.assert_not_called()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Fix #1: no re-force while IDLE; mid-record fallback ──────────────────
|
|
150
|
+
|
|
151
|
+
class TestIBusGoodCitizen:
|
|
152
|
+
def _idle_healthy_ibus(self):
|
|
153
|
+
from voiceio.app import _State
|
|
154
|
+
vio = _make_app(typer_name="ibus")
|
|
155
|
+
vio._state = _State.IDLE
|
|
156
|
+
vio.transcriber.is_worker_alive.return_value = True
|
|
157
|
+
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
158
|
+
vio.cfg.tray.enabled = False
|
|
159
|
+
vio._health_typer_upkeep = MagicMock()
|
|
160
|
+
vio._engine_proc = MagicMock()
|
|
161
|
+
vio._engine_proc.poll.return_value = None # alive
|
|
162
|
+
vio._ping_ibus_engine = MagicMock(return_value=True) # responsive
|
|
163
|
+
return vio
|
|
164
|
+
|
|
165
|
+
def test_idle_never_touches_input_source(self):
|
|
166
|
+
vio = self._idle_healthy_ibus()
|
|
167
|
+
vio._set_gnome_input_source_index = MagicMock()
|
|
168
|
+
vio._switch_gnome_input_source = MagicMock()
|
|
169
|
+
vio._detect_input_source_hijack = MagicMock()
|
|
170
|
+
vio._check_health()
|
|
171
|
+
# A responsive engine while IDLE => no source re-forcing at all.
|
|
172
|
+
vio._set_gnome_input_source_index.assert_not_called()
|
|
173
|
+
vio._switch_gnome_input_source.assert_not_called()
|
|
174
|
+
vio._detect_input_source_hijack.assert_not_called()
|
|
175
|
+
|
|
176
|
+
def test_recording_watches_for_hijack(self):
|
|
177
|
+
from voiceio.app import _State
|
|
178
|
+
vio = self._idle_healthy_ibus()
|
|
179
|
+
vio._state = _State.RECORDING
|
|
180
|
+
vio._detect_input_source_hijack = MagicMock()
|
|
181
|
+
vio._check_health()
|
|
182
|
+
vio._detect_input_source_hijack.assert_called_once()
|
|
183
|
+
|
|
184
|
+
def test_hijack_falls_back_to_clipboard(self):
|
|
185
|
+
vio = _make_app(typer_name="ibus")
|
|
186
|
+
vio._voiceio_source_index = 3
|
|
187
|
+
vio._get_current_input_source_index = lambda: 0 # user switched away
|
|
188
|
+
session = MagicMock()
|
|
189
|
+
vio._session = session
|
|
190
|
+
with patch("voiceio.typers.clipboard.ClipboardTyper", return_value=MagicMock()):
|
|
191
|
+
vio._detect_input_source_hijack()
|
|
192
|
+
assert vio._ibus_session_fallback is True
|
|
193
|
+
session.set_typer.assert_called_once()
|
|
194
|
+
|
|
195
|
+
def test_hijack_is_idempotent(self):
|
|
196
|
+
vio = _make_app(typer_name="ibus")
|
|
197
|
+
vio._voiceio_source_index = 3
|
|
198
|
+
vio._get_current_input_source_index = lambda: 0
|
|
199
|
+
session = MagicMock()
|
|
200
|
+
vio._session = session
|
|
201
|
+
with patch("voiceio.typers.clipboard.ClipboardTyper", return_value=MagicMock()):
|
|
202
|
+
vio._detect_input_source_hijack()
|
|
203
|
+
session.set_typer.reset_mock()
|
|
204
|
+
vio._detect_input_source_hijack() # already fell back
|
|
205
|
+
session.set_typer.assert_not_called()
|
|
206
|
+
|
|
207
|
+
def test_no_hijack_when_still_on_voiceio(self):
|
|
208
|
+
vio = _make_app(typer_name="ibus")
|
|
209
|
+
vio._voiceio_source_index = 3
|
|
210
|
+
vio._get_current_input_source_index = lambda: 3 # still on voiceio
|
|
211
|
+
session = MagicMock()
|
|
212
|
+
vio._session = session
|
|
213
|
+
vio._detect_input_source_hijack()
|
|
214
|
+
assert vio._ibus_session_fallback is False
|
|
215
|
+
session.set_typer.assert_not_called()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ── Fix #8: SIGTERM shutdown wiring ─────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
class TestSigterm:
|
|
221
|
+
def test_run_registers_sigterm(self, monkeypatch, tmp_path):
|
|
222
|
+
import signal as _signal
|
|
223
|
+
vio = _make_app(typer_name="clipboard")
|
|
224
|
+
|
|
225
|
+
import voiceio.config as vcfg
|
|
226
|
+
monkeypatch.setattr(vcfg, "PID_PATH", tmp_path / "pid")
|
|
227
|
+
monkeypatch.setattr(vcfg, "LOG_DIR", tmp_path / "logs")
|
|
228
|
+
monkeypatch.setattr("voiceio.pidlock.lock_pid_file", lambda fd: None)
|
|
229
|
+
# Neutralize anything that could touch the real session/system.
|
|
230
|
+
monkeypatch.setattr("voiceio.app._import_graphical_env", lambda: None)
|
|
231
|
+
monkeypatch.setattr("voiceio.app._redetect_platform", lambda: vio.platform)
|
|
232
|
+
vio._stop_ibus_engine = MagicMock()
|
|
233
|
+
vio._set_gnome_input_source_index = MagicMock()
|
|
234
|
+
vio._try_upgrade_typer = MagicMock()
|
|
235
|
+
vio.cfg.tray.enabled = False
|
|
236
|
+
vio.cfg.feedback.sound_enabled = False
|
|
237
|
+
vio._hotkey = MagicMock()
|
|
238
|
+
vio._socket = None
|
|
239
|
+
vio._tts_hotkey = None
|
|
240
|
+
|
|
241
|
+
handlers = {}
|
|
242
|
+
monkeypatch.setattr(
|
|
243
|
+
"voiceio.app.signal.signal",
|
|
244
|
+
lambda sig, handler: handlers.__setitem__(sig, handler),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
vio._shutdown.set() # make run()'s wait return immediately
|
|
248
|
+
vio.run()
|
|
249
|
+
|
|
250
|
+
assert _signal.SIGTERM in handlers
|
|
251
|
+
# Invoking the handler must trigger the shared shutdown path.
|
|
252
|
+
vio._shutdown.clear()
|
|
253
|
+
handlers[_signal.SIGTERM]()
|
|
254
|
+
assert vio._shutdown.is_set()
|
|
@@ -61,3 +61,16 @@ def test_migrate_x11_to_pynput():
|
|
|
61
61
|
raw = {"hotkey": {"backend": "x11"}}
|
|
62
62
|
result = _migrate_v1(raw)
|
|
63
63
|
assert result["hotkey"]["backend"] == "pynput"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_copy_to_clipboard_default_and_override(tmp_path):
|
|
67
|
+
cfg = load(path=tmp_path / "nonexistent.toml")
|
|
68
|
+
assert cfg.output.copy_to_clipboard == "final"
|
|
69
|
+
|
|
70
|
+
config_file = tmp_path / "config.toml"
|
|
71
|
+
config_file.write_text(textwrap.dedent("""\
|
|
72
|
+
[output]
|
|
73
|
+
copy_to_clipboard = "live"
|
|
74
|
+
"""))
|
|
75
|
+
cfg = load(path=config_file)
|
|
76
|
+
assert cfg.output.copy_to_clipboard == "live"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Tests for the IBus engine's pre-ready command buffer (fix #7).
|
|
2
|
+
|
|
3
|
+
The buffer must never replay commands that went stale before the engine
|
|
4
|
+
instance existed, and must be cleared outright when a fresh engine is created.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from voiceio.ibus.pending import PendingBuffer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_fresh_commands_are_replayed_in_order():
|
|
12
|
+
buf = PendingBuffer(max_age=3.0)
|
|
13
|
+
buf.add("preedit:a", now=100.0)
|
|
14
|
+
buf.add("commit:b", now=100.5)
|
|
15
|
+
assert buf.drain_fresh(now=101.0) == ["preedit:a", "commit:b"]
|
|
16
|
+
assert len(buf) == 0 # draining empties the buffer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_stale_commands_are_dropped():
|
|
20
|
+
buf = PendingBuffer(max_age=3.0)
|
|
21
|
+
buf.add("commit:old", now=100.0) # 5s before drain -> stale
|
|
22
|
+
buf.add("commit:new", now=104.0) # 1s before drain -> fresh
|
|
23
|
+
assert buf.drain_fresh(now=105.0) == ["commit:new"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_all_stale_yields_nothing():
|
|
27
|
+
buf = PendingBuffer(max_age=3.0)
|
|
28
|
+
buf.add("commit:x", now=0.0)
|
|
29
|
+
assert buf.drain_fresh(now=100.0) == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_clear_empties_buffer():
|
|
33
|
+
buf = PendingBuffer()
|
|
34
|
+
buf.add("preedit:z")
|
|
35
|
+
assert len(buf) == 1
|
|
36
|
+
buf.clear()
|
|
37
|
+
assert len(buf) == 0
|
|
38
|
+
assert buf.drain_fresh() == []
|