vinospeak 0.1.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.
- vinospeak-0.1.0/LICENSE +21 -0
- vinospeak-0.1.0/PKG-INFO +141 -0
- vinospeak-0.1.0/README.md +107 -0
- vinospeak-0.1.0/pyproject.toml +137 -0
- vinospeak-0.1.0/setup.cfg +4 -0
- vinospeak-0.1.0/tests/test_cli.py +52 -0
- vinospeak-0.1.0/tests/test_doctor.py +86 -0
- vinospeak-0.1.0/tests/test_model.py +187 -0
- vinospeak-0.1.0/tests/test_ovfetch.py +133 -0
- vinospeak-0.1.0/tests/test_playback.py +112 -0
- vinospeak-0.1.0/tests/test_telemetry.py +27 -0
- vinospeak-0.1.0/tests/test_text.py +57 -0
- vinospeak-0.1.0/tests/test_tts.py +242 -0
- vinospeak-0.1.0/vino_speak/__init__.py +23 -0
- vinospeak-0.1.0/vino_speak/cli.py +61 -0
- vinospeak-0.1.0/vino_speak/doctor.py +140 -0
- vinospeak-0.1.0/vino_speak/model.py +157 -0
- vinospeak-0.1.0/vino_speak/model_pins.json +273 -0
- vinospeak-0.1.0/vino_speak/ovfetch.py +135 -0
- vinospeak-0.1.0/vino_speak/playback.py +152 -0
- vinospeak-0.1.0/vino_speak/py.typed +0 -0
- vinospeak-0.1.0/vino_speak/text.py +64 -0
- vinospeak-0.1.0/vino_speak/tts.py +233 -0
- vinospeak-0.1.0/vinospeak.egg-info/PKG-INFO +141 -0
- vinospeak-0.1.0/vinospeak.egg-info/SOURCES.txt +27 -0
- vinospeak-0.1.0/vinospeak.egg-info/dependency_links.txt +1 -0
- vinospeak-0.1.0/vinospeak.egg-info/entry_points.txt +3 -0
- vinospeak-0.1.0/vinospeak.egg-info/requires.txt +6 -0
- vinospeak-0.1.0/vinospeak.egg-info/top_level.txt +1 -0
vinospeak-0.1.0/LICENSE
ADDED
|
@@ -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.
|
vinospeak-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vinospeak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Intel NPU-powered Kokoro text-to-speech with OpenVINO
|
|
5
|
+
Author: Karan Shukla
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/karanshukla/vinoSpeak
|
|
8
|
+
Project-URL: Repository, https://github.com/karanshukla/vinoSpeak
|
|
9
|
+
Project-URL: Issues, https://github.com/karanshukla/vinoSpeak/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/karanshukla/vinoSpeak/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: kokoro,openvino,npu,text-to-speech,tts,linux
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: espeakng-loader>=0.2.4
|
|
28
|
+
Requires-Dist: huggingface-hub>=0.24.0
|
|
29
|
+
Requires-Dist: numpy>=1.24.0
|
|
30
|
+
Requires-Dist: openvino==2026.4.1
|
|
31
|
+
Requires-Dist: openvino-genai==2026.4.1.0
|
|
32
|
+
Requires-Dist: soundfile>=0.12.1
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# vinoSpeak
|
|
36
|
+
|
|
37
|
+
Intel NPU-powered text-to-speech using the Kokoro-82M INT8 OpenVINO model.
|
|
38
|
+
Text goes in, a WAV comes out, and nothing leaves your machine after the
|
|
39
|
+
one-time model download.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Python 3.11 or newer
|
|
44
|
+
- An Intel NPU with a compatible driver. GPU and CPU also work
|
|
45
|
+
(`--device GPU`, `--device CPU`).
|
|
46
|
+
- Internet access on first run to download the model (~160 MB)
|
|
47
|
+
|
|
48
|
+
OpenVINO is pinned to the release vinoPop and vinoWhisper use, so programs
|
|
49
|
+
sharing one NPU never run mismatched runtimes.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
From PyPI, with [uv](https://docs.astral.sh/uv/):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv tool install vinospeak
|
|
57
|
+
vinospeak "Hello."
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
OpenVINO is pinned exactly; everything else resolves to the newest release
|
|
61
|
+
that fits. To install exactly what `uv.lock` pins, hashes included, use a
|
|
62
|
+
checkout. `--locked` fails if the lock is out of date:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/karanshukla/vinoSpeak && cd vinoSpeak
|
|
66
|
+
uv sync --locked
|
|
67
|
+
uv run vinospeak "Hello."
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To put that checkout's `vinospeak` on your PATH with the lock's versions as
|
|
71
|
+
constraints (`uv tool install` doesn't read `uv.lock` by itself):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv export --locked --no-dev --no-emit-project --no-hashes -o constraints.txt
|
|
75
|
+
uv tool install --constraints constraints.txt .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Synthesize speech
|
|
79
|
+
|
|
80
|
+
The first run downloads `OpenVINO/Kokoro-82M-int8-ov` from Hugging Face, at one
|
|
81
|
+
pinned commit, and checks every file against its sha256 (see
|
|
82
|
+
[SECURITY.md](SECURITY.md)). The model is cached under
|
|
83
|
+
`${XDG_CACHE_HOME:-~/.cache}/vinospeak/` by default.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
vinospeak "Hello from vinoSpeak." # plays it on your speakers
|
|
87
|
+
vinospeak "Hello from vinoSpeak." --output hello.wav # saves a WAV instead
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Playback goes through `pw-play` (PipeWire, what Wayland desktops run), falling
|
|
91
|
+
back to `paplay` or `aplay`.
|
|
92
|
+
|
|
93
|
+
The default inference device is the Intel NPU. Choose a different OpenVINO
|
|
94
|
+
device, voice, language, speed, or model directory with the corresponding
|
|
95
|
+
options:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
vinospeak "A sample sentence." \
|
|
99
|
+
--device NPU \
|
|
100
|
+
--voice af_heart \
|
|
101
|
+
--language en-us \
|
|
102
|
+
--speed 1.0 \
|
|
103
|
+
--model-dir ./kokoro-model \
|
|
104
|
+
--output speech.wav
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `vinospeak --help` to see all options. The audio uses the sample rate
|
|
108
|
+
reported by OpenVINO GenAI.
|
|
109
|
+
|
|
110
|
+
## Check this machine
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
vinospeak-doctor # --json for a bug report
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Reports the OpenVINO packages and devices, whether the cached model still
|
|
117
|
+
matches its pins, and, if [ovfetch](https://github.com/karanshukla/ovfetch) is
|
|
118
|
+
on your `PATH`, whether your OpenVINO version is inside the range recorded as
|
|
119
|
+
working with your NPU driver. Install it with `cargo install ovfetch --locked`.
|
|
120
|
+
ovfetch is advisory here: it never changes what gets installed or run.
|
|
121
|
+
|
|
122
|
+
## Use it from Python
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from vino_speak import Synthesizer
|
|
126
|
+
|
|
127
|
+
synth = Synthesizer("NPU") # compiles the model once; reuse it
|
|
128
|
+
audio = synth.synthesize("Hello.") # audio.samples (float32), audio.sample_rate
|
|
129
|
+
|
|
130
|
+
speech = synth.speak("A long passage.") # returns at once, plays in the background
|
|
131
|
+
speech.stop() # cuts it off; speech.wait() blocks until it finishes instead
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`speak()` synthesizes the first sentence on its own and plays it while the
|
|
135
|
+
rest is synthesized in chunks that at most double in size, so speech starts
|
|
136
|
+
after about 1.5 s on the NPU however long the text is.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). `uv run poe check` runs everything CI
|
|
141
|
+
does; none of it needs OpenVINO or an NPU.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# vinoSpeak
|
|
2
|
+
|
|
3
|
+
Intel NPU-powered text-to-speech using the Kokoro-82M INT8 OpenVINO model.
|
|
4
|
+
Text goes in, a WAV comes out, and nothing leaves your machine after the
|
|
5
|
+
one-time model download.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.11 or newer
|
|
10
|
+
- An Intel NPU with a compatible driver. GPU and CPU also work
|
|
11
|
+
(`--device GPU`, `--device CPU`).
|
|
12
|
+
- Internet access on first run to download the model (~160 MB)
|
|
13
|
+
|
|
14
|
+
OpenVINO is pinned to the release vinoPop and vinoWhisper use, so programs
|
|
15
|
+
sharing one NPU never run mismatched runtimes.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
From PyPI, with [uv](https://docs.astral.sh/uv/):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install vinospeak
|
|
23
|
+
vinospeak "Hello."
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
OpenVINO is pinned exactly; everything else resolves to the newest release
|
|
27
|
+
that fits. To install exactly what `uv.lock` pins, hashes included, use a
|
|
28
|
+
checkout. `--locked` fails if the lock is out of date:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/karanshukla/vinoSpeak && cd vinoSpeak
|
|
32
|
+
uv sync --locked
|
|
33
|
+
uv run vinospeak "Hello."
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
To put that checkout's `vinospeak` on your PATH with the lock's versions as
|
|
37
|
+
constraints (`uv tool install` doesn't read `uv.lock` by itself):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv export --locked --no-dev --no-emit-project --no-hashes -o constraints.txt
|
|
41
|
+
uv tool install --constraints constraints.txt .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Synthesize speech
|
|
45
|
+
|
|
46
|
+
The first run downloads `OpenVINO/Kokoro-82M-int8-ov` from Hugging Face, at one
|
|
47
|
+
pinned commit, and checks every file against its sha256 (see
|
|
48
|
+
[SECURITY.md](SECURITY.md)). The model is cached under
|
|
49
|
+
`${XDG_CACHE_HOME:-~/.cache}/vinospeak/` by default.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
vinospeak "Hello from vinoSpeak." # plays it on your speakers
|
|
53
|
+
vinospeak "Hello from vinoSpeak." --output hello.wav # saves a WAV instead
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Playback goes through `pw-play` (PipeWire, what Wayland desktops run), falling
|
|
57
|
+
back to `paplay` or `aplay`.
|
|
58
|
+
|
|
59
|
+
The default inference device is the Intel NPU. Choose a different OpenVINO
|
|
60
|
+
device, voice, language, speed, or model directory with the corresponding
|
|
61
|
+
options:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
vinospeak "A sample sentence." \
|
|
65
|
+
--device NPU \
|
|
66
|
+
--voice af_heart \
|
|
67
|
+
--language en-us \
|
|
68
|
+
--speed 1.0 \
|
|
69
|
+
--model-dir ./kokoro-model \
|
|
70
|
+
--output speech.wav
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `vinospeak --help` to see all options. The audio uses the sample rate
|
|
74
|
+
reported by OpenVINO GenAI.
|
|
75
|
+
|
|
76
|
+
## Check this machine
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
vinospeak-doctor # --json for a bug report
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Reports the OpenVINO packages and devices, whether the cached model still
|
|
83
|
+
matches its pins, and, if [ovfetch](https://github.com/karanshukla/ovfetch) is
|
|
84
|
+
on your `PATH`, whether your OpenVINO version is inside the range recorded as
|
|
85
|
+
working with your NPU driver. Install it with `cargo install ovfetch --locked`.
|
|
86
|
+
ovfetch is advisory here: it never changes what gets installed or run.
|
|
87
|
+
|
|
88
|
+
## Use it from Python
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from vino_speak import Synthesizer
|
|
92
|
+
|
|
93
|
+
synth = Synthesizer("NPU") # compiles the model once; reuse it
|
|
94
|
+
audio = synth.synthesize("Hello.") # audio.samples (float32), audio.sample_rate
|
|
95
|
+
|
|
96
|
+
speech = synth.speak("A long passage.") # returns at once, plays in the background
|
|
97
|
+
speech.stop() # cuts it off; speech.wait() blocks until it finishes instead
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`speak()` synthesizes the first sentence on its own and plays it while the
|
|
101
|
+
rest is synthesized in chunks that at most double in size, so speech starts
|
|
102
|
+
after about 1.5 s on the NPU however long the text is.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). `uv run poe check` runs everything CI
|
|
107
|
+
does; none of it needs OpenVINO or an NPU.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=78.1.1"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vinospeak"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Intel NPU-powered Kokoro text-to-speech with OpenVINO"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Karan Shukla" }]
|
|
13
|
+
keywords = ["kokoro", "openvino", "npu", "text-to-speech", "tts", "linux"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.11"
|
|
29
|
+
# OpenVINO is pinned to the exact release the other vino apps use (vinoPop's
|
|
30
|
+
# requirements-ai.txt, vinoWhisper's uv.lock): two programs on one NPU must not
|
|
31
|
+
# run different OpenVINO releases. Bump it in step with them; `ovfetch detect`
|
|
32
|
+
# reports the range your NPU driver is known to work with.
|
|
33
|
+
dependencies = [
|
|
34
|
+
"espeakng-loader>=0.2.4",
|
|
35
|
+
"huggingface-hub>=0.24.0",
|
|
36
|
+
"numpy>=1.24.0",
|
|
37
|
+
"openvino==2026.4.1",
|
|
38
|
+
"openvino-genai==2026.4.1.0",
|
|
39
|
+
"soundfile>=0.12.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/karanshukla/vinoSpeak"
|
|
44
|
+
Repository = "https://github.com/karanshukla/vinoSpeak"
|
|
45
|
+
Issues = "https://github.com/karanshukla/vinoSpeak/issues"
|
|
46
|
+
Changelog = "https://github.com/karanshukla/vinoSpeak/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
vinospeak = "vino_speak.cli:main"
|
|
50
|
+
vinospeak-doctor = "vino_speak.doctor:main"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
include = ["vino_speak*"]
|
|
54
|
+
|
|
55
|
+
# Dropping either from the wheel fails quietly: the pins are read at run time.
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
vino_speak = ["model_pins.json", "py.typed"]
|
|
58
|
+
|
|
59
|
+
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
# Installable alone, so CI never downloads OpenVINO
|
|
63
|
+
dev = [
|
|
64
|
+
"pytest>=8.0",
|
|
65
|
+
"pytest-cov>=5.0",
|
|
66
|
+
"ruff>=0.6",
|
|
67
|
+
"mypy>=1.10",
|
|
68
|
+
"poethepoet>=0.27",
|
|
69
|
+
"bump-my-version>=0.24",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# ── bump-my-version ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
[tool.bumpversion]
|
|
75
|
+
current_version = "0.1.0"
|
|
76
|
+
commit = true
|
|
77
|
+
tag = true
|
|
78
|
+
tag_name = "v{new_version}"
|
|
79
|
+
pre_commit_hooks = ["uv lock", "git add uv.lock"]
|
|
80
|
+
|
|
81
|
+
[[tool.bumpversion.files]]
|
|
82
|
+
filename = "pyproject.toml"
|
|
83
|
+
search = 'version = "{current_version}"'
|
|
84
|
+
replace = 'version = "{new_version}"'
|
|
85
|
+
|
|
86
|
+
# ── Tasks ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
[tool.poe.tasks]
|
|
89
|
+
lint = "ruff check ."
|
|
90
|
+
fmt = "ruff format --check ."
|
|
91
|
+
types = "mypy"
|
|
92
|
+
test = "pytest -v --tb=short"
|
|
93
|
+
fix.sequence = [{ cmd = "ruff check --fix ." }, { cmd = "ruff format ." }]
|
|
94
|
+
security.cmd = "uvx --from bandit[toml]==1.9.4 bandit -c pyproject.toml -r ."
|
|
95
|
+
security.help = "the same Bandit scan the Bandit workflow runs"
|
|
96
|
+
check.sequence = ["lint", "fmt", "types", "test"]
|
|
97
|
+
check.help = "everything CI runs, locally"
|
|
98
|
+
doctor = "vinospeak-doctor"
|
|
99
|
+
|
|
100
|
+
# ── Ruff ──────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
[tool.ruff]
|
|
103
|
+
line-length = 100
|
|
104
|
+
target-version = "py311"
|
|
105
|
+
extend-exclude = [".venv"]
|
|
106
|
+
|
|
107
|
+
[tool.ruff.lint]
|
|
108
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
109
|
+
ignore = [
|
|
110
|
+
"E501", # line length is the formatter's job
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# ── Mypy ──────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
[tool.mypy]
|
|
116
|
+
files = ["vino_speak", "scripts"]
|
|
117
|
+
ignore_missing_imports = true
|
|
118
|
+
strict = true
|
|
119
|
+
|
|
120
|
+
# ── Pytest ────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
[tool.pytest.ini_options]
|
|
123
|
+
testpaths = ["tests"]
|
|
124
|
+
python_files = ["test_*.py"]
|
|
125
|
+
addopts = "-q"
|
|
126
|
+
|
|
127
|
+
[tool.coverage.report]
|
|
128
|
+
show_missing = true
|
|
129
|
+
|
|
130
|
+
# ── Bandit ────────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
[tool.bandit]
|
|
133
|
+
exclude_dirs = ["tests", ".venv", ".git", "build", "dist"]
|
|
134
|
+
skips = [
|
|
135
|
+
"B404", # import subprocess: ovfetch is run by design
|
|
136
|
+
"B603", # subprocess without shell
|
|
137
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from vino_speak import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parser_defaults_to_npu():
|
|
7
|
+
arguments = cli.build_parser().parse_args(["Hello"])
|
|
8
|
+
assert arguments.device == "NPU"
|
|
9
|
+
assert arguments.voice == "af_heart"
|
|
10
|
+
assert arguments.model_dir is None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_main_reports_errors_through_the_parser(monkeypatch, capsys):
|
|
14
|
+
def refuse(*args, **kwargs):
|
|
15
|
+
raise ValueError("Speed must be between 0.5 and 2.0.")
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr(cli, "synthesize", refuse)
|
|
18
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
19
|
+
cli.main(["Hello", "--speed", "9"])
|
|
20
|
+
assert exit_info.value.code == 2
|
|
21
|
+
assert "Speed must be between" in capsys.readouterr().err
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_main_prints_where_the_speech_went(monkeypatch, capsys, tmp_path):
|
|
25
|
+
monkeypatch.setattr(cli, "synthesize", lambda *args, **kwargs: None)
|
|
26
|
+
cli.main(["Hello", "--output", str(tmp_path / "out.wav")])
|
|
27
|
+
assert str(tmp_path / "out.wav") in capsys.readouterr().out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_main_speaks_when_there_is_no_output(monkeypatch, capsys):
|
|
31
|
+
spoken = []
|
|
32
|
+
monkeypatch.setattr(cli, "speak", lambda text, **kwargs: spoken.append(text))
|
|
33
|
+
monkeypatch.setattr(cli, "synthesize", lambda *args, **kwargs: pytest.fail("wrote a file"))
|
|
34
|
+
cli.main(["Hello"])
|
|
35
|
+
assert spoken == ["Hello"]
|
|
36
|
+
assert capsys.readouterr().out == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_saving_to_a_file_does_not_speak(monkeypatch, tmp_path):
|
|
40
|
+
monkeypatch.setattr(cli, "synthesize", lambda *args, **kwargs: None)
|
|
41
|
+
monkeypatch.setattr(cli, "speak", lambda *args, **kwargs: pytest.fail("spoke a saved file"))
|
|
42
|
+
cli.main(["Hello", "--output", str(tmp_path / "out.wav")])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_ctrl_c_exits_quietly_with_130(monkeypatch):
|
|
46
|
+
def interrupted(*args, **kwargs):
|
|
47
|
+
raise KeyboardInterrupt
|
|
48
|
+
|
|
49
|
+
monkeypatch.setattr(cli, "speak", interrupted)
|
|
50
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
51
|
+
cli.main(["Hello"])
|
|
52
|
+
assert exit_info.value.code == 130
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from vino_speak import doctor, ovfetch
|
|
6
|
+
from vino_speak.model import MODEL_FILES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def isolated(monkeypatch, tmp_path):
|
|
11
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
12
|
+
monkeypatch.setattr(doctor, "_distribution", lambda name: "1.0")
|
|
13
|
+
monkeypatch.setattr(
|
|
14
|
+
doctor, "_devices", lambda: [doctor.Result(doctor.OK, "devices", "CPU, NPU")]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def by_label(results, label):
|
|
19
|
+
return next(r for r in results if r.label == label)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_a_missing_package_is_a_failure(monkeypatch):
|
|
23
|
+
monkeypatch.setattr(doctor, "_distribution", lambda name: None)
|
|
24
|
+
assert by_label(doctor._packages(), "openvino-genai").status == doctor.FAIL
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_a_model_not_yet_downloaded_is_unknown_not_failed():
|
|
28
|
+
assert by_label(doctor._model(), "model").status == doctor.UNKNOWN
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_stale_model_fails(monkeypatch, tmp_path):
|
|
32
|
+
for name in MODEL_FILES:
|
|
33
|
+
path = tmp_path / "vinospeak" / "kokoro-82m-int8-ov" / name
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
path.write_bytes(b"stale")
|
|
36
|
+
assert by_label(doctor._model(), "model").status == doctor.FAIL
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_ovfetch_is_only_consulted_with_an_npu(monkeypatch):
|
|
40
|
+
monkeypatch.setattr(ovfetch, "detect", lambda: pytest.fail("no NPU, so no ovfetch"))
|
|
41
|
+
assert doctor._ovfetch(has_npu=False) == []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_a_missing_ovfetch_is_unknown_with_an_install_hint(monkeypatch):
|
|
45
|
+
monkeypatch.setattr(ovfetch, "detect", lambda: None)
|
|
46
|
+
(result,) = doctor._ovfetch(has_npu=True)
|
|
47
|
+
assert result.status == doctor.UNKNOWN
|
|
48
|
+
assert ovfetch.INSTALL_HINT in result.detail
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_a_broken_ovfetch_is_a_warning_never_a_failure(monkeypatch):
|
|
52
|
+
def broken():
|
|
53
|
+
raise ovfetch.OvfetchError("ovfetch detect exited 2: boom")
|
|
54
|
+
|
|
55
|
+
monkeypatch.setattr(ovfetch, "detect", broken)
|
|
56
|
+
(result,) = doctor._ovfetch(has_npu=True)
|
|
57
|
+
assert result.status == doctor.WARN
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_ovfetch_notes_become_results_and_never_fail(monkeypatch):
|
|
61
|
+
status = {
|
|
62
|
+
"devices": [{"kind": "npu"}],
|
|
63
|
+
"openvino_ceiling": "2025.4",
|
|
64
|
+
"platforms": [],
|
|
65
|
+
"npu_driver": "1.32.0",
|
|
66
|
+
}
|
|
67
|
+
monkeypatch.setattr(ovfetch, "detect", lambda: status)
|
|
68
|
+
monkeypatch.setattr(doctor, "_distribution", lambda name: "2026.4.1")
|
|
69
|
+
results = doctor._ovfetch(has_npu=True)
|
|
70
|
+
assert by_label(results, "npu: openvino range").status == doctor.WARN
|
|
71
|
+
assert all(r.status != doctor.FAIL for r in results)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_json_output_and_exit_status(capsys, monkeypatch):
|
|
75
|
+
monkeypatch.setattr(doctor, "_ovfetch", lambda has_npu: [])
|
|
76
|
+
code = doctor.main(["--json"])
|
|
77
|
+
report = json.loads(capsys.readouterr().out)
|
|
78
|
+
assert code == 0
|
|
79
|
+
assert report["failed"] == []
|
|
80
|
+
assert by_label([doctor.Result(**c) for c in report["checks"]], "vinospeak").status == doctor.OK
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_exit_status_is_1_when_something_failed(capsys, monkeypatch):
|
|
84
|
+
monkeypatch.setattr(doctor, "_distribution", lambda name: None)
|
|
85
|
+
assert doctor.main([]) == 1
|
|
86
|
+
assert "failed" in capsys.readouterr().out
|