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.
- {python_voiceio-0.4.0/python_voiceio.egg-info → python_voiceio-0.4.1}/PKG-INFO +14 -6
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/README.md +9 -5
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/pyproject.toml +5 -1
- {python_voiceio-0.4.0 → python_voiceio-0.4.1/python_voiceio.egg-info}/PKG-INFO +14 -6
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/SOURCES.txt +6 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/requires.txt +5 -0
- python_voiceio-0.4.1/tests/test_concurrency_lockdown.py +254 -0
- python_voiceio-0.4.1/tests/test_ibus_pending.py +38 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_ibus_typer.py +57 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_prebuffer.py +46 -0
- python_voiceio-0.4.1/tests/test_security_hardening.py +238 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_streaming.py +116 -0
- python_voiceio-0.4.1/tests/test_transcriber.py +127 -0
- python_voiceio-0.4.1/tests/test_wizard.py +228 -0
- python_voiceio-0.4.1/voiceio/__init__.py +1 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/app.py +226 -79
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/cli.py +148 -30
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/config.py +123 -0
- python_voiceio-0.4.1/voiceio/consent.py +57 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/demo.py +2 -2
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/health.py +13 -4
- python_voiceio-0.4.1/voiceio/history.py +181 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/ibus/engine.py +18 -8
- python_voiceio-0.4.1/voiceio/ibus/pending.py +43 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/llm_api.py +37 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/platform.py +121 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/postcorrect.py +7 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/recorder.py +15 -12
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/retention.py +13 -1
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/streaming.py +77 -4
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/transcriber.py +38 -4
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/ibus.py +49 -8
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/ydotool.py +40 -2
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/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.4.1}/LICENSE +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/setup.cfg +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_adjudicate.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_app_wiring.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_audio_quality.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_audit.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_autocorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_autocorrect_state.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_cli.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_commands.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_config.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_correct_batch.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_corrections.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_fallback.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_health.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_hints.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_history.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_llm.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_llm_api.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_numbers.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_platform.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_postcorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_postprocess.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_prompt.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_retention.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_robustness.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_snapshots.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_tts.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_vad.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_vocabulary.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/tests/test_wordfreq.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/__main__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/audit.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/autocorrect.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/autocorrect_state.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/backends.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/commands.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/corrections.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/feedback.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hints.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/llm.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/models/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/numbers.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/pidlock.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/postprocess.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/prompt.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/service.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/snapshots.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/tts/player.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/vad.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/vocabulary.py +0 -0
- {python_voiceio-0.4.0 → python_voiceio-0.4.1}/voiceio/wordfreq.py +0 -0
- {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.
|
|
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
|
-
|
|
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.4.
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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
|