python-voiceio 0.4.0__tar.gz → 0.4.1__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.
Files changed (126) hide show
  1. {python_voiceio-0.4.0/python_voiceio.egg-info → python_voiceio-0.4.1}/PKG-INFO +14 -6
  2. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/README.md +9 -5
  3. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/pyproject.toml +5 -1
  4. {python_voiceio-0.4.0 → python_voiceio-0.4.1/python_voiceio.egg-info}/PKG-INFO +14 -6
  5. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/SOURCES.txt +6 -0
  6. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/requires.txt +5 -0
  7. python_voiceio-0.4.1/tests/test_concurrency_lockdown.py +254 -0
  8. python_voiceio-0.4.1/tests/test_ibus_pending.py +38 -0
  9. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_ibus_typer.py +57 -0
  10. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_prebuffer.py +46 -0
  11. python_voiceio-0.4.1/tests/test_security_hardening.py +238 -0
  12. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_streaming.py +116 -0
  13. python_voiceio-0.4.1/tests/test_transcriber.py +127 -0
  14. python_voiceio-0.4.1/tests/test_wizard.py +228 -0
  15. python_voiceio-0.4.1/voiceio/__init__.py +1 -0
  16. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/app.py +226 -79
  17. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/cli.py +148 -30
  18. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/config.py +123 -0
  19. python_voiceio-0.4.1/voiceio/consent.py +57 -0
  20. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/demo.py +2 -2
  21. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/health.py +13 -4
  22. python_voiceio-0.4.1/voiceio/history.py +181 -0
  23. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/ibus/engine.py +18 -8
  24. python_voiceio-0.4.1/voiceio/ibus/pending.py +43 -0
  25. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/llm_api.py +37 -0
  26. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/platform.py +121 -0
  27. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/postcorrect.py +7 -0
  28. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/recorder.py +15 -12
  29. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/retention.py +13 -1
  30. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/streaming.py +77 -4
  31. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/transcriber.py +38 -4
  32. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/ibus.py +49 -8
  33. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/ydotool.py +40 -2
  34. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/wizard.py +433 -80
  35. python_voiceio-0.4.0/tests/test_transcriber.py +0 -69
  36. python_voiceio-0.4.0/voiceio/__init__.py +0 -1
  37. python_voiceio-0.4.0/voiceio/history.py +0 -88
  38. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/LICENSE +0 -0
  39. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
  40. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/entry_points.txt +0 -0
  41. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/top_level.txt +0 -0
  42. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/setup.cfg +0 -0
  43. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_adjudicate.py +0 -0
  44. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_app_wiring.py +0 -0
  45. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_audio_quality.py +0 -0
  46. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_audit.py +0 -0
  47. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_autocorrect.py +0 -0
  48. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_autocorrect_state.py +0 -0
  49. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_backend_probes.py +0 -0
  50. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_cli.py +0 -0
  51. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_clipboard_read.py +0 -0
  52. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_commands.py +0 -0
  53. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_config.py +0 -0
  54. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_correct_batch.py +0 -0
  55. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_corrections.py +0 -0
  56. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_fallback.py +0 -0
  57. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_health.py +0 -0
  58. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_hints.py +0 -0
  59. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_history.py +0 -0
  60. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_llm.py +0 -0
  61. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_llm_api.py +0 -0
  62. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_numbers.py +0 -0
  63. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_platform.py +0 -0
  64. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_postcorrect.py +0 -0
  65. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_postprocess.py +0 -0
  66. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_prompt.py +0 -0
  67. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_recorder_integration.py +0 -0
  68. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_retention.py +0 -0
  69. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_robustness.py +0 -0
  70. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_snapshots.py +0 -0
  71. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_tts.py +0 -0
  72. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_vad.py +0 -0
  73. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_vocabulary.py +0 -0
  74. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_wordfreq.py +0 -0
  75. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/__main__.py +0 -0
  76. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/audit.py +0 -0
  77. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/autocorrect.py +0 -0
  78. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/autocorrect_state.py +0 -0
  79. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/backends.py +0 -0
  80. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/clipboard_read.py +0 -0
  81. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/commands.py +0 -0
  82. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/corrections.py +0 -0
  83. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/feedback.py +0 -0
  84. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hints.py +0 -0
  85. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/__init__.py +0 -0
  86. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/base.py +0 -0
  87. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/chain.py +0 -0
  88. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/evdev.py +0 -0
  89. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/pynput_backend.py +0 -0
  90. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/socket_backend.py +0 -0
  91. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/ibus/__init__.py +0 -0
  92. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/llm.py +0 -0
  93. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/models/__init__.py +0 -0
  94. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/models/silero_vad.onnx +0 -0
  95. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/numbers.py +0 -0
  96. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/pidlock.py +0 -0
  97. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/postprocess.py +0 -0
  98. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/prompt.py +0 -0
  99. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/service.py +0 -0
  100. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/snapshots.py +0 -0
  101. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/__init__.py +0 -0
  102. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/commit.wav +0 -0
  103. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/start.wav +0 -0
  104. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/stop.wav +0 -0
  105. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/__init__.py +0 -0
  106. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_icons.py +0 -0
  107. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_indicator.py +0 -0
  108. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_pystray.py +0 -0
  109. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/__init__.py +0 -0
  110. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/base.py +0 -0
  111. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/chain.py +0 -0
  112. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/edge_engine.py +0 -0
  113. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/espeak.py +0 -0
  114. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/piper_engine.py +0 -0
  115. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/player.py +0 -0
  116. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/__init__.py +0 -0
  117. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/base.py +0 -0
  118. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/chain.py +0 -0
  119. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/clipboard.py +0 -0
  120. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/pynput_type.py +0 -0
  121. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/wtype.py +0 -0
  122. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/xdotool.py +0 -0
  123. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/vad.py +0 -0
  124. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/vocabulary.py +0 -0
  125. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/wordfreq.py +0 -0
  126. {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
- sudo apt install pipx ibus gir1.2-ibus-1.0 python3-gi python3-dev portaudio19-dev
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 ibus python3-gobject python3-devel portaudio-devel
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 portaudio
104
- # Note: Arch includes Python headers by default with the python package
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
- sudo apt install pipx ibus gir1.2-ibus-1.0 python3-gi python3-dev portaudio19-dev
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 ibus python3-gobject python3-devel portaudio-devel
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 portaudio
60
- # Note: Arch includes Python headers by default with the python package
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.4.0"
7
+ version = "0.4.1"
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.4.0
3
+ Version: 0.4.1
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
- sudo apt install pipx ibus gir1.2-ibus-1.0 python3-gi python3-dev portaudio19-dev
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 ibus python3-gobject python3-devel portaudio-devel
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 portaudio
104
- # Note: Arch includes Python headers by default with the python package
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
@@ -16,6 +16,11 @@ win11toast>=0.36
16
16
  pytest>=7.0
17
17
  pytest-mock
18
18
 
19
+ [linux]
20
+ pystray>=0.19
21
+ Pillow>=10.0
22
+ piper-tts>=1.2.0
23
+
19
24
  [tray]
20
25
  pystray>=0.19
21
26
  Pillow>=10.0
@@ -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()
@@ -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() == []
@@ -149,6 +149,63 @@ class TestIBusTyper:
149
149
  assert result.ok
150
150
 
151
151
 
152
+ class TestProbeReadOnly:
153
+ """Fix #6: probe() must be side-effect-free (no install / IBus restart)
154
+ and must cache a successful result to avoid subprocess storms."""
155
+
156
+ def test_missing_component_does_not_install(self):
157
+ with patch("voiceio.typers.ibus.shutil.which", return_value="/usr/bin/ibus"), \
158
+ patch("voiceio.typers.ibus._has_ibus_gi", return_value=True), \
159
+ patch("voiceio.typers.ibus._ibus_daemon_running", return_value=True), \
160
+ patch("voiceio.typers.ibus._component_installed", return_value=False), \
161
+ patch("voiceio.typers.ibus.install_component") as mock_install:
162
+ typer = IBusTyper()
163
+ result = typer.probe()
164
+ assert not result.ok
165
+ assert "not installed" in result.reason
166
+ mock_install.assert_not_called() # read-only!
167
+
168
+ def test_ok_result_is_cached(self):
169
+ with patch("voiceio.typers.ibus.shutil.which", return_value="/usr/bin/ibus"), \
170
+ patch("voiceio.typers.ibus._has_ibus_gi", return_value=True), \
171
+ patch("voiceio.typers.ibus._ibus_daemon_running", return_value=True) as mock_daemon, \
172
+ patch("voiceio.typers.ibus._component_installed", return_value=True), \
173
+ patch("voiceio.typers.ibus._gnome_source_configured", return_value=True):
174
+ typer = IBusTyper()
175
+ assert typer.probe().ok
176
+ assert typer.probe().ok
177
+ # Second call served from cache — no repeated subprocess probing.
178
+ assert mock_daemon.call_count == 1
179
+
180
+ def test_failure_is_not_cached(self):
181
+ with patch("voiceio.typers.ibus.shutil.which", return_value="/usr/bin/ibus"), \
182
+ patch("voiceio.typers.ibus._has_ibus_gi", return_value=True), \
183
+ patch("voiceio.typers.ibus._ibus_daemon_running", return_value=False) as mock_daemon:
184
+ typer = IBusTyper()
185
+ assert not typer.probe().ok
186
+ assert not typer.probe().ok
187
+ # Failures re-check every time (recover immediately when fixed).
188
+ assert mock_daemon.call_count == 2
189
+
190
+ def test_ensure_installed_installs_when_missing(self):
191
+ with patch("voiceio.typers.ibus._component_installed", return_value=False), \
192
+ patch("voiceio.typers.ibus.install_component", return_value=True) as mock_install, \
193
+ patch("voiceio.typers.ibus._ensure_gnome_input_source") as mock_src:
194
+ typer = IBusTyper()
195
+ assert typer.ensure_installed() is True
196
+ mock_install.assert_called_once()
197
+ mock_src.assert_called_once()
198
+
199
+ def test_ensure_installed_skips_install_when_present(self):
200
+ with patch("voiceio.typers.ibus._component_installed", return_value=True), \
201
+ patch("voiceio.typers.ibus.install_component") as mock_install, \
202
+ patch("voiceio.typers.ibus._ensure_gnome_input_source") as mock_src:
203
+ typer = IBusTyper()
204
+ assert typer.ensure_installed() is True
205
+ mock_install.assert_not_called()
206
+ mock_src.assert_called_once()
207
+
208
+
152
209
  class TestStreamingTyperProtocol:
153
210
  def test_ibus_is_streaming_typer(self):
154
211
  from voiceio.typers.base import StreamingTyper
@@ -103,3 +103,49 @@ class TestAudioRecorderPrebuffer:
103
103
  assert audio.ndim == 1
104
104
  # Should contain prebuffer + callback data
105
105
  assert len(audio) > 1024
106
+
107
+
108
+ class TestNoCrossGenerationTruncation:
109
+ """Fix #5: mark_transcribed is gone; stop() returns the FULL recording and
110
+ the streaming pause gate uses a session-local high-water mark."""
111
+
112
+ def _recorder(self):
113
+ cfg = AudioConfig(sample_rate=16000, prebuffer_secs=0.0, silence_duration=0.1)
114
+ rec = AudioRecorder(cfg)
115
+ rec._stream = MagicMock()
116
+ return rec
117
+
118
+ def test_mark_transcribed_removed(self):
119
+ rec = self._recorder()
120
+ assert not hasattr(rec, "mark_transcribed")
121
+ assert not hasattr(rec, "_last_transcribed_len")
122
+
123
+ def test_stop_returns_full_audio(self):
124
+ rec = self._recorder()
125
+ rec.start()
126
+ # 1s of audio in 4 chunks
127
+ for _ in range(4):
128
+ rec._callback(np.ones((4000, 1), dtype=np.float32) * 0.3, 4000, None, None)
129
+ audio = rec.stop()
130
+ assert audio is not None
131
+ assert len(audio) == 16000 # nothing truncated
132
+
133
+ def test_pause_high_water_resets_each_recording(self):
134
+ rec = self._recorder()
135
+ fired = []
136
+ rec.set_on_speech_pause(lambda: fired.append(1))
137
+
138
+ # First recording: speech then silence -> fires once.
139
+ rec.start()
140
+ assert rec._pause_fired_at == 0
141
+ with patch.object(rec._vad, "is_speech", return_value=True):
142
+ rec._callback(np.ones((16000, 1), dtype=np.float32) * 0.3, 16000, None, None)
143
+ with patch.object(rec._vad, "is_speech", return_value=False):
144
+ rec._callback(np.zeros((3200, 1), dtype=np.float32), 3200, None, None)
145
+ assert len(fired) == 1
146
+ assert rec._pause_fired_at > 0
147
+ rec.stop()
148
+
149
+ # New recording must reset the high-water mark (no leak across gens).
150
+ rec.start()
151
+ assert rec._pause_fired_at == 0