ollama-speak 1.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.
- ollama_speak-1.1.0/.github/workflows/release.yml +31 -0
- ollama_speak-1.1.0/.gitignore +29 -0
- ollama_speak-1.1.0/LICENSE +21 -0
- ollama_speak-1.1.0/PKG-INFO +230 -0
- ollama_speak-1.1.0/README.md +208 -0
- ollama_speak-1.1.0/install.ps1 +65 -0
- ollama_speak-1.1.0/install.sh +118 -0
- ollama_speak-1.1.0/pyproject.toml +41 -0
- ollama_speak-1.1.0/requirements.txt +4 -0
- ollama_speak-1.1.0/src/ollama_speak/__init__.py +5 -0
- ollama_speak-1.1.0/src/ollama_speak/__main__.py +4 -0
- ollama_speak-1.1.0/src/ollama_speak/cli.py +100 -0
- ollama_speak-1.1.0/src/ollama_speak/voice.py +371 -0
- ollama_speak-1.1.0/verify.py +89 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Build, Package & Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-pypi:
|
|
14
|
+
name: Build & Publish to PyPI
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.11'
|
|
22
|
+
- name: Install build tools
|
|
23
|
+
run: pip install --upgrade build twine
|
|
24
|
+
- name: Build wheel and sdist
|
|
25
|
+
run: python -m build
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
28
|
+
env:
|
|
29
|
+
TWINE_USERNAME: __token__
|
|
30
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
31
|
+
run: twine upload --skip-existing dist/*
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
share/python-wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
.env
|
|
23
|
+
.venv
|
|
24
|
+
env/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env.bak/
|
|
28
|
+
venv.bak/
|
|
29
|
+
.git/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cicada & Antigravity Community
|
|
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,230 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ollama-speak
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: 100% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M & Piper).
|
|
5
|
+
Project-URL: Homepage, https://github.com/AjarnSpencer/ollama-speak
|
|
6
|
+
Project-URL: Repository, https://github.com/AjarnSpencer/ollama-speak
|
|
7
|
+
Project-URL: Issues, https://github.com/AjarnSpencer/ollama-speak/issues
|
|
8
|
+
Author-email: Ajarn Spencer Littlewood <ajarnspencer@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: kokoro>=0.8.4
|
|
13
|
+
Requires-Dist: numpy>=1.21.0
|
|
14
|
+
Requires-Dist: soundfile>=0.12.0
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: kokoro>=0.8.4; extra == 'all'
|
|
17
|
+
Requires-Dist: piper-tts>=1.2.0; extra == 'all'
|
|
18
|
+
Requires-Dist: soundfile>=0.12.0; extra == 'all'
|
|
19
|
+
Provides-Extra: piper
|
|
20
|
+
Requires-Dist: piper-tts>=1.2.0; extra == 'piper'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# 🗣️ ollama-speak (`ospeak`)
|
|
24
|
+
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
[](https://www.python.org/)
|
|
27
|
+
[](https://ollama.ai/)
|
|
28
|
+
[](#100-offline-and-sovereign-guarantee)
|
|
29
|
+
|
|
30
|
+
> **100% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M and Piper ONNX).**
|
|
31
|
+
|
|
32
|
+
`ollama-speak` (`ospeak`) is a standalone, sovereign voice engine built specifically for the local AI and Ollama community. It runs **100% locally on your own machine** using open-weights models:
|
|
33
|
+
|
|
34
|
+
- **Kokoro-82M (Default):** Apache 2.0 open-weight 82M-parameter neural model providing studio-grade broadcast speech.
|
|
35
|
+
- **Piper TTS (Ultra-lightweight):** Fast ONNX neural voice engine designed to run with sub-100ms latency even on low-spec hardware or a Raspberry Pi.
|
|
36
|
+
|
|
37
|
+
Zero cloud API calls, zero telemetry, zero corporate tracking, and zero accounts. Once local weights are downloaded, `ollama-speak` works completely with your internet disconnected.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 💻 Minimum System and Hardware Requirements
|
|
42
|
+
|
|
43
|
+
`ollama-speak` is designed to run smoothly across consumer laptops, older desktop PCs, MacBooks, and high-performance workstations.
|
|
44
|
+
|
|
45
|
+
### Hardware Requirements Table
|
|
46
|
+
|
|
47
|
+
| Component | Piper ONNX (Ultra-Lightweight) | Kokoro-82M (Studio Quality Default) |
|
|
48
|
+
| :--- | :--- | :--- |
|
|
49
|
+
| **CPU** | Any 64-bit CPU (Intel, AMD, ARM64 / Apple Silicon, Raspberry Pi 4/5) | Modern multi-core CPU (AVX2 recommended) or Apple Silicon M1–M4 |
|
|
50
|
+
| **GPU (Optional)** | Not needed (pure CPU ONNX) | Accelerated on CUDA / ROCm / Metal, runs easily on CPU |
|
|
51
|
+
| **System RAM** | **1 GB minimum** (uses ~80 MB during inference) | **2 GB to 4 GB RAM** (uses ~350 MB during inference) |
|
|
52
|
+
| **Disk Space** | **~35 MB** for voice model | **~350 MB** for open-weight model checkpoint |
|
|
53
|
+
| **Audio System** | Standard OS sound (ALSA, PulseAudio, PipeWire, CoreAudio, WASAPI) | Standard OS sound |
|
|
54
|
+
| **Internet** | Only required once for initial model download | Only required once for initial model download |
|
|
55
|
+
|
|
56
|
+
### Supported Operating Systems
|
|
57
|
+
|
|
58
|
+
- **Linux:** Ubuntu 20.04+, Debian 11+, Fedora 38+, Arch Linux, DGX OS (x86_64 and ARM64).
|
|
59
|
+
- **macOS:** macOS 11 (Big Sur) and newer (Intel and Apple Silicon M1/M2/M3/M4). Uses native `afplay`.
|
|
60
|
+
- **Windows:** Windows 10 & 11 (64-bit). Uses native PowerShell / System.Media audio backend.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ✨ Features
|
|
65
|
+
|
|
66
|
+
- **100% Sovereign and Offline:** No queries leave your computer. Never sends your text to Microsoft, Google, Bing, or any remote server.
|
|
67
|
+
- **Dual Local Engine Architecture:**
|
|
68
|
+
- **Kokoro-82M:** Deeply expressive, human-like voice with natural breath pacing and emotional cadence.
|
|
69
|
+
- **Piper ONNX:** Blazing fast sub-100ms latency for real-time voice assistants.
|
|
70
|
+
- **Self-Contained Model Downloader:** Run `ollama-speak --setup` to pre-download open weights once, or let it fetch them on first run. Once cached, it runs offline indefinitely.
|
|
71
|
+
- **Zero-Swallow Pipe Echo:** When piping text from any command (`ollama run llama3 "..." | ollama-speak`), it mirrors text directly to your terminal so your transcript remains visible and copy-pasteable.
|
|
72
|
+
- **Zero-Flag Pairing with `ollama-web`:** Simply run `ollama-web "your question"`. The text streams live to your terminal while local voice speaks the answer automatically.
|
|
73
|
+
- **Native Cross-Platform Playback:** Automatically utilizes local audio players (`pw-play`, `paplay`, `mpv`, `ffplay`, `afplay` on macOS, PowerShell on Windows).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🚀 Installation
|
|
78
|
+
|
|
79
|
+
### Option 1 (Recommended) — One-Line Turnkey Installer
|
|
80
|
+
Works automatically on fresh Linux, macOS, and WSL systems without requiring manual virtual environments or encountering PEP 668 system python locks.
|
|
81
|
+
|
|
82
|
+
**Linux and macOS:**
|
|
83
|
+
```bash
|
|
84
|
+
curl -fsSL https://raw.githubusercontent.com/AjarnSpencer/ollama-speak/main/install.sh | bash
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Windows (PowerShell):**
|
|
88
|
+
```powershell
|
|
89
|
+
irm https://raw.githubusercontent.com/AjarnSpencer/ollama-speak/main/install.ps1 | iex
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
What the turnkey installer does:
|
|
93
|
+
- Sets up an isolated virtual environment in `~/.local/share/ollama-speak/venv` (zero system pollution).
|
|
94
|
+
- Symlinks `ollama-speak` and `ospeak` launchers into `~/.local/bin/`.
|
|
95
|
+
- Configures user `PATH` if not already set.
|
|
96
|
+
- Automatically pre-downloads local model weights so it works offline immediately.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Option 2 — Install from GitHub with pipx or pip
|
|
101
|
+
If you prefer managing Python CLI tools via `pipx` (recommended by Python Packaging Authority for standalone tools) or an active virtual environment:
|
|
102
|
+
|
|
103
|
+
**Using pipx (Recommended for Python users):**
|
|
104
|
+
```bash
|
|
105
|
+
pipx install git+https://github.com/AjarnSpencer/ollama-speak.git
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Using pip in a virtual environment:**
|
|
109
|
+
```bash
|
|
110
|
+
pip install git+https://github.com/AjarnSpencer/ollama-speak.git
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### Option 3 — Clone and Install Locally
|
|
116
|
+
```bash
|
|
117
|
+
git clone https://github.com/AjarnSpencer/ollama-speak.git
|
|
118
|
+
cd ollama-speak
|
|
119
|
+
./install.sh
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Pre-Download Local Models for 100% Offline Use
|
|
125
|
+
```bash
|
|
126
|
+
ollama-speak --setup
|
|
127
|
+
```
|
|
128
|
+
This downloads and caches the Kokoro-82M and Piper open weights into your local user directory. After this step, you can disconnect your computer from the internet completely.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### 📦 Publishing to PyPI (For Maintainers)
|
|
133
|
+
To unlock global installation via `pip install ollama-speak` worldwide:
|
|
134
|
+
1. Build distributions: `python3 -m build` (generates wheels and tarballs in `dist/`).
|
|
135
|
+
2. Upload to PyPI: `twine upload dist/*` with your PyPI token.
|
|
136
|
+
Once published, users can install directly via `pip install --upgrade ollama-speak`.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 📖 Command Line Reference and Flags
|
|
141
|
+
|
|
142
|
+
`ollama-speak` can be invoked as either `ollama-speak` or `ospeak`.
|
|
143
|
+
|
|
144
|
+
| Flag | Short | Default | Description |
|
|
145
|
+
| :--- | :--- | :--- | :--- |
|
|
146
|
+
| `text` | | STDIN | Text string to speak aloud (reads from STDIN pipe if omitted) |
|
|
147
|
+
| `--engine ENGINE` | `-e` | `kokoro` | Local TTS engine: `kokoro` (studio quality) or `piper` (ultra-fast ONNX) |
|
|
148
|
+
| `--kokoro` | | | Shortcut for `--engine kokoro` |
|
|
149
|
+
| `--piper` | | | Shortcut for `--engine piper` |
|
|
150
|
+
| `--voice VOICE` | `-v` | `bm_george` | Select specific local voice (e.g. `bm_george`, `af_heart`, `bf_alice`) |
|
|
151
|
+
| `--output FILE` | `-o` | `None` | Save spoken audio to a local WAV/MP3 file instead of playing |
|
|
152
|
+
| `--setup` | `--download-models` | | Pre-download all local weights for 100% offline use |
|
|
153
|
+
| `--list-voices` | | | List all available local open-weight voices and aliases |
|
|
154
|
+
| `-h`, `--help` | | | Show full help manual and flags |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🎙️ Supported Local Open-Weight Voices
|
|
159
|
+
|
|
160
|
+
Run `ollama-speak --list-voices` to see all local voices:
|
|
161
|
+
|
|
162
|
+
| Voice Name | Language | Gender / Style | Engine |
|
|
163
|
+
| :--- | :--- | :--- | :--- |
|
|
164
|
+
| **`bm_george` (Default)** | British English | Male (Warm Baritone) | Kokoro-82M |
|
|
165
|
+
| **`bm_lewis`** | British English | Male | Kokoro-82M |
|
|
166
|
+
| **`bf_alice`** | British English | Female | Kokoro-82M |
|
|
167
|
+
| **`af_heart`** | American English | Female (Warm) | Kokoro-82M |
|
|
168
|
+
| **`am_adam`** | American English | Male | Kokoro-82M |
|
|
169
|
+
| **`jf_tebukuro`** | Japanese | Female | Kokoro-82M |
|
|
170
|
+
| **`zf_xiaobei`** | Mandarin Chinese | Female | Kokoro-82M |
|
|
171
|
+
| **`ef_dora`** | Spanish | Female | Kokoro-82M |
|
|
172
|
+
| **`ff_siwis`** | French | Female | Kokoro-82M |
|
|
173
|
+
| **`if_sara`** | Italian | Female | Kokoro-82M |
|
|
174
|
+
| **`pf_dora`** | Portuguese | Female | Kokoro-82M |
|
|
175
|
+
| **`hf_alpha`** | Hindi | Female | Kokoro-82M |
|
|
176
|
+
| **`piper`** | British English | Clear / Fast ONNX | Piper |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 💡 Practical Tutorials and Workflows
|
|
181
|
+
|
|
182
|
+
### Tutorial 1 — Direct Local Text Narration
|
|
183
|
+
Speak any text locally using your default Kokoro voice:
|
|
184
|
+
```bash
|
|
185
|
+
ollama-speak "All local systems are operational and running completely offline."
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Tutorial 2 — Zero-Swallow Piped Input from Local Ollama
|
|
189
|
+
Pipe output from any local Ollama model directly into `ollama-speak`. The text prints on screen in real-time, and local voice speaks the answer:
|
|
190
|
+
```bash
|
|
191
|
+
ollama run llama3 "Explain quantum computing in two sentences" | ollama-speak
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Tutorial 3 — Fast Piper ONNX for Low-Spec PCs
|
|
195
|
+
If you are running on a Raspberry Pi or low-spec laptop, pass `--piper` for ultra-fast, lightweight inference:
|
|
196
|
+
```bash
|
|
197
|
+
ollama run llama3 "Give me one piece of life advice" | ollama-speak --piper
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Tutorial 4 — Changing Local Voices
|
|
201
|
+
Choose between different genders, accents, and languages:
|
|
202
|
+
```bash
|
|
203
|
+
# American Female (Kokoro-82M):
|
|
204
|
+
ollama-speak -v af_heart "Welcome to the future of sovereign, local artificial intelligence."
|
|
205
|
+
|
|
206
|
+
# British Female (Kokoro-82M):
|
|
207
|
+
ollama-speak -v bf_alice "It is a pleasure to assist you today."
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Tutorial 5 — Exporting Audio to File
|
|
211
|
+
Save the synthesized speech directly to a standard audio file:
|
|
212
|
+
```bash
|
|
213
|
+
ollama-speak -o ~/Music/summary.wav "Your daily briefing has been saved locally."
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 🔒 100% Offline and Sovereign Guarantee
|
|
219
|
+
|
|
220
|
+
- **No Remote Servers:** Text is synthesized entirely inside your computer's RAM using open-weights models.
|
|
221
|
+
- **No Cloud Fallbacks:** It will never silently phone home to Microsoft, Bing, Google, or any cloud API.
|
|
222
|
+
- **No API Keys or Subscriptions:** You own your hardware, and you own your voice assistant.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 📄 License
|
|
227
|
+
|
|
228
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
229
|
+
Kokoro-82M model weights are released under the [Apache 2.0 License](https://huggingface.co/hexgrad/Kokoro-82M).
|
|
230
|
+
Piper TTS is released under the [MIT License](https://github.com/rhasspy/piper).
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# 🗣️ ollama-speak (`ospeak`)
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](https://ollama.ai/)
|
|
6
|
+
[](#100-offline-and-sovereign-guarantee)
|
|
7
|
+
|
|
8
|
+
> **100% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M and Piper ONNX).**
|
|
9
|
+
|
|
10
|
+
`ollama-speak` (`ospeak`) is a standalone, sovereign voice engine built specifically for the local AI and Ollama community. It runs **100% locally on your own machine** using open-weights models:
|
|
11
|
+
|
|
12
|
+
- **Kokoro-82M (Default):** Apache 2.0 open-weight 82M-parameter neural model providing studio-grade broadcast speech.
|
|
13
|
+
- **Piper TTS (Ultra-lightweight):** Fast ONNX neural voice engine designed to run with sub-100ms latency even on low-spec hardware or a Raspberry Pi.
|
|
14
|
+
|
|
15
|
+
Zero cloud API calls, zero telemetry, zero corporate tracking, and zero accounts. Once local weights are downloaded, `ollama-speak` works completely with your internet disconnected.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 💻 Minimum System and Hardware Requirements
|
|
20
|
+
|
|
21
|
+
`ollama-speak` is designed to run smoothly across consumer laptops, older desktop PCs, MacBooks, and high-performance workstations.
|
|
22
|
+
|
|
23
|
+
### Hardware Requirements Table
|
|
24
|
+
|
|
25
|
+
| Component | Piper ONNX (Ultra-Lightweight) | Kokoro-82M (Studio Quality Default) |
|
|
26
|
+
| :--- | :--- | :--- |
|
|
27
|
+
| **CPU** | Any 64-bit CPU (Intel, AMD, ARM64 / Apple Silicon, Raspberry Pi 4/5) | Modern multi-core CPU (AVX2 recommended) or Apple Silicon M1–M4 |
|
|
28
|
+
| **GPU (Optional)** | Not needed (pure CPU ONNX) | Accelerated on CUDA / ROCm / Metal, runs easily on CPU |
|
|
29
|
+
| **System RAM** | **1 GB minimum** (uses ~80 MB during inference) | **2 GB to 4 GB RAM** (uses ~350 MB during inference) |
|
|
30
|
+
| **Disk Space** | **~35 MB** for voice model | **~350 MB** for open-weight model checkpoint |
|
|
31
|
+
| **Audio System** | Standard OS sound (ALSA, PulseAudio, PipeWire, CoreAudio, WASAPI) | Standard OS sound |
|
|
32
|
+
| **Internet** | Only required once for initial model download | Only required once for initial model download |
|
|
33
|
+
|
|
34
|
+
### Supported Operating Systems
|
|
35
|
+
|
|
36
|
+
- **Linux:** Ubuntu 20.04+, Debian 11+, Fedora 38+, Arch Linux, DGX OS (x86_64 and ARM64).
|
|
37
|
+
- **macOS:** macOS 11 (Big Sur) and newer (Intel and Apple Silicon M1/M2/M3/M4). Uses native `afplay`.
|
|
38
|
+
- **Windows:** Windows 10 & 11 (64-bit). Uses native PowerShell / System.Media audio backend.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Features
|
|
43
|
+
|
|
44
|
+
- **100% Sovereign and Offline:** No queries leave your computer. Never sends your text to Microsoft, Google, Bing, or any remote server.
|
|
45
|
+
- **Dual Local Engine Architecture:**
|
|
46
|
+
- **Kokoro-82M:** Deeply expressive, human-like voice with natural breath pacing and emotional cadence.
|
|
47
|
+
- **Piper ONNX:** Blazing fast sub-100ms latency for real-time voice assistants.
|
|
48
|
+
- **Self-Contained Model Downloader:** Run `ollama-speak --setup` to pre-download open weights once, or let it fetch them on first run. Once cached, it runs offline indefinitely.
|
|
49
|
+
- **Zero-Swallow Pipe Echo:** When piping text from any command (`ollama run llama3 "..." | ollama-speak`), it mirrors text directly to your terminal so your transcript remains visible and copy-pasteable.
|
|
50
|
+
- **Zero-Flag Pairing with `ollama-web`:** Simply run `ollama-web "your question"`. The text streams live to your terminal while local voice speaks the answer automatically.
|
|
51
|
+
- **Native Cross-Platform Playback:** Automatically utilizes local audio players (`pw-play`, `paplay`, `mpv`, `ffplay`, `afplay` on macOS, PowerShell on Windows).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🚀 Installation
|
|
56
|
+
|
|
57
|
+
### Option 1 (Recommended) — One-Line Turnkey Installer
|
|
58
|
+
Works automatically on fresh Linux, macOS, and WSL systems without requiring manual virtual environments or encountering PEP 668 system python locks.
|
|
59
|
+
|
|
60
|
+
**Linux and macOS:**
|
|
61
|
+
```bash
|
|
62
|
+
curl -fsSL https://raw.githubusercontent.com/AjarnSpencer/ollama-speak/main/install.sh | bash
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Windows (PowerShell):**
|
|
66
|
+
```powershell
|
|
67
|
+
irm https://raw.githubusercontent.com/AjarnSpencer/ollama-speak/main/install.ps1 | iex
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
What the turnkey installer does:
|
|
71
|
+
- Sets up an isolated virtual environment in `~/.local/share/ollama-speak/venv` (zero system pollution).
|
|
72
|
+
- Symlinks `ollama-speak` and `ospeak` launchers into `~/.local/bin/`.
|
|
73
|
+
- Configures user `PATH` if not already set.
|
|
74
|
+
- Automatically pre-downloads local model weights so it works offline immediately.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Option 2 — Install from GitHub with pipx or pip
|
|
79
|
+
If you prefer managing Python CLI tools via `pipx` (recommended by Python Packaging Authority for standalone tools) or an active virtual environment:
|
|
80
|
+
|
|
81
|
+
**Using pipx (Recommended for Python users):**
|
|
82
|
+
```bash
|
|
83
|
+
pipx install git+https://github.com/AjarnSpencer/ollama-speak.git
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Using pip in a virtual environment:**
|
|
87
|
+
```bash
|
|
88
|
+
pip install git+https://github.com/AjarnSpencer/ollama-speak.git
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Option 3 — Clone and Install Locally
|
|
94
|
+
```bash
|
|
95
|
+
git clone https://github.com/AjarnSpencer/ollama-speak.git
|
|
96
|
+
cd ollama-speak
|
|
97
|
+
./install.sh
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Pre-Download Local Models for 100% Offline Use
|
|
103
|
+
```bash
|
|
104
|
+
ollama-speak --setup
|
|
105
|
+
```
|
|
106
|
+
This downloads and caches the Kokoro-82M and Piper open weights into your local user directory. After this step, you can disconnect your computer from the internet completely.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 📦 Publishing to PyPI (For Maintainers)
|
|
111
|
+
To unlock global installation via `pip install ollama-speak` worldwide:
|
|
112
|
+
1. Build distributions: `python3 -m build` (generates wheels and tarballs in `dist/`).
|
|
113
|
+
2. Upload to PyPI: `twine upload dist/*` with your PyPI token.
|
|
114
|
+
Once published, users can install directly via `pip install --upgrade ollama-speak`.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 📖 Command Line Reference and Flags
|
|
119
|
+
|
|
120
|
+
`ollama-speak` can be invoked as either `ollama-speak` or `ospeak`.
|
|
121
|
+
|
|
122
|
+
| Flag | Short | Default | Description |
|
|
123
|
+
| :--- | :--- | :--- | :--- |
|
|
124
|
+
| `text` | | STDIN | Text string to speak aloud (reads from STDIN pipe if omitted) |
|
|
125
|
+
| `--engine ENGINE` | `-e` | `kokoro` | Local TTS engine: `kokoro` (studio quality) or `piper` (ultra-fast ONNX) |
|
|
126
|
+
| `--kokoro` | | | Shortcut for `--engine kokoro` |
|
|
127
|
+
| `--piper` | | | Shortcut for `--engine piper` |
|
|
128
|
+
| `--voice VOICE` | `-v` | `bm_george` | Select specific local voice (e.g. `bm_george`, `af_heart`, `bf_alice`) |
|
|
129
|
+
| `--output FILE` | `-o` | `None` | Save spoken audio to a local WAV/MP3 file instead of playing |
|
|
130
|
+
| `--setup` | `--download-models` | | Pre-download all local weights for 100% offline use |
|
|
131
|
+
| `--list-voices` | | | List all available local open-weight voices and aliases |
|
|
132
|
+
| `-h`, `--help` | | | Show full help manual and flags |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🎙️ Supported Local Open-Weight Voices
|
|
137
|
+
|
|
138
|
+
Run `ollama-speak --list-voices` to see all local voices:
|
|
139
|
+
|
|
140
|
+
| Voice Name | Language | Gender / Style | Engine |
|
|
141
|
+
| :--- | :--- | :--- | :--- |
|
|
142
|
+
| **`bm_george` (Default)** | British English | Male (Warm Baritone) | Kokoro-82M |
|
|
143
|
+
| **`bm_lewis`** | British English | Male | Kokoro-82M |
|
|
144
|
+
| **`bf_alice`** | British English | Female | Kokoro-82M |
|
|
145
|
+
| **`af_heart`** | American English | Female (Warm) | Kokoro-82M |
|
|
146
|
+
| **`am_adam`** | American English | Male | Kokoro-82M |
|
|
147
|
+
| **`jf_tebukuro`** | Japanese | Female | Kokoro-82M |
|
|
148
|
+
| **`zf_xiaobei`** | Mandarin Chinese | Female | Kokoro-82M |
|
|
149
|
+
| **`ef_dora`** | Spanish | Female | Kokoro-82M |
|
|
150
|
+
| **`ff_siwis`** | French | Female | Kokoro-82M |
|
|
151
|
+
| **`if_sara`** | Italian | Female | Kokoro-82M |
|
|
152
|
+
| **`pf_dora`** | Portuguese | Female | Kokoro-82M |
|
|
153
|
+
| **`hf_alpha`** | Hindi | Female | Kokoro-82M |
|
|
154
|
+
| **`piper`** | British English | Clear / Fast ONNX | Piper |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 💡 Practical Tutorials and Workflows
|
|
159
|
+
|
|
160
|
+
### Tutorial 1 — Direct Local Text Narration
|
|
161
|
+
Speak any text locally using your default Kokoro voice:
|
|
162
|
+
```bash
|
|
163
|
+
ollama-speak "All local systems are operational and running completely offline."
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Tutorial 2 — Zero-Swallow Piped Input from Local Ollama
|
|
167
|
+
Pipe output from any local Ollama model directly into `ollama-speak`. The text prints on screen in real-time, and local voice speaks the answer:
|
|
168
|
+
```bash
|
|
169
|
+
ollama run llama3 "Explain quantum computing in two sentences" | ollama-speak
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Tutorial 3 — Fast Piper ONNX for Low-Spec PCs
|
|
173
|
+
If you are running on a Raspberry Pi or low-spec laptop, pass `--piper` for ultra-fast, lightweight inference:
|
|
174
|
+
```bash
|
|
175
|
+
ollama run llama3 "Give me one piece of life advice" | ollama-speak --piper
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Tutorial 4 — Changing Local Voices
|
|
179
|
+
Choose between different genders, accents, and languages:
|
|
180
|
+
```bash
|
|
181
|
+
# American Female (Kokoro-82M):
|
|
182
|
+
ollama-speak -v af_heart "Welcome to the future of sovereign, local artificial intelligence."
|
|
183
|
+
|
|
184
|
+
# British Female (Kokoro-82M):
|
|
185
|
+
ollama-speak -v bf_alice "It is a pleasure to assist you today."
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Tutorial 5 — Exporting Audio to File
|
|
189
|
+
Save the synthesized speech directly to a standard audio file:
|
|
190
|
+
```bash
|
|
191
|
+
ollama-speak -o ~/Music/summary.wav "Your daily briefing has been saved locally."
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🔒 100% Offline and Sovereign Guarantee
|
|
197
|
+
|
|
198
|
+
- **No Remote Servers:** Text is synthesized entirely inside your computer's RAM using open-weights models.
|
|
199
|
+
- **No Cloud Fallbacks:** It will never silently phone home to Microsoft, Bing, Google, or any cloud API.
|
|
200
|
+
- **No API Keys or Subscriptions:** You own your hardware, and you own your voice assistant.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 📄 License
|
|
205
|
+
|
|
206
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
207
|
+
Kokoro-82M model weights are released under the [Apache 2.0 License](https://huggingface.co/hexgrad/Kokoro-82M).
|
|
208
|
+
Piper TTS is released under the [MIT License](https://github.com/rhasspy/piper).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ollama-speak turnkey isolated installer for Windows PowerShell
|
|
2
|
+
# 100% Local Neural Voice CLI (Kokoro-82M and Piper ONNX)
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
Write-Host "==================================================" -ForegroundColor Cyan
|
|
6
|
+
Write-Host " 🗣️ Installing ollama-speak for Windows (Isolated)" -ForegroundColor Cyan
|
|
7
|
+
Write-Host "==================================================" -ForegroundColor Cyan
|
|
8
|
+
|
|
9
|
+
# 1. Detect Python
|
|
10
|
+
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
|
11
|
+
if (-not $pythonCmd) {
|
|
12
|
+
$pythonCmd = Get-Command py -ErrorAction SilentlyContinue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (-not $pythonCmd) {
|
|
16
|
+
Write-Host "[!] Error: Python 3.9+ was not found on your system PATH." -ForegroundColor Red
|
|
17
|
+
Write-Host "Please install Python from https://www.python.org/ (ensure 'Add Python to PATH' is checked)." -ForegroundColor Yellow
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$InstallDir = Join-Path $env:LOCALAPPDATA "ollama-speak"
|
|
22
|
+
$VenvDir = Join-Path $InstallDir "venv"
|
|
23
|
+
$VenvPython = Join-Path $VenvDir "Scripts\python.exe"
|
|
24
|
+
$VenvPip = Join-Path $VenvDir "Scripts\pip.exe"
|
|
25
|
+
$VenvScripts = Join-Path $VenvDir "Scripts"
|
|
26
|
+
|
|
27
|
+
# 2. Create isolated venv
|
|
28
|
+
Write-Host "→ Creating isolated virtual environment in $VenvDir..." -ForegroundColor Green
|
|
29
|
+
if (-not (Test-Path $VenvDir)) {
|
|
30
|
+
& $pythonCmd.Source -m venv $VenvDir
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# 3. Upgrade pip and install package
|
|
34
|
+
Write-Host "→ Installing dependencies in isolated environment..." -ForegroundColor Green
|
|
35
|
+
& $VenvPip install --quiet --upgrade pip wheel setuptools
|
|
36
|
+
|
|
37
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
38
|
+
if (Test-Path (Join-Path $ScriptDir "pyproject.toml")) {
|
|
39
|
+
Write-Host "→ Installing from local directory ($ScriptDir)..." -ForegroundColor Green
|
|
40
|
+
& $VenvPip install --upgrade $ScriptDir
|
|
41
|
+
} else {
|
|
42
|
+
Write-Host "→ Installing from GitHub..." -ForegroundColor Green
|
|
43
|
+
& $VenvPip install --upgrade "git+https://github.com/AjarnSpencer/ollama-speak.git"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 4. Add Scripts to User PATH
|
|
47
|
+
$UserPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User)
|
|
48
|
+
if ($UserPath -notlike "*$VenvScripts*") {
|
|
49
|
+
Write-Host "→ Adding $VenvScripts to User PATH..." -ForegroundColor Green
|
|
50
|
+
[Environment]::SetEnvironmentVariable("Path", "$UserPath;$VenvScripts", [EnvironmentVariableTarget]::User)
|
|
51
|
+
$env:Path = "$env:Path;$VenvScripts"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# 5. Pre-download open weights
|
|
55
|
+
$OllamaSpeakExe = Join-Path $VenvScripts "ollama-speak.exe"
|
|
56
|
+
if (Test-Path $OllamaSpeakExe) {
|
|
57
|
+
Write-Host "→ Pre-downloading open weights for 100% offline use..." -ForegroundColor Green
|
|
58
|
+
& $OllamaSpeakExe --setup
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Write-Host "==================================================" -ForegroundColor Cyan
|
|
62
|
+
Write-Host " ✓ Installation complete!" -ForegroundColor Green
|
|
63
|
+
Write-Host " Run: 'ollama-speak ""Hello from local voice""'" -ForegroundColor White
|
|
64
|
+
Write-Host " Or shortcut: 'ospeak ""Fast voice""'" -ForegroundColor White
|
|
65
|
+
Write-Host "==================================================" -ForegroundColor Cyan
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ollama-speak turnkey isolated installer for Linux and macOS
|
|
3
|
+
# 100% Local Neural Voice CLI (Kokoro-82M and Piper ONNX)
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
APP_NAME="ollama-speak"
|
|
7
|
+
INSTALL_DIR="${HOME}/.local/share/${APP_NAME}"
|
|
8
|
+
BIN_DIR="${HOME}/.local/bin"
|
|
9
|
+
REPO_URL="https://github.com/AjarnSpencer/ollama-speak.git"
|
|
10
|
+
|
|
11
|
+
echo "=================================================="
|
|
12
|
+
echo " 🗣️ Installing ${APP_NAME} (Local Neural Voice CLI)"
|
|
13
|
+
echo "=================================================="
|
|
14
|
+
|
|
15
|
+
# 1. Detect Python 3.9+
|
|
16
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
PY_SYSTEM="python3"
|
|
18
|
+
elif command -v python >/dev/null 2>&1; then
|
|
19
|
+
PY_SYSTEM="python"
|
|
20
|
+
else
|
|
21
|
+
echo "❌ Error: Python 3.9+ is required but not installed." >&2
|
|
22
|
+
echo " Ubuntu/Debian: sudo apt update && sudo apt install -y python3 python3-venv python3-pip" >&2
|
|
23
|
+
echo " Fedora/RHEL: sudo dnf install -y python3" >&2
|
|
24
|
+
echo " macOS: brew install python" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Verify Python version >= 3.9
|
|
29
|
+
PY_VER=$($PY_SYSTEM -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
|
30
|
+
PY_MAJOR=$($PY_SYSTEM -c "import sys; print(sys.version_info.major)")
|
|
31
|
+
PY_MINOR=$($PY_SYSTEM -c "import sys; print(sys.version_info.minor)")
|
|
32
|
+
if [ "$PY_MAJOR" -lt 3 ] || ([ "$PY_MAJOR" -eq 3 ] && [ "$PY_MINOR" -lt 9 ]); then
|
|
33
|
+
echo "❌ Error: Python 3.9+ required. Found Python ${PY_VER}." >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
echo "✓ Detected Python ${PY_VER} (${PY_SYSTEM})"
|
|
37
|
+
|
|
38
|
+
# 2. Check for python3-venv support
|
|
39
|
+
if ! $PY_SYSTEM -m venv --help >/dev/null 2>&1; then
|
|
40
|
+
echo "❌ Error: The 'venv' module is missing from your Python installation." >&2
|
|
41
|
+
echo " On Ubuntu/Debian, run: sudo apt install -y python3-venv" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# 3. Create dedicated isolated virtual environment (bypasses PEP 668 cleanly)
|
|
46
|
+
echo "→ Setting up isolated virtual environment in ${INSTALL_DIR}/venv..."
|
|
47
|
+
mkdir -p "${INSTALL_DIR}"
|
|
48
|
+
$PY_SYSTEM -m venv "${INSTALL_DIR}/venv"
|
|
49
|
+
VENV_PY="${INSTALL_DIR}/venv/bin/python"
|
|
50
|
+
VENV_PIP="${INSTALL_DIR}/venv/bin/pip"
|
|
51
|
+
|
|
52
|
+
# Upgrade pip and wheel in isolated venv
|
|
53
|
+
"${VENV_PIP}" install --quiet --upgrade pip wheel setuptools
|
|
54
|
+
|
|
55
|
+
# 4. Install the package
|
|
56
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
57
|
+
if [ -f "${SCRIPT_DIR}/pyproject.toml" ]; then
|
|
58
|
+
echo "→ Installing from local source (${SCRIPT_DIR})..."
|
|
59
|
+
"${VENV_PIP}" install --upgrade "${SCRIPT_DIR}"
|
|
60
|
+
else
|
|
61
|
+
echo "→ Installing latest release from GitHub (${REPO_URL})..."
|
|
62
|
+
"${VENV_PIP}" install --upgrade "git+${REPO_URL}"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# 5. Create symlinks in ~/.local/bin
|
|
66
|
+
mkdir -p "${BIN_DIR}"
|
|
67
|
+
ln -sf "${INSTALL_DIR}/venv/bin/ollama-speak" "${BIN_DIR}/ollama-speak"
|
|
68
|
+
ln -sf "${INSTALL_DIR}/venv/bin/ospeak" "${BIN_DIR}/ospeak"
|
|
69
|
+
echo "✓ Created launcher symlinks in ${BIN_DIR}"
|
|
70
|
+
|
|
71
|
+
# 6. Ensure ~/.local/bin is on PATH
|
|
72
|
+
PATH_CONFIGURED=false
|
|
73
|
+
case ":$PATH:" in
|
|
74
|
+
*":${BIN_DIR}:"*) PATH_CONFIGURED=true ;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
77
|
+
if [ "$PATH_CONFIGURED" = false ]; then
|
|
78
|
+
echo "→ Adding ${BIN_DIR} to your shell profile..."
|
|
79
|
+
SHELL_NAME=$(basename "${SHELL:-bash}")
|
|
80
|
+
RC_FILE=""
|
|
81
|
+
if [ "$SHELL_NAME" = "zsh" ]; then
|
|
82
|
+
RC_FILE="${HOME}/.zshrc"
|
|
83
|
+
elif [ "$SHELL_NAME" = "bash" ]; then
|
|
84
|
+
if [ -f "${HOME}/.bashrc" ]; then
|
|
85
|
+
RC_FILE="${HOME}/.bashrc"
|
|
86
|
+
elif [ -f "${HOME}/.bash_profile" ]; then
|
|
87
|
+
RC_FILE="${HOME}/.bash_profile"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
if [ -n "$RC_FILE" ] && [ -w "$RC_FILE" ]; then
|
|
91
|
+
if ! grep -q ".local/bin" "$RC_FILE"; then
|
|
92
|
+
echo '' >> "$RC_FILE"
|
|
93
|
+
echo '# Added by ollama-speak installer' >> "$RC_FILE"
|
|
94
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$RC_FILE"
|
|
95
|
+
echo "✓ Added PATH export to ${RC_FILE}"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# 7. Pre-download open weights for 100% offline use
|
|
101
|
+
echo "→ Pre-downloading open weights for 100% offline use..."
|
|
102
|
+
"${BIN_DIR}/ollama-speak" --setup || true
|
|
103
|
+
|
|
104
|
+
echo "=================================================="
|
|
105
|
+
echo " 🎉 Installation Complete!"
|
|
106
|
+
echo "=================================================="
|
|
107
|
+
echo " You can now run:"
|
|
108
|
+
echo " ollama-speak \"Hello from local voice\""
|
|
109
|
+
echo " ospeak \"Fast shortcut\""
|
|
110
|
+
echo " Or pipe directly from Ollama:"
|
|
111
|
+
echo " ollama run llama3 \"Tell me a one-liner\" | ollama-speak"
|
|
112
|
+
if [ "$PATH_CONFIGURED" = false ]; then
|
|
113
|
+
echo ""
|
|
114
|
+
echo " ⚠️ Note: If 'ollama-speak' is not recognized immediately in your current terminal,"
|
|
115
|
+
echo " run: export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
116
|
+
echo " or restart your terminal."
|
|
117
|
+
fi
|
|
118
|
+
echo "=================================================="
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ollama-speak"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "100% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M & Piper)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Ajarn Spencer Littlewood", email = "ajarnspencer@gmail.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"kokoro>=0.8.4",
|
|
17
|
+
"soundfile>=0.12.0",
|
|
18
|
+
"numpy>=1.21.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
piper = [
|
|
23
|
+
"piper-tts>=1.2.0",
|
|
24
|
+
]
|
|
25
|
+
all = [
|
|
26
|
+
"kokoro>=0.8.4",
|
|
27
|
+
"piper-tts>=1.2.0",
|
|
28
|
+
"soundfile>=0.12.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
ollama-speak = "ollama_speak.cli:main"
|
|
33
|
+
ospeak = "ollama_speak.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/AjarnSpencer/ollama-speak"
|
|
37
|
+
Repository = "https://github.com/AjarnSpencer/ollama-speak"
|
|
38
|
+
Issues = "https://github.com/AjarnSpencer/ollama-speak/issues"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/ollama_speak"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for ollama-speak.
|
|
3
|
+
100% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M & Piper).
|
|
4
|
+
Accepts text from arguments or STDIN pipes (e.g. `ollama-web "query" | ollama-speak`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import argparse
|
|
10
|
+
from .voice import (
|
|
11
|
+
synthesize_and_speak,
|
|
12
|
+
download_all_models,
|
|
13
|
+
KOKORO_VOICES,
|
|
14
|
+
DEFAULT_KOKORO_VOICE
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_voices():
|
|
19
|
+
print("==================================================================")
|
|
20
|
+
print(" 🗣️ ollama-speak 100% Local Neural Voices (Kokoro-82M & Piper)")
|
|
21
|
+
print("==================================================================")
|
|
22
|
+
print(f"\nDefault Local Voice: {DEFAULT_KOKORO_VOICE} (Warm British Baritone, Kokoro-82M)")
|
|
23
|
+
print("\nSupported Local Open-Weight Voices:")
|
|
24
|
+
print("-" * 66)
|
|
25
|
+
print(f" {'ALIAS / NAME':<18} {'LANGUAGE':<18} {'GENDER / STYLE'}")
|
|
26
|
+
print("-" * 66)
|
|
27
|
+
print(f" {'bm_george (default)':<18} {'British English':<18} {'Male (Warm Baritone)'}")
|
|
28
|
+
print(f" {'bm_lewis':<18} {'British English':<18} {'Male'}")
|
|
29
|
+
print(f" {'bf_alice':<18} {'British English':<18} {'Female'}")
|
|
30
|
+
print(f" {'af_heart':<18} {'American English':<18} {'Female (Warm)'}")
|
|
31
|
+
print(f" {'am_adam':<18} {'American English':<18} {'Male'}")
|
|
32
|
+
print(f" {'jf_tebukuro':<18} {'Japanese':<18} {'Female'}")
|
|
33
|
+
print(f" {'zf_xiaobei':<18} {'Mandarin Chinese':<18} {'Female'}")
|
|
34
|
+
print(f" {'ef_dora':<18} {'Spanish':<18} {'Female'}")
|
|
35
|
+
print(f" {'ff_siwis':<18} {'French':<18} {'Female'}")
|
|
36
|
+
print(f" {'if_sara':<18} {'Italian':<18} {'Female'}")
|
|
37
|
+
print(f" {'pf_dora':<18} {'Portuguese':<18} {'Female'}")
|
|
38
|
+
print(f" {'hf_alpha':<18} {'Hindi':<18} {'Female'}")
|
|
39
|
+
print(f" {'piper (engine)':<18} {'British / English':<18} {'Ultra-lightweight ONNX'}")
|
|
40
|
+
print("\nAll models run 100% locally and offline on your CPU or GPU.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
parser = argparse.ArgumentParser(
|
|
45
|
+
prog="ollama-speak",
|
|
46
|
+
description="100%% Local, Offline Multilingual Neural Text-to-Speech CLI for Ollama (Kokoro-82M and Piper)."
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument("text", nargs="*", help="Text to speak (reads from STDIN if omitted)")
|
|
49
|
+
parser.add_argument("-v", "--voice", default=None, help="Local voice name (e.g. bm_george, af_heart, bf_alice)")
|
|
50
|
+
parser.add_argument("-e", "--engine", choices=["kokoro", "piper"], default="kokoro", help="TTS engine: kokoro (default, studio quality) or piper (ultra-fast ONNX)")
|
|
51
|
+
parser.add_argument("--piper", action="store_true", help="Shortcut for --engine piper (ultra-lightweight local ONNX)")
|
|
52
|
+
parser.add_argument("--kokoro", action="store_true", help="Shortcut for --engine kokoro (studio quality open-weights)")
|
|
53
|
+
parser.add_argument("-o", "--output", default=None, help="Save audio to specified WAV/MP3 file instead of playing")
|
|
54
|
+
parser.add_argument("--setup", "--download-models", action="store_true", help="Pre-download local model weights for 100%% offline use")
|
|
55
|
+
parser.add_argument("--list-voices", action="store_true", help="List available local open-weight voices")
|
|
56
|
+
|
|
57
|
+
args = parser.parse_args()
|
|
58
|
+
|
|
59
|
+
if args.setup:
|
|
60
|
+
download_all_models()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if args.list_voices:
|
|
64
|
+
print_voices()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
engine = "piper" if args.piper else ("kokoro" if args.kokoro else args.engine)
|
|
68
|
+
|
|
69
|
+
# 1. Check positional arguments
|
|
70
|
+
if args.text:
|
|
71
|
+
content = " ".join(args.text).strip()
|
|
72
|
+
# 2. Check STDIN pipe (e.g. ollama-web ... | ollama-speak)
|
|
73
|
+
elif not sys.stdin.isatty():
|
|
74
|
+
content = sys.stdin.read().strip()
|
|
75
|
+
# Always echo piped text to stdout so the human sees, reads, and can copy-paste it!
|
|
76
|
+
if content:
|
|
77
|
+
print(content, flush=True)
|
|
78
|
+
else:
|
|
79
|
+
parser.print_help()
|
|
80
|
+
sys.exit(0)
|
|
81
|
+
|
|
82
|
+
if not content:
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
output_path = os.path.expanduser(args.output) if args.output else None
|
|
86
|
+
|
|
87
|
+
# Synthesize and speak using 100% local model
|
|
88
|
+
success = synthesize_and_speak(
|
|
89
|
+
text=content,
|
|
90
|
+
voice=args.voice,
|
|
91
|
+
engine=engine,
|
|
92
|
+
output_file=output_path
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if output_path and success:
|
|
96
|
+
print(f"✓ Saved audio to: {output_path}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
main()
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core local voice engine for ollama-speak.
|
|
3
|
+
100% Local, Offline, Private Neural Text-to-Speech using Kokoro-82M and Piper ONNX.
|
|
4
|
+
Zero cloud APIs, zero big-tech telemetry, zero external data leaks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import socket
|
|
12
|
+
import shutil
|
|
13
|
+
import urllib.request
|
|
14
|
+
import tempfile
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Common Kokoro-82M open-weight voices
|
|
19
|
+
KOKORO_VOICES = {
|
|
20
|
+
# British English (b)
|
|
21
|
+
"en-gb": ("bm_george", "b"),
|
|
22
|
+
"british": ("bm_george", "b"),
|
|
23
|
+
"bm_george": ("bm_george", "b"),
|
|
24
|
+
"bm_lewis": ("bm_lewis", "b"),
|
|
25
|
+
"bm_daniel": ("bm_daniel", "b"),
|
|
26
|
+
"bm_fable": ("bm_fable", "b"),
|
|
27
|
+
"bf_alice": ("bf_alice", "b"),
|
|
28
|
+
"bf_isabella": ("bf_isabella", "b"),
|
|
29
|
+
"bf_emma": ("bf_emma", "b"),
|
|
30
|
+
"bf_lily": ("bf_lily", "b"),
|
|
31
|
+
|
|
32
|
+
# American English (a)
|
|
33
|
+
"en-us": ("af_heart", "a"),
|
|
34
|
+
"american": ("af_heart", "a"),
|
|
35
|
+
"af_heart": ("af_heart", "a"),
|
|
36
|
+
"af_bella": ("af_bella", "a"),
|
|
37
|
+
"af_sarah": ("af_sarah", "a"),
|
|
38
|
+
"af_nicole": ("af_nicole", "a"),
|
|
39
|
+
"af_sky": ("af_sky", "a"),
|
|
40
|
+
"am_adam": ("am_adam", "a"),
|
|
41
|
+
"am_michael": ("am_michael", "a"),
|
|
42
|
+
"am_echo": ("am_echo", "a"),
|
|
43
|
+
"am_eric": ("am_eric", "a"),
|
|
44
|
+
"am_onyx": ("am_onyx", "a"),
|
|
45
|
+
|
|
46
|
+
# Japanese (j)
|
|
47
|
+
"ja": ("jf_tebukuro", "j"),
|
|
48
|
+
"japanese": ("jf_tebukuro", "j"),
|
|
49
|
+
"jf_tebukuro": ("jf_tebukuro", "j"),
|
|
50
|
+
"jm_kumo": ("jm_kumo", "j"),
|
|
51
|
+
|
|
52
|
+
# Mandarin Chinese (z)
|
|
53
|
+
"zh": ("zf_xiaobei", "z"),
|
|
54
|
+
"chinese": ("zf_xiaobei", "z"),
|
|
55
|
+
"zf_xiaobei": ("zf_xiaobei", "z"),
|
|
56
|
+
"zm_yunjian": ("zm_yunjian", "z"),
|
|
57
|
+
|
|
58
|
+
# Spanish (e)
|
|
59
|
+
"es": ("ef_dora", "e"),
|
|
60
|
+
"spanish": ("ef_dora", "e"),
|
|
61
|
+
"ef_dora": ("ef_dora", "e"),
|
|
62
|
+
"em_alex": ("em_alex", "e"),
|
|
63
|
+
|
|
64
|
+
# French (f)
|
|
65
|
+
"fr": ("ff_siwis", "f"),
|
|
66
|
+
"french": ("ff_siwis", "f"),
|
|
67
|
+
"ff_siwis": ("ff_siwis", "f"),
|
|
68
|
+
|
|
69
|
+
# Italian (i)
|
|
70
|
+
"it": ("if_sara", "i"),
|
|
71
|
+
"italian": ("if_sara", "i"),
|
|
72
|
+
"if_sara": ("if_sara", "i"),
|
|
73
|
+
"im_nicola": ("im_nicola", "i"),
|
|
74
|
+
|
|
75
|
+
# Portuguese (p)
|
|
76
|
+
"pt": ("pf_dora", "p"),
|
|
77
|
+
"portuguese": ("pf_dora", "p"),
|
|
78
|
+
"pf_dora": ("pf_dora", "p"),
|
|
79
|
+
"pm_alex": ("pm_alex", "p"),
|
|
80
|
+
|
|
81
|
+
# Hindi (h)
|
|
82
|
+
"hi": ("hf_alpha", "h"),
|
|
83
|
+
"hindi": ("hf_alpha", "h"),
|
|
84
|
+
"hf_alpha": ("hf_alpha", "h"),
|
|
85
|
+
"hm_psi": ("hm_psi", "h"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
DEFAULT_KOKORO_VOICE = "bm_george"
|
|
89
|
+
DEFAULT_KOKORO_LANG = "b"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_models_dir() -> Path:
|
|
93
|
+
"""Returns local model cache directory (~/.local/share/ollama-speak/models)."""
|
|
94
|
+
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
95
|
+
d = base / "ollama-speak" / "models"
|
|
96
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
return d
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def find_piper_model() -> tuple[str, str]:
|
|
101
|
+
"""Finds or returns expected path for local Piper ONNX model and config."""
|
|
102
|
+
# 1. Custom env var
|
|
103
|
+
custom = os.environ.get("PIPER_MODEL")
|
|
104
|
+
if custom and os.path.exists(custom):
|
|
105
|
+
json_path = custom + ".json" if not custom.endswith(".json") else custom
|
|
106
|
+
onnx_path = custom.replace(".json", "")
|
|
107
|
+
return onnx_path, json_path
|
|
108
|
+
|
|
109
|
+
# 2. DGX / Host workspace path
|
|
110
|
+
workspace_piper = Path.home() / "ai-workspace" / "models" / "tts" / "piper" / "en_GB-alan-medium.onnx"
|
|
111
|
+
if workspace_piper.exists():
|
|
112
|
+
return str(workspace_piper), str(workspace_piper) + ".json"
|
|
113
|
+
|
|
114
|
+
# 3. Standard user data dir
|
|
115
|
+
user_piper = get_models_dir() / "piper" / "en_GB-alan-medium.onnx"
|
|
116
|
+
return str(user_piper), str(user_piper) + ".json"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def play_audio_file(file_path: str) -> bool:
|
|
120
|
+
"""Plays an audio file (.wav or .mp3) natively across Linux, macOS, and Windows."""
|
|
121
|
+
if not os.path.exists(file_path) or os.path.getsize(file_path) < 100:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
# macOS: native afplay
|
|
125
|
+
if sys.platform == "darwin":
|
|
126
|
+
if shutil.which("afplay"):
|
|
127
|
+
res = subprocess.run(["afplay", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
128
|
+
return res.returncode == 0
|
|
129
|
+
|
|
130
|
+
# Windows: native PowerShell
|
|
131
|
+
if sys.platform == "win32":
|
|
132
|
+
abs_p = os.path.abspath(file_path).replace("\\", "\\\\")
|
|
133
|
+
ps_cmd = (
|
|
134
|
+
f"$player = New-Object System.Media.SoundPlayer('{abs_p}'); "
|
|
135
|
+
f"if ($player) {{ $player.PlaySync() }}"
|
|
136
|
+
)
|
|
137
|
+
res = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
138
|
+
return res.returncode == 0
|
|
139
|
+
|
|
140
|
+
# Linux: check audio players
|
|
141
|
+
for cmd in ["pw-play", "paplay", "mpv", "ffplay", "aplay"]:
|
|
142
|
+
if shutil.which(cmd):
|
|
143
|
+
if cmd == "mpv":
|
|
144
|
+
res = subprocess.run(["mpv", "--no-video", "--really-quiet", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
145
|
+
elif cmd == "ffplay":
|
|
146
|
+
res = subprocess.run(["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
147
|
+
elif cmd in ("pw-play", "paplay"):
|
|
148
|
+
res = subprocess.run([cmd, file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
149
|
+
elif cmd == "aplay" and file_path.endswith(".wav"):
|
|
150
|
+
res = subprocess.run(["aplay", "-q", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
151
|
+
else:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if res.returncode == 0:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def try_local_daemon(text: str, engine: str = "kokoro") -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Checks if a local sovereign voice daemon is already active on localhost.
|
|
163
|
+
Provides sub-50ms warm inference without reloading weights into memory.
|
|
164
|
+
"""
|
|
165
|
+
uid = os.getuid() if hasattr(os, "getuid") else 1000
|
|
166
|
+
socket_path = f"/run/user/{uid}/voice_dictate.sock"
|
|
167
|
+
if not os.path.exists(socket_path):
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
cmd = f"SPEAK_KOKORO:{text}\n" if engine == "kokoro" else f"SPEAK:{text}\n"
|
|
171
|
+
try:
|
|
172
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
173
|
+
s.settimeout(10.0)
|
|
174
|
+
s.connect(socket_path)
|
|
175
|
+
s.sendall(cmd.encode("utf-8"))
|
|
176
|
+
resp = s.recv(1024).decode("utf-8", errors="replace")
|
|
177
|
+
s.close()
|
|
178
|
+
return "OK:" in resp
|
|
179
|
+
except Exception:
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def synthesize_with_kokoro(
|
|
184
|
+
text: str,
|
|
185
|
+
voice_name: str = None,
|
|
186
|
+
speed: float = 1.0,
|
|
187
|
+
output_path: str = None
|
|
188
|
+
) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Synthesizes speech using local Kokoro-82M open-weight model.
|
|
191
|
+
Runs 100% locally on CPU or GPU with zero cloud network calls.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
import numpy as np
|
|
195
|
+
import soundfile as sf
|
|
196
|
+
from kokoro import KPipeline
|
|
197
|
+
except ImportError as e:
|
|
198
|
+
print(f"[!] Kokoro dependencies not found ({e}). Run: pip install kokoro soundfile", file=sys.stderr)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
# Resolve voice and language code
|
|
202
|
+
selected_voice = DEFAULT_KOKORO_VOICE
|
|
203
|
+
selected_lang = DEFAULT_KOKORO_LANG
|
|
204
|
+
|
|
205
|
+
if voice_name:
|
|
206
|
+
v_lower = voice_name.lower().strip()
|
|
207
|
+
if v_lower in KOKORO_VOICES:
|
|
208
|
+
selected_voice, selected_lang = KOKORO_VOICES[v_lower]
|
|
209
|
+
else:
|
|
210
|
+
selected_voice = voice_name
|
|
211
|
+
# Auto-detect lang from prefix
|
|
212
|
+
prefix = voice_name[0].lower()
|
|
213
|
+
if prefix in ("b", "a", "j", "z", "e", "f", "i", "p", "h"):
|
|
214
|
+
selected_lang = prefix
|
|
215
|
+
|
|
216
|
+
# Load local Kokoro pipeline
|
|
217
|
+
try:
|
|
218
|
+
pipeline = KPipeline(lang_code=selected_lang, repo_id="hexgrad/Kokoro-82M")
|
|
219
|
+
generator = pipeline(text, voice=selected_voice, speed=speed)
|
|
220
|
+
chunks = [a for _, _, a in generator]
|
|
221
|
+
if not chunks:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
audio = np.concatenate(chunks)
|
|
225
|
+
save_file = output_path
|
|
226
|
+
is_temp = False
|
|
227
|
+
if not save_file:
|
|
228
|
+
is_temp = True
|
|
229
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
230
|
+
save_file = tmp.name
|
|
231
|
+
tmp.close()
|
|
232
|
+
|
|
233
|
+
sf.write(save_file, audio, 24000)
|
|
234
|
+
|
|
235
|
+
if is_temp:
|
|
236
|
+
success = play_audio_file(save_file)
|
|
237
|
+
try:
|
|
238
|
+
os.remove(save_file)
|
|
239
|
+
except OSError:
|
|
240
|
+
pass
|
|
241
|
+
return success
|
|
242
|
+
else:
|
|
243
|
+
return os.path.exists(save_file) and os.path.getsize(save_file) > 100
|
|
244
|
+
except Exception as e:
|
|
245
|
+
print(f"[!] Kokoro synthesis error: {e}", file=sys.stderr)
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def synthesize_with_piper(
|
|
250
|
+
text: str,
|
|
251
|
+
output_path: str = None
|
|
252
|
+
) -> bool:
|
|
253
|
+
"""
|
|
254
|
+
Synthesizes speech using local Piper ONNX model.
|
|
255
|
+
Ultra-lightweight local engine (~35MB model) for low-spec PCs.
|
|
256
|
+
"""
|
|
257
|
+
onnx_path, json_path = find_piper_model()
|
|
258
|
+
if not os.path.exists(onnx_path):
|
|
259
|
+
print(f"[!] Piper ONNX model not found at: {onnx_path}", file=sys.stderr)
|
|
260
|
+
print(" Download it with: ollama-speak --setup", file=sys.stderr)
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
save_file = output_path
|
|
264
|
+
is_temp = False
|
|
265
|
+
if not save_file:
|
|
266
|
+
is_temp = True
|
|
267
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
268
|
+
save_file = tmp.name
|
|
269
|
+
tmp.close()
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Try python piper module
|
|
273
|
+
import wave
|
|
274
|
+
from piper.voice import PiperVoice
|
|
275
|
+
voice = PiperVoice.load(onnx_path, config_path=json_path if os.path.exists(json_path) else None)
|
|
276
|
+
with wave.open(save_file, "wb") as wf:
|
|
277
|
+
voice.synthesize_wav(text, wf)
|
|
278
|
+
except Exception:
|
|
279
|
+
# Fall back to piper CLI if available
|
|
280
|
+
piper_bin = shutil.which("piper")
|
|
281
|
+
if piper_bin:
|
|
282
|
+
res = subprocess.run([piper_bin, "-m", onnx_path, "-f", save_file], input=text.encode("utf-8"), check=False)
|
|
283
|
+
if res.returncode != 0:
|
|
284
|
+
return False
|
|
285
|
+
else:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
if is_temp:
|
|
289
|
+
success = play_audio_file(save_file)
|
|
290
|
+
try:
|
|
291
|
+
os.remove(save_file)
|
|
292
|
+
except OSError:
|
|
293
|
+
pass
|
|
294
|
+
return success
|
|
295
|
+
else:
|
|
296
|
+
return os.path.exists(save_file) and os.path.getsize(save_file) > 100
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def download_all_models() -> None:
|
|
300
|
+
"""
|
|
301
|
+
Pre-downloads Kokoro-82M and Piper open-weight models for 100% offline use.
|
|
302
|
+
"""
|
|
303
|
+
print("==================================================================")
|
|
304
|
+
print(" 📥 ollama-speak Local Model Downloader (100% Offline Setup)")
|
|
305
|
+
print("==================================================================")
|
|
306
|
+
|
|
307
|
+
# 1. Kokoro-82M
|
|
308
|
+
print("\n1. Verifying / Downloading Kokoro-82M open-weight model (~350 MB)...")
|
|
309
|
+
try:
|
|
310
|
+
from kokoro import KPipeline
|
|
311
|
+
p = KPipeline(lang_code="b", repo_id="hexgrad/Kokoro-82M")
|
|
312
|
+
# Synthesize a 1-word smoke test to cache default voices
|
|
313
|
+
for v in ["bm_george", "af_heart"]:
|
|
314
|
+
gen = p("Ready", voice=v)
|
|
315
|
+
_ = [a for _, _, a in gen]
|
|
316
|
+
print("✓ Kokoro-82M weights verified and cached locally.")
|
|
317
|
+
except Exception as e:
|
|
318
|
+
print(f"[!] Kokoro download error: {e}")
|
|
319
|
+
|
|
320
|
+
# 2. Piper ONNX
|
|
321
|
+
print("\n2. Verifying / Downloading Piper British ONNX model (~35 MB)...")
|
|
322
|
+
onnx_path, json_path = find_piper_model()
|
|
323
|
+
if os.path.exists(onnx_path):
|
|
324
|
+
print(f"✓ Piper ONNX already exists at: {onnx_path}")
|
|
325
|
+
else:
|
|
326
|
+
try:
|
|
327
|
+
target_dir = Path(onnx_path).parent
|
|
328
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
url_base = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_GB/alan/medium"
|
|
330
|
+
print(f"Downloading {onnx_path}...")
|
|
331
|
+
urllib.request.urlretrieve(f"{url_base}/en_GB-alan-medium.onnx", onnx_path)
|
|
332
|
+
urllib.request.urlretrieve(f"{url_base}/en_GB-alan-medium.onnx.json", json_path)
|
|
333
|
+
print("✓ Piper ONNX model downloaded successfully.")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
print(f"[!] Piper download warning: {e}")
|
|
336
|
+
|
|
337
|
+
print("\n==================================================================")
|
|
338
|
+
print(" All local models are ready. ollama-speak is 100% offline and sovereign!")
|
|
339
|
+
print("==================================================================")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def synthesize_and_speak(
|
|
343
|
+
text: str,
|
|
344
|
+
voice: str = None,
|
|
345
|
+
engine: str = "kokoro",
|
|
346
|
+
output_file: str = None
|
|
347
|
+
) -> bool:
|
|
348
|
+
"""
|
|
349
|
+
Main dispatch function for ollama-speak.
|
|
350
|
+
Synthesizes and speaks text using 100% LOCAL models.
|
|
351
|
+
"""
|
|
352
|
+
clean = re.sub(r"https?://\S+", "", text)
|
|
353
|
+
clean = re.sub(r"[`*_#>\[\]()~|]+", " ", clean)
|
|
354
|
+
clean = re.sub(r"\s+", " ", clean).strip()
|
|
355
|
+
if not clean:
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
# Detect unsupported scripts for local models
|
|
359
|
+
if re.search(r"[\u0e00-\u0e7f]", clean):
|
|
360
|
+
print("[ollama-speak] Note: Thai script detected. Local Kokoro/Piper currently support English, Japanese, Chinese, Spanish, French, Italian, Portuguese, and Hindi.", file=sys.stderr)
|
|
361
|
+
|
|
362
|
+
# 1. Fast-path: Check for already warm local voice daemon on host
|
|
363
|
+
if not output_file and try_local_daemon(clean, engine=engine):
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
# 2. Piper Engine
|
|
367
|
+
if engine.lower() in ("piper", "fast"):
|
|
368
|
+
return synthesize_with_piper(clean, output_path=output_file)
|
|
369
|
+
|
|
370
|
+
# 3. Kokoro Engine (Default Local SOTA)
|
|
371
|
+
return synthesize_with_kokoro(clean, voice_name=voice, output_path=output_file)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Diagnostic & Verification Suite for ollama-speak.
|
|
4
|
+
Runs automated smoke tests to verify local Kokoro-82M, Piper ONNX, and OS audio playback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
# Auto re-exec into isolated venv if run from bare system python without dependencies
|
|
13
|
+
venv_dir = os.path.expanduser("~/.local/share/ollama-speak/venv")
|
|
14
|
+
venv_py = os.path.join(venv_dir, "bin", "python")
|
|
15
|
+
if os.path.exists(venv_py) and sys.prefix != venv_dir:
|
|
16
|
+
try:
|
|
17
|
+
import kokoro
|
|
18
|
+
except ImportError:
|
|
19
|
+
os.execv(venv_py, [venv_py] + sys.argv)
|
|
20
|
+
|
|
21
|
+
def test_audio_backends():
|
|
22
|
+
print("[1/3] Checking OS audio playback engines...", end=" ", flush=True)
|
|
23
|
+
found = []
|
|
24
|
+
for cmd in ["pw-play", "paplay", "mpv", "ffplay", "aplay", "afplay"]:
|
|
25
|
+
if shutil.which(cmd):
|
|
26
|
+
found.append(cmd)
|
|
27
|
+
if sys.platform == "win32":
|
|
28
|
+
found.append("powershell (System.Media)")
|
|
29
|
+
|
|
30
|
+
if found:
|
|
31
|
+
print(f"✓ OK (Found: {', '.join(found)})")
|
|
32
|
+
return True
|
|
33
|
+
else:
|
|
34
|
+
print("❌ Warning: No audio player found. Install mpv or ffmpeg.")
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def test_kokoro_local():
|
|
38
|
+
print("[2/3] Checking local Kokoro-82M open-weight model...", end=" ", flush=True)
|
|
39
|
+
try:
|
|
40
|
+
from kokoro import KPipeline
|
|
41
|
+
import soundfile as sf
|
|
42
|
+
import numpy as np
|
|
43
|
+
|
|
44
|
+
p = KPipeline(lang_code="b", repo_id="hexgrad/Kokoro-82M")
|
|
45
|
+
gen = p("Diagnostic test.", voice="bm_george", speed=1.0)
|
|
46
|
+
chunks = [a for _, _, a in gen]
|
|
47
|
+
if chunks:
|
|
48
|
+
audio = np.concatenate(chunks)
|
|
49
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
50
|
+
sf.write(tmp.name, audio, 24000)
|
|
51
|
+
sz = os.path.getsize(tmp.name)
|
|
52
|
+
os.remove(tmp.name)
|
|
53
|
+
print(f"✓ OK (Synthesized {len(audio)} samples, {sz} bytes)")
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
print("❌ Failed (Empty audio generated)")
|
|
57
|
+
return False
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"❌ Error: {e}")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def test_piper_local():
|
|
63
|
+
print("[3/3] Checking local Piper ONNX model...", end=" ", flush=True)
|
|
64
|
+
from ollama_speak.voice import find_piper_model
|
|
65
|
+
onnx_path, json_path = find_piper_model()
|
|
66
|
+
if os.path.exists(onnx_path):
|
|
67
|
+
print(f"✓ OK (Found: {onnx_path})")
|
|
68
|
+
return True
|
|
69
|
+
else:
|
|
70
|
+
print(f"⚠️ Notice: Piper ONNX not cached yet at {onnx_path}. Run 'ollama-speak --setup' to pre-download.")
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
print("==================================================")
|
|
75
|
+
print(" 🛠️ ollama-speak System Diagnostic & Smoke Test")
|
|
76
|
+
print("==================================================")
|
|
77
|
+
t1 = test_audio_backends()
|
|
78
|
+
t2 = test_kokoro_local()
|
|
79
|
+
t3 = test_piper_local()
|
|
80
|
+
print("==================================================")
|
|
81
|
+
if t1 and t2 and t3:
|
|
82
|
+
print("🎉 100% Local voice engine verified and operational!")
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
else:
|
|
85
|
+
print("❌ Diagnostic failures detected. Check logs above.")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|