vinowhisper 0.3.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.
- vinowhisper-0.3.0/LICENSE +21 -0
- vinowhisper-0.3.0/PKG-INFO +160 -0
- vinowhisper-0.3.0/README.md +125 -0
- vinowhisper-0.3.0/pyproject.toml +204 -0
- vinowhisper-0.3.0/setup.cfg +4 -0
- vinowhisper-0.3.0/tests/test_audio.py +101 -0
- vinowhisper-0.3.0/tests/test_capture.py +181 -0
- vinowhisper-0.3.0/tests/test_devices.py +195 -0
- vinowhisper-0.3.0/tests/test_distro.py +75 -0
- vinowhisper-0.3.0/tests/test_doctor.py +140 -0
- vinowhisper-0.3.0/tests/test_session.py +91 -0
- vinowhisper-0.3.0/tests/test_stitch.py +136 -0
- vinowhisper-0.3.0/tests/test_ui.py +156 -0
- vinowhisper-0.3.0/tests/test_wizard.py +129 -0
- vinowhisper-0.3.0/vinowhisper/__init__.py +10 -0
- vinowhisper-0.3.0/vinowhisper/audio.py +114 -0
- vinowhisper-0.3.0/vinowhisper/caption.py +353 -0
- vinowhisper-0.3.0/vinowhisper/capture.py +305 -0
- vinowhisper-0.3.0/vinowhisper/client.py +70 -0
- vinowhisper-0.3.0/vinowhisper/config.py +133 -0
- vinowhisper-0.3.0/vinowhisper/devices.py +445 -0
- vinowhisper-0.3.0/vinowhisper/distro.py +330 -0
- vinowhisper-0.3.0/vinowhisper/doctor.py +445 -0
- vinowhisper-0.3.0/vinowhisper/events.py +71 -0
- vinowhisper-0.3.0/vinowhisper/recorder.py +161 -0
- vinowhisper-0.3.0/vinowhisper/replay.py +184 -0
- vinowhisper-0.3.0/vinowhisper/server.py +175 -0
- vinowhisper-0.3.0/vinowhisper/session.py +80 -0
- vinowhisper-0.3.0/vinowhisper/stitch.py +215 -0
- vinowhisper-0.3.0/vinowhisper/transcriber.py +171 -0
- vinowhisper-0.3.0/vinowhisper/ui.py +404 -0
- vinowhisper-0.3.0/vinowhisper/wizard.py +427 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/PKG-INFO +160 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/SOURCES.txt +36 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/dependency_links.txt +1 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/entry_points.txt +6 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/requires.txt +9 -0
- vinowhisper-0.3.0/vinowhisper.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karan Shukla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vinowhisper
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: NPU-accelerated local live captioning for Linux, via OpenVINO GenAI Whisper
|
|
5
|
+
Author: Karan Shukla
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/karanshukla/vinoWhisper
|
|
8
|
+
Project-URL: Repository, https://github.com/karanshukla/vinoWhisper
|
|
9
|
+
Project-URL: Issues, https://github.com/karanshukla/vinoWhisper/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/karanshukla/vinoWhisper/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: whisper,openvino,npu,captions,speech-to-text,pipewire,linux
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: <3.14,>=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: openvino>=2026.3.1
|
|
26
|
+
Requires-Dist: openvino-genai>=2026.3.1.0
|
|
27
|
+
Requires-Dist: openvino-tokenizers>=2026.3.1.0
|
|
28
|
+
Requires-Dist: optimum[openvino]
|
|
29
|
+
Requires-Dist: flask
|
|
30
|
+
Requires-Dist: werkzeug
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: numpy
|
|
33
|
+
Requires-Dist: rich
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# vinoWhisper
|
|
37
|
+
|
|
38
|
+
[](https://github.com/karanshukla/vinoWhisper/actions/workflows/ci.yml)
|
|
39
|
+
[](https://github.com/karanshukla/vinoWhisper/blob/main/LICENSE)
|
|
40
|
+
[](https://github.com/karanshukla/vinoWhisper/blob/main/pyproject.toml)
|
|
41
|
+
[](https://github.com/astral-sh/ruff)
|
|
42
|
+
|
|
43
|
+
NPU-accelerated local live captioning for Linux, using OpenVINO GenAI's
|
|
44
|
+
`WhisperPipeline` on an Intel NPU. Named after
|
|
45
|
+
[vinoAuthFace](https://github.com/karanshukla/vinoAuthFace), same idea of
|
|
46
|
+
OpenVINO doing the NPU work, different feature.
|
|
47
|
+
|
|
48
|
+
Point it at whatever is playing and it captions in your terminal. Nothing
|
|
49
|
+
leaves the machine. Start it and it goes; there is nothing to interact with.
|
|
50
|
+
|
|
51
|
+
<img width="1237" height="530" alt="image" src="https://github.com/user-attachments/assets/f263eabf-f1f4-4ab2-9b68-bc50eaf92ea0" />
|
|
52
|
+
|
|
53
|
+
The transcript scrolls above that bar in your terminal's own scrollback, so it
|
|
54
|
+
is still there after you quit and your terminal's selection and search still
|
|
55
|
+
work on it. `hearing…` is the words heard once but still waiting on a second
|
|
56
|
+
cycle to agree, which is the two-cycle commit delay made visible rather than
|
|
57
|
+
felt as a freeze.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -fsSL https://raw.githubusercontent.com/karanshukla/vinoWhisper/main/scripts/install.sh | bash
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That installs [uv](https://docs.astral.sh/uv/), clones the repo, builds the
|
|
66
|
+
environment, and hands over to `vinowhisper-setup`, which is where every
|
|
67
|
+
machine-specific decision happens: your capture tool, your NPU driver, the
|
|
68
|
+
model export your device needs, and systemd units generated against the paths
|
|
69
|
+
that actually exist. It prints every command before running it and asks first.
|
|
70
|
+
|
|
71
|
+
From a checkout, or to see what it would do without doing it:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/karanshukla/vinoWhisper && cd vinoWhisper
|
|
75
|
+
uv sync
|
|
76
|
+
uv run vinowhisper-setup --dry-run # the whole plan, nothing changed
|
|
77
|
+
uv run vinowhisper-setup # for real, one prompt per step
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or from PyPI, if you would rather wire up the machine yourself:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install vinowhisper # needs Python 3.11-3.13
|
|
84
|
+
vinowhisper-setup # still worth running: NPU driver, model export, units
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`pip install` gets you the five commands and the Python dependencies. It cannot
|
|
88
|
+
get you an NPU driver, a model export or systemd units, which is what
|
|
89
|
+
`vinowhisper-setup` is for either way. See
|
|
90
|
+
[docs/install.md](https://github.com/karanshukla/vinoWhisper/blob/main/docs/install.md) for the OpenVINO version
|
|
91
|
+
floor and why this could not be a pip install until 2026-08-31.
|
|
92
|
+
|
|
93
|
+
## Commands
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
vinowhisper-caption # caption system audio
|
|
97
|
+
vinowhisper-caption --source mic # caption yourself
|
|
98
|
+
vinowhisper-caption --list-targets # capture one app instead of the whole sink
|
|
99
|
+
vinowhisper-caption --debug # per-cycle timings, levels, raw transcript
|
|
100
|
+
vinowhisper-caption --record ~/sess # save the session for replay
|
|
101
|
+
vinowhisper-caption --plain > out.txt # no status bar (implied when piping)
|
|
102
|
+
|
|
103
|
+
vinowhisper-setup # guided install; re-runnable, idempotent
|
|
104
|
+
vinowhisper-setup --dry-run # print the plan, change nothing
|
|
105
|
+
vinowhisper-setup --print-units # the systemd units it would generate
|
|
106
|
+
|
|
107
|
+
vinowhisper-doctor # devices, model, audio, live levels
|
|
108
|
+
vinowhisper-doctor --json # the same, for a bug report
|
|
109
|
+
vinowhisper-doctor --no-probe # skip the 2s-per-target level capture
|
|
110
|
+
|
|
111
|
+
vinowhisper-replay ~/sess --restitch # re-run the merge logic offline
|
|
112
|
+
vinowhisper-replay ~/sess --sweep 8,12,20 # measure what --window actually costs
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Hardware
|
|
116
|
+
|
|
117
|
+
The NPU is the point. Everything below it exists so a broken driver degrades
|
|
118
|
+
the tool instead of bricking it.
|
|
119
|
+
|
|
120
|
+
| Device | Selected | Model export | What you get |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| **NPU** (`Intel(R) AI Boost`) | first | `--disable-stateful` | ~1.19s per 30s window, measured 2026-08-03 |
|
|
123
|
+
| **GPU** (Arc / Xe) | second | stateful | Untested here. Works in principle; watch the lag figure |
|
|
124
|
+
| **CPU** | last resort | stateful | Runs. Competes with everything else on the machine, and lags |
|
|
125
|
+
|
|
126
|
+
Selection is automatic and a fallback is never silent: it shows up in the
|
|
127
|
+
server journal, in `/health`, in `vinowhisper-doctor`, and on the status bar as
|
|
128
|
+
a red border. The two model exports are not interchangeable, and the NPU needs
|
|
129
|
+
a userspace driver half that no distro packages completely.
|
|
130
|
+
[docs/hardware.md](https://github.com/karanshukla/vinoWhisper/blob/main/docs/hardware.md) covers all of it, including what to do
|
|
131
|
+
when the NPU does not show up.
|
|
132
|
+
|
|
133
|
+
Audio capture works on PipeWire (`pw-record`) or PulseAudio (`parec`), picked
|
|
134
|
+
automatically, and package names for eight distro families live in one table in
|
|
135
|
+
[`vinowhisper/distro.py`](https://github.com/karanshukla/vinoWhisper/blob/main/vinowhisper/distro.py). **If a name is wrong for your
|
|
136
|
+
distro, that is expected, and it is the fastest thing here to fix.**
|
|
137
|
+
|
|
138
|
+
## Docs
|
|
139
|
+
|
|
140
|
+
| | |
|
|
141
|
+
|---|---|
|
|
142
|
+
| [Installing](https://github.com/karanshukla/vinoWhisper/blob/main/docs/install.md) | What the installer does, the OpenVINO version floor and why, pinning the window on top |
|
|
143
|
+
| [Hardware](https://github.com/karanshukla/vinoWhisper/blob/main/docs/hardware.md) | Device selection, the two model exports, and every way the NPU fails to appear |
|
|
144
|
+
| [Audio capture](https://github.com/karanshukla/vinoWhisper/blob/main/docs/audio.md) | PipeWire vs PulseAudio, distro coverage, and what actually silences a capture (it is not the mute button) |
|
|
145
|
+
| [Latency](https://github.com/karanshukla/vinoWhisper/blob/main/docs/latency.md) | Why captions trail the audio, the one knob that changes it, and why the wording drifts |
|
|
146
|
+
| [Debugging](https://github.com/karanshukla/vinoWhisper/blob/main/docs/debugging.md) | `--record`, offline replay, and what `vinowhisper-doctor` measures |
|
|
147
|
+
| [Architecture](https://github.com/karanshukla/vinoWhisper/blob/main/docs/architecture.md) | Socket activation and scale-to-zero, and how to stop it |
|
|
148
|
+
|
|
149
|
+
## More
|
|
150
|
+
|
|
151
|
+
- Design doc, benchmarks, and the three export bugs hit getting to a working
|
|
152
|
+
NPU pipeline:
|
|
153
|
+
[wildcat-lake-linux/input/f5-voice-typing.md](https://github.com/karanshukla/wildcat-lake-linux/blob/main/input/f5-voice-typing.md)
|
|
154
|
+
- [CONTRIBUTING.md](https://github.com/karanshukla/vinoWhisper/blob/main/CONTRIBUTING.md), where the useful contributions are distro
|
|
155
|
+
corrections and reports from hardware that isn't this laptop
|
|
156
|
+
- [SECURITY.md](https://github.com/karanshukla/vinoWhisper/blob/main/SECURITY.md), what stays on the machine and what the loopback
|
|
157
|
+
server's trust boundary actually is
|
|
158
|
+
- [CHANGELOG.md](https://github.com/karanshukla/vinoWhisper/blob/main/CHANGELOG.md)
|
|
159
|
+
|
|
160
|
+
MIT licensed.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# vinoWhisper
|
|
2
|
+
|
|
3
|
+
[](https://github.com/karanshukla/vinoWhisper/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/karanshukla/vinoWhisper/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/karanshukla/vinoWhisper/blob/main/pyproject.toml)
|
|
6
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
|
+
|
|
8
|
+
NPU-accelerated local live captioning for Linux, using OpenVINO GenAI's
|
|
9
|
+
`WhisperPipeline` on an Intel NPU. Named after
|
|
10
|
+
[vinoAuthFace](https://github.com/karanshukla/vinoAuthFace), same idea of
|
|
11
|
+
OpenVINO doing the NPU work, different feature.
|
|
12
|
+
|
|
13
|
+
Point it at whatever is playing and it captions in your terminal. Nothing
|
|
14
|
+
leaves the machine. Start it and it goes; there is nothing to interact with.
|
|
15
|
+
|
|
16
|
+
<img width="1237" height="530" alt="image" src="https://github.com/user-attachments/assets/f263eabf-f1f4-4ab2-9b68-bc50eaf92ea0" />
|
|
17
|
+
|
|
18
|
+
The transcript scrolls above that bar in your terminal's own scrollback, so it
|
|
19
|
+
is still there after you quit and your terminal's selection and search still
|
|
20
|
+
work on it. `hearing…` is the words heard once but still waiting on a second
|
|
21
|
+
cycle to agree, which is the two-cycle commit delay made visible rather than
|
|
22
|
+
felt as a freeze.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -fsSL https://raw.githubusercontent.com/karanshukla/vinoWhisper/main/scripts/install.sh | bash
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That installs [uv](https://docs.astral.sh/uv/), clones the repo, builds the
|
|
31
|
+
environment, and hands over to `vinowhisper-setup`, which is where every
|
|
32
|
+
machine-specific decision happens: your capture tool, your NPU driver, the
|
|
33
|
+
model export your device needs, and systemd units generated against the paths
|
|
34
|
+
that actually exist. It prints every command before running it and asks first.
|
|
35
|
+
|
|
36
|
+
From a checkout, or to see what it would do without doing it:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/karanshukla/vinoWhisper && cd vinoWhisper
|
|
40
|
+
uv sync
|
|
41
|
+
uv run vinowhisper-setup --dry-run # the whole plan, nothing changed
|
|
42
|
+
uv run vinowhisper-setup # for real, one prompt per step
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or from PyPI, if you would rather wire up the machine yourself:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install vinowhisper # needs Python 3.11-3.13
|
|
49
|
+
vinowhisper-setup # still worth running: NPU driver, model export, units
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`pip install` gets you the five commands and the Python dependencies. It cannot
|
|
53
|
+
get you an NPU driver, a model export or systemd units, which is what
|
|
54
|
+
`vinowhisper-setup` is for either way. See
|
|
55
|
+
[docs/install.md](https://github.com/karanshukla/vinoWhisper/blob/main/docs/install.md) for the OpenVINO version
|
|
56
|
+
floor and why this could not be a pip install until 2026-08-31.
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
vinowhisper-caption # caption system audio
|
|
62
|
+
vinowhisper-caption --source mic # caption yourself
|
|
63
|
+
vinowhisper-caption --list-targets # capture one app instead of the whole sink
|
|
64
|
+
vinowhisper-caption --debug # per-cycle timings, levels, raw transcript
|
|
65
|
+
vinowhisper-caption --record ~/sess # save the session for replay
|
|
66
|
+
vinowhisper-caption --plain > out.txt # no status bar (implied when piping)
|
|
67
|
+
|
|
68
|
+
vinowhisper-setup # guided install; re-runnable, idempotent
|
|
69
|
+
vinowhisper-setup --dry-run # print the plan, change nothing
|
|
70
|
+
vinowhisper-setup --print-units # the systemd units it would generate
|
|
71
|
+
|
|
72
|
+
vinowhisper-doctor # devices, model, audio, live levels
|
|
73
|
+
vinowhisper-doctor --json # the same, for a bug report
|
|
74
|
+
vinowhisper-doctor --no-probe # skip the 2s-per-target level capture
|
|
75
|
+
|
|
76
|
+
vinowhisper-replay ~/sess --restitch # re-run the merge logic offline
|
|
77
|
+
vinowhisper-replay ~/sess --sweep 8,12,20 # measure what --window actually costs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Hardware
|
|
81
|
+
|
|
82
|
+
The NPU is the point. Everything below it exists so a broken driver degrades
|
|
83
|
+
the tool instead of bricking it.
|
|
84
|
+
|
|
85
|
+
| Device | Selected | Model export | What you get |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| **NPU** (`Intel(R) AI Boost`) | first | `--disable-stateful` | ~1.19s per 30s window, measured 2026-08-03 |
|
|
88
|
+
| **GPU** (Arc / Xe) | second | stateful | Untested here. Works in principle; watch the lag figure |
|
|
89
|
+
| **CPU** | last resort | stateful | Runs. Competes with everything else on the machine, and lags |
|
|
90
|
+
|
|
91
|
+
Selection is automatic and a fallback is never silent: it shows up in the
|
|
92
|
+
server journal, in `/health`, in `vinowhisper-doctor`, and on the status bar as
|
|
93
|
+
a red border. The two model exports are not interchangeable, and the NPU needs
|
|
94
|
+
a userspace driver half that no distro packages completely.
|
|
95
|
+
[docs/hardware.md](https://github.com/karanshukla/vinoWhisper/blob/main/docs/hardware.md) covers all of it, including what to do
|
|
96
|
+
when the NPU does not show up.
|
|
97
|
+
|
|
98
|
+
Audio capture works on PipeWire (`pw-record`) or PulseAudio (`parec`), picked
|
|
99
|
+
automatically, and package names for eight distro families live in one table in
|
|
100
|
+
[`vinowhisper/distro.py`](https://github.com/karanshukla/vinoWhisper/blob/main/vinowhisper/distro.py). **If a name is wrong for your
|
|
101
|
+
distro, that is expected, and it is the fastest thing here to fix.**
|
|
102
|
+
|
|
103
|
+
## Docs
|
|
104
|
+
|
|
105
|
+
| | |
|
|
106
|
+
|---|---|
|
|
107
|
+
| [Installing](https://github.com/karanshukla/vinoWhisper/blob/main/docs/install.md) | What the installer does, the OpenVINO version floor and why, pinning the window on top |
|
|
108
|
+
| [Hardware](https://github.com/karanshukla/vinoWhisper/blob/main/docs/hardware.md) | Device selection, the two model exports, and every way the NPU fails to appear |
|
|
109
|
+
| [Audio capture](https://github.com/karanshukla/vinoWhisper/blob/main/docs/audio.md) | PipeWire vs PulseAudio, distro coverage, and what actually silences a capture (it is not the mute button) |
|
|
110
|
+
| [Latency](https://github.com/karanshukla/vinoWhisper/blob/main/docs/latency.md) | Why captions trail the audio, the one knob that changes it, and why the wording drifts |
|
|
111
|
+
| [Debugging](https://github.com/karanshukla/vinoWhisper/blob/main/docs/debugging.md) | `--record`, offline replay, and what `vinowhisper-doctor` measures |
|
|
112
|
+
| [Architecture](https://github.com/karanshukla/vinoWhisper/blob/main/docs/architecture.md) | Socket activation and scale-to-zero, and how to stop it |
|
|
113
|
+
|
|
114
|
+
## More
|
|
115
|
+
|
|
116
|
+
- Design doc, benchmarks, and the three export bugs hit getting to a working
|
|
117
|
+
NPU pipeline:
|
|
118
|
+
[wildcat-lake-linux/input/f5-voice-typing.md](https://github.com/karanshukla/wildcat-lake-linux/blob/main/input/f5-voice-typing.md)
|
|
119
|
+
- [CONTRIBUTING.md](https://github.com/karanshukla/vinoWhisper/blob/main/CONTRIBUTING.md), where the useful contributions are distro
|
|
120
|
+
corrections and reports from hardware that isn't this laptop
|
|
121
|
+
- [SECURITY.md](https://github.com/karanshukla/vinoWhisper/blob/main/SECURITY.md), what stays on the machine and what the loopback
|
|
122
|
+
server's trust boundary actually is
|
|
123
|
+
- [CHANGELOG.md](https://github.com/karanshukla/vinoWhisper/blob/main/CHANGELOG.md)
|
|
124
|
+
|
|
125
|
+
MIT licensed.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vinowhisper"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "NPU-accelerated local live captioning for Linux, via OpenVINO GenAI Whisper"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Karan Shukla" }]
|
|
9
|
+
keywords = ["whisper", "openvino", "npu", "captions", "speech-to-text", "pipewire", "linux"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: End Users/Desktop",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
]
|
|
22
|
+
# Python 3.14 is unusable here: it made functools.partial a descriptor, which
|
|
23
|
+
# breaks optimum's `NORMALIZED_CONFIG_CLASS = SomeConfig.with_args(...)` class-
|
|
24
|
+
# attribute idiom (self gets auto-bound as an extra positional arg). Confirmed
|
|
25
|
+
# 2026-08-03 across every optimum/optimum-intel/transformers version tried —
|
|
26
|
+
# root cause, not a version-pairing issue. Use 3.13 until upstream fixes it.
|
|
27
|
+
requires-python = ">=3.11,<3.14"
|
|
28
|
+
dependencies = [
|
|
29
|
+
# 2026.3.1 is the floor because it is the first *stable* release that can
|
|
30
|
+
# build the NPU static Whisper pipeline. Stable 2026.2.1 could not: its
|
|
31
|
+
# pipeline_static.cpp pattern-matcher did not recognize the optimum-intel
|
|
32
|
+
# export's SDPA attention-mask node shape
|
|
33
|
+
# (OPENVINO_ASSERT(!self_attn_nodes.empty())), which is why this project
|
|
34
|
+
# pinned the nightly wheel index from 2026-08-03 until 2026-08-31.
|
|
35
|
+
# Re-measured 2026-08-31 on the Wildcat Lake NPU against the same
|
|
36
|
+
# --disable-stateful export, WhisperPipeline(..., STATIC_PIPELINE=True):
|
|
37
|
+
# 2026.3.1 stable builds in 2.0s and decodes, as do nightly 2026.4.0.dev-
|
|
38
|
+
# 20260805 and 2026.5.0.dev20260831. Stable caught up, so the nightly
|
|
39
|
+
# index, the global prerelease policy and the weekly resolve canary are
|
|
40
|
+
# all gone and these resolve from PyPI like anything else.
|
|
41
|
+
"openvino>=2026.3.1",
|
|
42
|
+
"openvino-genai>=2026.3.1.0",
|
|
43
|
+
# Pulled in transitively by openvino-genai and still listed directly: the
|
|
44
|
+
# NPU pipeline hard-depends on it, and a transitive-only dependency is one
|
|
45
|
+
# upstream packaging change away from disappearing silently.
|
|
46
|
+
"openvino-tokenizers>=2026.3.1.0",
|
|
47
|
+
"optimum[openvino]",
|
|
48
|
+
"flask",
|
|
49
|
+
"werkzeug",
|
|
50
|
+
"requests",
|
|
51
|
+
"numpy",
|
|
52
|
+
# Status bar only. Rich rather than Textual because the transcript is
|
|
53
|
+
# append-only (see stitch.py), so it belongs in the terminal's own
|
|
54
|
+
# scrollback and only the status line needs to redraw.
|
|
55
|
+
"rich",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://github.com/karanshukla/vinoWhisper"
|
|
60
|
+
Repository = "https://github.com/karanshukla/vinoWhisper"
|
|
61
|
+
Issues = "https://github.com/karanshukla/vinoWhisper/issues"
|
|
62
|
+
Changelog = "https://github.com/karanshukla/vinoWhisper/blob/main/CHANGELOG.md"
|
|
63
|
+
|
|
64
|
+
[project.scripts]
|
|
65
|
+
vinowhisper-caption = "vinowhisper.caption:main"
|
|
66
|
+
vinowhisper-server = "vinowhisper.server:main"
|
|
67
|
+
vinowhisper-replay = "vinowhisper.replay:main"
|
|
68
|
+
vinowhisper-doctor = "vinowhisper.doctor:main"
|
|
69
|
+
vinowhisper-setup = "vinowhisper.wizard:main"
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["setuptools>=77"] # PEP 639 license expressions
|
|
73
|
+
build-backend = "setuptools.build_meta"
|
|
74
|
+
|
|
75
|
+
[tool.setuptools]
|
|
76
|
+
# Without this, setuptools' flat-layout auto-discovery sees the top-level
|
|
77
|
+
# systemd/ directory (unit files, not a package) and refuses to build,
|
|
78
|
+
# thinking it found two packages. Confirmed failing 2026-08-03.
|
|
79
|
+
packages = ["vinowhisper"]
|
|
80
|
+
|
|
81
|
+
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
[dependency-groups]
|
|
84
|
+
# Deliberately installable on its own (`uv sync --only-group dev`): CI has no
|
|
85
|
+
# NPU, no audio server and no reason to download ~400MB of OpenVINO to run
|
|
86
|
+
# tests that are forbidden from importing it. Nothing under tests/ imports
|
|
87
|
+
# openvino, transcriber or server, which is what keeps this split honest.
|
|
88
|
+
dev = [
|
|
89
|
+
"pytest>=8.0",
|
|
90
|
+
"pytest-cov>=5.0",
|
|
91
|
+
"ruff>=0.6",
|
|
92
|
+
"mypy>=1.10",
|
|
93
|
+
"poethepoet>=0.27",
|
|
94
|
+
"bump-my-version>=0.24",
|
|
95
|
+
"types-requests",
|
|
96
|
+
"numpy",
|
|
97
|
+
"rich",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# ── Tasks ─────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
[tool.poe.tasks]
|
|
103
|
+
lint = "ruff check ."
|
|
104
|
+
fmt = "ruff format --check ."
|
|
105
|
+
types = "mypy vinowhisper/"
|
|
106
|
+
test = "pytest -v --tb=short"
|
|
107
|
+
fix.sequence = [{ cmd = "ruff check --fix ." }, { cmd = "ruff format ." }]
|
|
108
|
+
security.cmd = "uvx --from bandit[toml] bandit -c pyproject.toml -r vinowhisper/"
|
|
109
|
+
security.help = "the same Bandit scan the Bandit workflow runs"
|
|
110
|
+
check.sequence = ["lint", "fmt", "types", "test"]
|
|
111
|
+
check.help = "everything CI runs, locally"
|
|
112
|
+
doctor = "python -m vinowhisper.doctor"
|
|
113
|
+
|
|
114
|
+
# ── Ruff ──────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
[tool.ruff]
|
|
117
|
+
line-length = 100
|
|
118
|
+
target-version = "py311"
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint]
|
|
121
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
122
|
+
ignore = [
|
|
123
|
+
"E501", # line length is the formatter's job
|
|
124
|
+
"SIM108", # ternary-instead-of-if is often less readable
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.isort]
|
|
128
|
+
known-first-party = ["vinowhisper"]
|
|
129
|
+
|
|
130
|
+
# ── Mypy ──────────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
[tool.mypy]
|
|
133
|
+
# Deliberately not pinned to python_version = "3.11". Numpy's own stubs use
|
|
134
|
+
# 3.12-only syntax (`type X = ...`), and mypy parses them against the target
|
|
135
|
+
# version, so pinning 3.11 while running on 3.12 fails inside numpy with a
|
|
136
|
+
# syntax error before it checks any of this project's code. The floor is
|
|
137
|
+
# enforced where it belongs anyway: requires-python and ruff's target-version.
|
|
138
|
+
#
|
|
139
|
+
# openvino_genai ships no stubs, and CI type-checks without it installed at all
|
|
140
|
+
# (see the dev group note above).
|
|
141
|
+
ignore_missing_imports = true
|
|
142
|
+
warn_unused_ignores = false
|
|
143
|
+
|
|
144
|
+
# ── Pytest ────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
[tool.pytest.ini_options]
|
|
147
|
+
testpaths = ["tests"]
|
|
148
|
+
python_files = ["test_*.py"]
|
|
149
|
+
addopts = "-q"
|
|
150
|
+
# The package is imported from the source tree, not from an install: CI never
|
|
151
|
+
# installs the project itself, because that would drag in the OpenVINO nightly
|
|
152
|
+
# stack it deliberately does without.
|
|
153
|
+
pythonpath = ["."]
|
|
154
|
+
|
|
155
|
+
# ── Coverage ──────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
[tool.coverage.run]
|
|
158
|
+
source = ["vinowhisper"]
|
|
159
|
+
# Not covered by design: these need an NPU, an audio server, or a live socket.
|
|
160
|
+
omit = ["vinowhisper/transcriber.py", "vinowhisper/server.py", "vinowhisper/replay.py"]
|
|
161
|
+
|
|
162
|
+
[tool.coverage.report]
|
|
163
|
+
show_missing = true
|
|
164
|
+
|
|
165
|
+
# ── Bandit ────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
# Read with `bandit -c pyproject.toml` (needs the `toml` extra). The Bandit
|
|
168
|
+
# workflow passes the same flag, so CI and `poe security` agree.
|
|
169
|
+
[tool.bandit]
|
|
170
|
+
exclude_dirs = ["tests", ".venv", ".git", "build", "dist"]
|
|
171
|
+
# Three blanket skips, each because the finding is this tool's entire job
|
|
172
|
+
# rather than a defect in it. B602 (shell=True) stays ON, and is the one that
|
|
173
|
+
# would actually matter: there is no shell anywhere in this codebase, and if
|
|
174
|
+
# one ever appears it should fail this scan.
|
|
175
|
+
skips = [
|
|
176
|
+
# blacklist — flags the `import subprocess` line itself, in a program whose
|
|
177
|
+
# purpose is to drive pw-record, parec, pactl, optimum-cli and systemctl.
|
|
178
|
+
"B404",
|
|
179
|
+
# subprocess_without_shell_equals_true. Every call site builds a literal
|
|
180
|
+
# argv list and passes it without a shell, so there is nothing to inject
|
|
181
|
+
# into: the variable parts are PipeWire node names read from pw-dump and a
|
|
182
|
+
# --target the user typed, both of which arrive as single argv elements.
|
|
183
|
+
"B603",
|
|
184
|
+
# start_process_with_partial_path. Deliberate: pw-record, parec and pactl
|
|
185
|
+
# live in different places on different distros, and hardcoding /usr/bin
|
|
186
|
+
# would break the fallbacks this release exists to add. PATH resolution is
|
|
187
|
+
# the portable behaviour here, and the tools are located with shutil.which
|
|
188
|
+
# before use.
|
|
189
|
+
"B607",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# ── bump-my-version ───────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
[tool.bumpversion]
|
|
195
|
+
current_version = "0.3.0"
|
|
196
|
+
commit = true
|
|
197
|
+
tag = true
|
|
198
|
+
tag_name = "v{new_version}"
|
|
199
|
+
pre_commit_hooks = ["uv lock", "git add uv.lock"]
|
|
200
|
+
|
|
201
|
+
[[tool.bumpversion.files]]
|
|
202
|
+
filename = "pyproject.toml"
|
|
203
|
+
search = 'version = "{current_version}"'
|
|
204
|
+
replace = 'version = "{new_version}"'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""RingBuffer and level helpers.
|
|
2
|
+
|
|
3
|
+
The ring buffer replaced a `np.concatenate([buf, chunk])[-cap:]` that copied
|
|
4
|
+
~1.9MB twice per 100ms read. The interesting cases are all about the wrap:
|
|
5
|
+
writing across the end, reading across it, and a chunk larger than the whole
|
|
6
|
+
buffer, which must not corrupt the write index.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from vinowhisper.audio import RingBuffer, normalize, rms
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ramp(count: int, start: int = 0) -> np.ndarray:
|
|
16
|
+
return np.arange(start, start + count, dtype=np.float32)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_capacity_must_be_positive():
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
RingBuffer(0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_reads_back_what_was_written():
|
|
25
|
+
buffer = RingBuffer(10)
|
|
26
|
+
buffer.write(ramp(4))
|
|
27
|
+
assert list(buffer.read_last(4)) == [0, 1, 2, 3]
|
|
28
|
+
assert buffer.total_written == 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_read_more_than_written_returns_what_there_is():
|
|
32
|
+
buffer = RingBuffer(10)
|
|
33
|
+
buffer.write(ramp(3))
|
|
34
|
+
assert list(buffer.read_last(100)) == [0, 1, 2]
|
|
35
|
+
assert buffer.read_last(0).size == 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_keeps_the_newest_samples_when_it_wraps():
|
|
39
|
+
buffer = RingBuffer(5)
|
|
40
|
+
buffer.write(ramp(8)) # 0..7, so 3..7 survive
|
|
41
|
+
assert list(buffer.read_last(5)) == [3, 4, 5, 6, 7]
|
|
42
|
+
# total_written is monotonic — the caption loop measures new audio with it.
|
|
43
|
+
assert buffer.total_written == 8
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_read_spanning_the_wrap_point_stays_in_order():
|
|
47
|
+
buffer = RingBuffer(5)
|
|
48
|
+
buffer.write(ramp(3))
|
|
49
|
+
buffer.write(ramp(4, start=3)) # writes 3,4,5,6 across the boundary
|
|
50
|
+
assert list(buffer.read_last(5)) == [2, 3, 4, 5, 6]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_chunk_larger_than_capacity_keeps_the_tail_and_the_count():
|
|
54
|
+
buffer = RingBuffer(4)
|
|
55
|
+
buffer.write(ramp(10))
|
|
56
|
+
assert list(buffer.read_last(4)) == [6, 7, 8, 9]
|
|
57
|
+
assert buffer.total_written == 10
|
|
58
|
+
# The write index must still be consistent afterwards.
|
|
59
|
+
buffer.write(ramp(2, start=10))
|
|
60
|
+
assert list(buffer.read_last(4)) == [8, 9, 10, 11]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_empty_write_is_a_noop():
|
|
64
|
+
buffer = RingBuffer(4)
|
|
65
|
+
buffer.write(np.zeros(0, dtype=np.float32))
|
|
66
|
+
assert buffer.total_written == 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_rms_of_silence_and_of_a_constant():
|
|
70
|
+
assert rms(np.zeros(0, dtype=np.float32)) == 0.0
|
|
71
|
+
assert rms(np.zeros(100, dtype=np.float32)) == 0.0
|
|
72
|
+
assert rms(np.full(100, 0.5, dtype=np.float32)) == pytest.approx(0.5)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_normalize_boosts_quiet_audio_towards_the_target():
|
|
76
|
+
quiet = np.full(100, 0.01, dtype=np.float32)
|
|
77
|
+
boosted, gain = normalize(quiet, target_rms=0.05, max_gain=20.0)
|
|
78
|
+
assert gain == pytest.approx(5.0)
|
|
79
|
+
assert rms(boosted) == pytest.approx(0.05, rel=1e-3)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_normalize_never_attenuates():
|
|
83
|
+
loud = np.full(100, 0.5, dtype=np.float32)
|
|
84
|
+
same, gain = normalize(loud, target_rms=0.05, max_gain=20.0)
|
|
85
|
+
assert gain == 1.0
|
|
86
|
+
assert np.array_equal(same, loud)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_normalize_respects_max_gain_and_clips():
|
|
90
|
+
very_quiet = np.full(100, 0.001, dtype=np.float32)
|
|
91
|
+
boosted, gain = normalize(very_quiet, target_rms=0.05, max_gain=20.0)
|
|
92
|
+
assert gain == 20.0
|
|
93
|
+
assert boosted.dtype == np.float32
|
|
94
|
+
assert boosted.max() <= 1.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_normalize_leaves_digital_silence_alone():
|
|
98
|
+
silence = np.zeros(100, dtype=np.float32)
|
|
99
|
+
same, gain = normalize(silence, target_rms=0.05, max_gain=20.0)
|
|
100
|
+
assert gain == 1.0
|
|
101
|
+
assert np.array_equal(same, silence)
|