par-cli-tts 0.4.0__tar.gz → 0.4.2__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.
- par_cli_tts-0.4.2/.claude/output-styles/tts-summary.md +78 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/.gitignore +1 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/PKG-INFO +195 -4
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/README.md +194 -3
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/pyproject.toml +3 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/__init__.py +1 -1
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/config_file.py +8 -0
- par_cli_tts-0.4.2/src/install_claude_style.py +219 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/elevenlabs.py +8 -12
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/tts_cli.py +40 -18
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/utils.py +150 -2
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/LICENSE +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/config.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/console.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/defaults.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/errors.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/http_client.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/kokoro_cli.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/model_downloader.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/__init__.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/base.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/kokoro_onnx.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/openai.py +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/py.typed +0 -0
- {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/voice_cache.py +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: TTS Summary
|
|
3
|
+
description: Audio task completion announcements with TTS
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TTS Summary Output Style
|
|
7
|
+
|
|
8
|
+
You are Claude Code with an experimental TTS announcement feature designed to communicate directly with the user about what you've accomplished.
|
|
9
|
+
|
|
10
|
+
## Variables
|
|
11
|
+
- **USER_NAME**: Paul
|
|
12
|
+
|
|
13
|
+
## Standard Behavior
|
|
14
|
+
Respond normally to all user requests, using your full capabilities for:
|
|
15
|
+
- Code generation and editing
|
|
16
|
+
- File operations
|
|
17
|
+
- Running commands
|
|
18
|
+
- Analysis and explanations
|
|
19
|
+
- All standard Claude Code features
|
|
20
|
+
|
|
21
|
+
## Critical Addition: Audio Task Summary
|
|
22
|
+
|
|
23
|
+
**At the very END of EVERY response**, you MUST provide an audio summary for the user:
|
|
24
|
+
|
|
25
|
+
1. Write a clear separator: `---`
|
|
26
|
+
2. Add the heading: `## Audio Summary for Paul`
|
|
27
|
+
3. Craft a message that speaks DIRECTLY to Paul about what you did for them
|
|
28
|
+
4. **IMMEDIATELY execute the TTS command using the Bash tool** - do NOT just display it in a code block
|
|
29
|
+
|
|
30
|
+
### How to Execute the TTS Command
|
|
31
|
+
|
|
32
|
+
You MUST use the Bash tool to actually run the command. Do NOT wrap it in a markdown code block.
|
|
33
|
+
|
|
34
|
+
**CORRECT** - Execute with Bash tool:
|
|
35
|
+
```
|
|
36
|
+
[Bash tool call with command: par-tts "Paul, I've completed the task."]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**WRONG** - Just displaying (DO NOT DO THIS):
|
|
40
|
+
```markdown
|
|
41
|
+
```bash
|
|
42
|
+
par-tts "Paul, I've completed the task."
|
|
43
|
+
```
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The user should hear the audio playback, not just see the command text.
|
|
47
|
+
|
|
48
|
+
## Communication Guidelines
|
|
49
|
+
|
|
50
|
+
- **Address Paul directly** when appropriate: "Paul, I've updated your..." or "Fixed the bug in..."
|
|
51
|
+
- **Focus on outcomes** for the user: what they can now do, what's been improved
|
|
52
|
+
- **Be conversational** - speak as if telling Paul what you just did
|
|
53
|
+
- **Highlight value** - emphasize what's useful about the change
|
|
54
|
+
- **Keep it concise** - one clear sentence (under 20 words)
|
|
55
|
+
|
|
56
|
+
## Example Response Pattern
|
|
57
|
+
|
|
58
|
+
[Your normal response content here...]
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Audio Summary for Paul
|
|
63
|
+
|
|
64
|
+
Paul, I've created three new output styles to customize how you receive information.
|
|
65
|
+
|
|
66
|
+
[Bash tool call: par-tts "Paul, I've created three new output styles to customize how you receive information."]
|
|
67
|
+
|
|
68
|
+
## Important Rules
|
|
69
|
+
|
|
70
|
+
- ALWAYS include the audio summary, even for simple queries
|
|
71
|
+
- Speak TO the user, not about abstract tasks
|
|
72
|
+
- Use natural, conversational language
|
|
73
|
+
- Focus on the user benefit or outcome
|
|
74
|
+
- Make it feel like a helpful assistant reporting completion
|
|
75
|
+
- **EXECUTE the command with the Bash tool - do NOT just display it in a code block**
|
|
76
|
+
- The user should HEAR the audio, not just see the command text
|
|
77
|
+
|
|
78
|
+
This experimental feature provides personalized audio feedback about task completion.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: par-cli-tts
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: PAR CLI TTS - Command line text-to-speech tool using ElevenLabs with voice caching and name resolution
|
|
5
5
|
Project-URL: Homepage, https://github.com/paulrobello/par-cli-tts
|
|
6
6
|
Project-URL: Documentation, https://github.com/paulrobello/par-cli-tts/blob/main/README.md
|
|
@@ -68,15 +68,45 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|

|
|
69
69
|
|
|
70
70
|

|
|
71
|
-

|
|
72
72
|

|
|
73
73
|
|
|
74
74
|
A powerful command-line text-to-speech tool supporting multiple TTS providers (ElevenLabs, OpenAI, and Kokoro ONNX) with intelligent voice caching, name resolution, and flexible output options.
|
|
75
75
|
|
|
76
76
|
[](https://buymeacoffee.com/probello3)
|
|
77
77
|
|
|
78
|
+
## Table of Contents
|
|
79
|
+
|
|
80
|
+
- [What's New](#whats-new)
|
|
81
|
+
- [Features](#features)
|
|
82
|
+
- [Technology Stack](#technology-stack)
|
|
83
|
+
- [Prerequisites](#prerequisites)
|
|
84
|
+
- [Installation](#installation)
|
|
85
|
+
- [From PyPI](#installation-from-pypi-recommended)
|
|
86
|
+
- [From Source](#installation-from-source)
|
|
87
|
+
- [Kokoro ONNX Setup](#kokoro-onnx-setup)
|
|
88
|
+
- [Using with AI Agents](#using-with-ai-agents)
|
|
89
|
+
- [Claude Code Setup](#claude-code-setup)
|
|
90
|
+
- [Claude Code Output Style](#claude-code-output-style)
|
|
91
|
+
- [Configuration](#configuration)
|
|
92
|
+
- [Usage](#usage)
|
|
93
|
+
- [Command Line Options](#command-line-options)
|
|
94
|
+
- [Providers](#providers)
|
|
95
|
+
- [Cache Locations](#cache-locations)
|
|
96
|
+
- [Development](#development)
|
|
97
|
+
- [Troubleshooting](#troubleshooting)
|
|
98
|
+
- [Contributing](#contributing)
|
|
99
|
+
- [License](#license)
|
|
100
|
+
- [Author](#author)
|
|
101
|
+
- [Acknowledgments](#acknowledgments)
|
|
102
|
+
- [Support](#support)
|
|
103
|
+
|
|
78
104
|
## What's New
|
|
79
105
|
|
|
106
|
+
### v0.4.1
|
|
107
|
+
- **Claude Code output style installer** - New `install-claude-style` command to automatically set up TTS audio summaries
|
|
108
|
+
- **Bundled output style** - TTS Summary output style included with the package
|
|
109
|
+
|
|
80
110
|
### v0.4.0
|
|
81
111
|
- **OpenAI gpt-4o-mini-tts** - New steerable TTS model with `--instructions` option
|
|
82
112
|
- **7 new OpenAI voices** - ash, ballad, coral, sage, verse, marin, cedar (13 total)
|
|
@@ -106,7 +136,7 @@ A powerful command-line text-to-speech tool supporting multiple TTS providers (E
|
|
|
106
136
|
- **Smarter Voice Cache** - Change detection, manual refresh, and voice sample caching
|
|
107
137
|
- **Consistent Error Handling** - Clear error messages with proper exit codes
|
|
108
138
|
- **Multiple Input Methods** - Direct text, stdin piping, and file input (`@filename`)
|
|
109
|
-
- **Volume Control** - Adjust playback volume (0.0 to 5.0)
|
|
139
|
+
- **Volume Control** - Adjust playback volume (0.0 to 5.0) across all platforms (macOS, Linux, Windows)
|
|
110
140
|
- **Voice Preview** - Test voices with sample text before using
|
|
111
141
|
|
|
112
142
|
### Improvements
|
|
@@ -122,7 +152,7 @@ A powerful command-line text-to-speech tool supporting multiple TTS providers (E
|
|
|
122
152
|
- **Configuration File** - Set default preferences in YAML config file (`~/.config/par-tts/config.yaml`)
|
|
123
153
|
- **Flexible Input Methods** - Accept text from command line, stdin pipe, or files (`@filename`)
|
|
124
154
|
- **Voice Name Support** - Use voice names like "Juniper" or "nova" instead of cryptic IDs
|
|
125
|
-
- **Volume Control** - Adjust playback volume (0.0 to 5.0)
|
|
155
|
+
- **Volume Control** - Adjust playback volume (0.0 to 5.0) across all platforms (macOS, Linux, Windows)
|
|
126
156
|
- **Voice Preview** - Test voices with sample text using `--preview-voice`
|
|
127
157
|
- **Smart Voice Caching** - Change detection, auto-refresh, and voice sample caching
|
|
128
158
|
- **Partial Name Matching** - Type "char" to match "Charlotte" (ElevenLabs)
|
|
@@ -163,6 +193,43 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
|
163
193
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
164
194
|
```
|
|
165
195
|
|
|
196
|
+
### Windows Audio Requirements
|
|
197
|
+
|
|
198
|
+
For the best audio playback experience on Windows with volume control, install one of these audio players:
|
|
199
|
+
|
|
200
|
+
#### ffplay (Recommended)
|
|
201
|
+
```powershell
|
|
202
|
+
# Using Chocolatey
|
|
203
|
+
choco install ffmpeg
|
|
204
|
+
|
|
205
|
+
# Using Scoop
|
|
206
|
+
scoop install ffmpeg
|
|
207
|
+
|
|
208
|
+
# Using winget
|
|
209
|
+
winget install ffmpeg
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### VLC Media Player (Alternative)
|
|
213
|
+
Download from [videolan.org](https://www.videolan.org/vlc/) or:
|
|
214
|
+
```powershell
|
|
215
|
+
# Using Chocolatey
|
|
216
|
+
choco install vlc
|
|
217
|
+
|
|
218
|
+
# Using winget
|
|
219
|
+
winget install VideoLAN.VLC
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### mpg123 (Lightweight Option)
|
|
223
|
+
```powershell
|
|
224
|
+
# Using Chocolatey
|
|
225
|
+
choco install mpg123
|
|
226
|
+
|
|
227
|
+
# Using Scoop
|
|
228
|
+
scoop install mpg123
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Note**: If no external player is installed, PAR CLI TTS will use Windows PowerShell's built-in MediaPlayer COM object as a fallback. This provides basic playback with volume control (capped at 1.0/100%). For full volume control up to 5.0x, install ffplay, VLC, or mpg123.
|
|
232
|
+
|
|
166
233
|
## Installation
|
|
167
234
|
|
|
168
235
|
### Installation from PyPI (Recommended)
|
|
@@ -258,6 +325,121 @@ export KOKORO_VOICE_PATH=/path/to/voices-v1.0.bin
|
|
|
258
325
|
|
|
259
326
|
When these environment variables are set, automatic download is disabled.
|
|
260
327
|
|
|
328
|
+
## Using with AI Agents
|
|
329
|
+
|
|
330
|
+
PAR CLI TTS works great with AI agents like [Claude Code](https://claude.ai/code). When using it in an agent, you'll need to grant permission for the agent to run the `par-tts` command.
|
|
331
|
+
|
|
332
|
+
### Claude Code Setup
|
|
333
|
+
|
|
334
|
+
The easiest way to allow Claude Code to use `par-tts` is to add the following to your `~/.claude/settings.json`:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"permissions": {
|
|
339
|
+
"allow": [
|
|
340
|
+
"Bash(par-tts:*)"
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
This grants Claude Code permission to run any `par-tts` command without prompting for approval each time.
|
|
347
|
+
|
|
348
|
+
### Example Agent Usage
|
|
349
|
+
|
|
350
|
+
Once configured, your AI agent can easily generate speech:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Agent can run TTS commands directly
|
|
354
|
+
par-tts "Task completed successfully!"
|
|
355
|
+
|
|
356
|
+
# Save audio for notifications
|
|
357
|
+
par-tts "Build finished" --output /tmp/notify.mp3 --no-play
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Claude Code Output Style
|
|
361
|
+
|
|
362
|
+
This project includes a **TTS Summary** output style for Claude Code that provides audio announcements when tasks are completed. This creates a personalized audio feedback experience where Claude announces what it has accomplished.
|
|
363
|
+
|
|
364
|
+
#### Features
|
|
365
|
+
|
|
366
|
+
- Automatic audio summary at the end of every Claude Code response
|
|
367
|
+
- Personalized messages addressing you by name
|
|
368
|
+
- Focus on outcomes and user benefits
|
|
369
|
+
- Natural, conversational language
|
|
370
|
+
|
|
371
|
+
#### Installation
|
|
372
|
+
|
|
373
|
+
The easiest way to install the output style is using the built-in CLI command:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# Interactive installation (prompts for your name)
|
|
377
|
+
par-tts-install-style
|
|
378
|
+
|
|
379
|
+
# Non-interactive with name specified
|
|
380
|
+
par-tts-install-style --name "YourName"
|
|
381
|
+
|
|
382
|
+
# Force overwrite if already installed
|
|
383
|
+
par-tts-install-style --name "YourName" --force
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This command will:
|
|
387
|
+
1. Copy the TTS Summary output style to `~/.claude/output-styles/tts-summary.md`
|
|
388
|
+
2. Update `~/.claude/settings.json` with the required `Bash(par-tts:*)` permission
|
|
389
|
+
3. Personalize the output style with your name
|
|
390
|
+
|
|
391
|
+
#### Prerequisites
|
|
392
|
+
|
|
393
|
+
**Important**: Before using this output style, ensure:
|
|
394
|
+
|
|
395
|
+
1. `par-cli-tts` is installed (see [Installation](#installation))
|
|
396
|
+
2. The `install-claude-style` command has been run (automatically grants permissions)
|
|
397
|
+
|
|
398
|
+
If you prefer manual installation, you can:
|
|
399
|
+
1. Copy `.claude/output-styles/tts-summary.md` to `~/.claude/output-styles/`
|
|
400
|
+
2. Add the following to `~/.claude/settings.json`:
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"permissions": {
|
|
404
|
+
"allow": [
|
|
405
|
+
"Bash(par-tts:*)"
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### Usage
|
|
412
|
+
|
|
413
|
+
Activate the output style using the `/output-style` command in Claude Code:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
/output-style tts-summary
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Once activated, Claude will automatically announce completed tasks with audio feedback.
|
|
420
|
+
|
|
421
|
+
#### Customization
|
|
422
|
+
|
|
423
|
+
Edit `~/.claude/output-styles/tts-summary.md` to personalize the experience:
|
|
424
|
+
|
|
425
|
+
1. **Change your name** - Find the `USER_NAME` variable and update it:
|
|
426
|
+
```markdown
|
|
427
|
+
## Variables
|
|
428
|
+
- **USER_NAME**: YourNameHere
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
2. **Update the heading** - Search for "Paul" and replace with your name:
|
|
432
|
+
```markdown
|
|
433
|
+
## Audio Summary for YourNameHere
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
3. **Customize the TTS command** - Use a different voice or provider:
|
|
437
|
+
```markdown
|
|
438
|
+
par-tts "YourNameHere, task completed." --voice nova --provider openai
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
4. **Adjust message style** - Modify the Communication Guidelines section to change how Claude speaks to you
|
|
442
|
+
|
|
261
443
|
## Configuration
|
|
262
444
|
|
|
263
445
|
### Configuration File (Recommended)
|
|
@@ -281,6 +463,10 @@ provider: kokoro-onnx
|
|
|
281
463
|
# Default voice
|
|
282
464
|
voice: Rachel
|
|
283
465
|
|
|
466
|
+
# API keys (optional - can also be set via environment variables)
|
|
467
|
+
# elevenlabs_api_key: your-elevenlabs-api-key-here
|
|
468
|
+
# openai_api_key: your-openai-api-key-here
|
|
469
|
+
|
|
284
470
|
# Output settings
|
|
285
471
|
output_dir: ~/Documents/audio
|
|
286
472
|
keep_temp: false
|
|
@@ -701,6 +887,9 @@ par-cli-tts/
|
|
|
701
887
|
├── docs/
|
|
702
888
|
│ ├── ARCHITECTURE.md # System architecture documentation
|
|
703
889
|
│ └── CLAUDE.md # Development guidelines
|
|
890
|
+
├── .claude/
|
|
891
|
+
│ └── output-styles/
|
|
892
|
+
│ └── tts-summary.md # Claude Code TTS output style
|
|
704
893
|
├── .env.example # Example environment file
|
|
705
894
|
├── pyproject.toml # Project configuration
|
|
706
895
|
├── Makefile # Development commands
|
|
@@ -739,6 +928,8 @@ par-cli-tts/
|
|
|
739
928
|
- Check system volume settings
|
|
740
929
|
- Try adjusting `--volume` flag
|
|
741
930
|
- On Linux, verify audio subsystem (ALSA/PulseAudio) is working
|
|
931
|
+
- On Windows, install ffplay (`choco install ffmpeg`) for best results
|
|
932
|
+
- On Windows without external players, the PowerShell fallback will be used
|
|
742
933
|
|
|
743
934
|
6. **Slow Response Times**
|
|
744
935
|
- Voice previews are cached after first use
|
|
@@ -5,15 +5,45 @@
|
|
|
5
5
|

|
|
6
6
|
|
|
7
7
|

|
|
8
|
-

|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
A powerful command-line text-to-speech tool supporting multiple TTS providers (ElevenLabs, OpenAI, and Kokoro ONNX) with intelligent voice caching, name resolution, and flexible output options.
|
|
12
12
|
|
|
13
13
|
[](https://buymeacoffee.com/probello3)
|
|
14
14
|
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [What's New](#whats-new)
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Technology Stack](#technology-stack)
|
|
20
|
+
- [Prerequisites](#prerequisites)
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [From PyPI](#installation-from-pypi-recommended)
|
|
23
|
+
- [From Source](#installation-from-source)
|
|
24
|
+
- [Kokoro ONNX Setup](#kokoro-onnx-setup)
|
|
25
|
+
- [Using with AI Agents](#using-with-ai-agents)
|
|
26
|
+
- [Claude Code Setup](#claude-code-setup)
|
|
27
|
+
- [Claude Code Output Style](#claude-code-output-style)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [Command Line Options](#command-line-options)
|
|
31
|
+
- [Providers](#providers)
|
|
32
|
+
- [Cache Locations](#cache-locations)
|
|
33
|
+
- [Development](#development)
|
|
34
|
+
- [Troubleshooting](#troubleshooting)
|
|
35
|
+
- [Contributing](#contributing)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
- [Author](#author)
|
|
38
|
+
- [Acknowledgments](#acknowledgments)
|
|
39
|
+
- [Support](#support)
|
|
40
|
+
|
|
15
41
|
## What's New
|
|
16
42
|
|
|
43
|
+
### v0.4.1
|
|
44
|
+
- **Claude Code output style installer** - New `install-claude-style` command to automatically set up TTS audio summaries
|
|
45
|
+
- **Bundled output style** - TTS Summary output style included with the package
|
|
46
|
+
|
|
17
47
|
### v0.4.0
|
|
18
48
|
- **OpenAI gpt-4o-mini-tts** - New steerable TTS model with `--instructions` option
|
|
19
49
|
- **7 new OpenAI voices** - ash, ballad, coral, sage, verse, marin, cedar (13 total)
|
|
@@ -43,7 +73,7 @@ A powerful command-line text-to-speech tool supporting multiple TTS providers (E
|
|
|
43
73
|
- **Smarter Voice Cache** - Change detection, manual refresh, and voice sample caching
|
|
44
74
|
- **Consistent Error Handling** - Clear error messages with proper exit codes
|
|
45
75
|
- **Multiple Input Methods** - Direct text, stdin piping, and file input (`@filename`)
|
|
46
|
-
- **Volume Control** - Adjust playback volume (0.0 to 5.0)
|
|
76
|
+
- **Volume Control** - Adjust playback volume (0.0 to 5.0) across all platforms (macOS, Linux, Windows)
|
|
47
77
|
- **Voice Preview** - Test voices with sample text before using
|
|
48
78
|
|
|
49
79
|
### Improvements
|
|
@@ -59,7 +89,7 @@ A powerful command-line text-to-speech tool supporting multiple TTS providers (E
|
|
|
59
89
|
- **Configuration File** - Set default preferences in YAML config file (`~/.config/par-tts/config.yaml`)
|
|
60
90
|
- **Flexible Input Methods** - Accept text from command line, stdin pipe, or files (`@filename`)
|
|
61
91
|
- **Voice Name Support** - Use voice names like "Juniper" or "nova" instead of cryptic IDs
|
|
62
|
-
- **Volume Control** - Adjust playback volume (0.0 to 5.0)
|
|
92
|
+
- **Volume Control** - Adjust playback volume (0.0 to 5.0) across all platforms (macOS, Linux, Windows)
|
|
63
93
|
- **Voice Preview** - Test voices with sample text using `--preview-voice`
|
|
64
94
|
- **Smart Voice Caching** - Change detection, auto-refresh, and voice sample caching
|
|
65
95
|
- **Partial Name Matching** - Type "char" to match "Charlotte" (ElevenLabs)
|
|
@@ -100,6 +130,43 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
|
100
130
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
101
131
|
```
|
|
102
132
|
|
|
133
|
+
### Windows Audio Requirements
|
|
134
|
+
|
|
135
|
+
For the best audio playback experience on Windows with volume control, install one of these audio players:
|
|
136
|
+
|
|
137
|
+
#### ffplay (Recommended)
|
|
138
|
+
```powershell
|
|
139
|
+
# Using Chocolatey
|
|
140
|
+
choco install ffmpeg
|
|
141
|
+
|
|
142
|
+
# Using Scoop
|
|
143
|
+
scoop install ffmpeg
|
|
144
|
+
|
|
145
|
+
# Using winget
|
|
146
|
+
winget install ffmpeg
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### VLC Media Player (Alternative)
|
|
150
|
+
Download from [videolan.org](https://www.videolan.org/vlc/) or:
|
|
151
|
+
```powershell
|
|
152
|
+
# Using Chocolatey
|
|
153
|
+
choco install vlc
|
|
154
|
+
|
|
155
|
+
# Using winget
|
|
156
|
+
winget install VideoLAN.VLC
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### mpg123 (Lightweight Option)
|
|
160
|
+
```powershell
|
|
161
|
+
# Using Chocolatey
|
|
162
|
+
choco install mpg123
|
|
163
|
+
|
|
164
|
+
# Using Scoop
|
|
165
|
+
scoop install mpg123
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Note**: If no external player is installed, PAR CLI TTS will use Windows PowerShell's built-in MediaPlayer COM object as a fallback. This provides basic playback with volume control (capped at 1.0/100%). For full volume control up to 5.0x, install ffplay, VLC, or mpg123.
|
|
169
|
+
|
|
103
170
|
## Installation
|
|
104
171
|
|
|
105
172
|
### Installation from PyPI (Recommended)
|
|
@@ -195,6 +262,121 @@ export KOKORO_VOICE_PATH=/path/to/voices-v1.0.bin
|
|
|
195
262
|
|
|
196
263
|
When these environment variables are set, automatic download is disabled.
|
|
197
264
|
|
|
265
|
+
## Using with AI Agents
|
|
266
|
+
|
|
267
|
+
PAR CLI TTS works great with AI agents like [Claude Code](https://claude.ai/code). When using it in an agent, you'll need to grant permission for the agent to run the `par-tts` command.
|
|
268
|
+
|
|
269
|
+
### Claude Code Setup
|
|
270
|
+
|
|
271
|
+
The easiest way to allow Claude Code to use `par-tts` is to add the following to your `~/.claude/settings.json`:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"permissions": {
|
|
276
|
+
"allow": [
|
|
277
|
+
"Bash(par-tts:*)"
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This grants Claude Code permission to run any `par-tts` command without prompting for approval each time.
|
|
284
|
+
|
|
285
|
+
### Example Agent Usage
|
|
286
|
+
|
|
287
|
+
Once configured, your AI agent can easily generate speech:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Agent can run TTS commands directly
|
|
291
|
+
par-tts "Task completed successfully!"
|
|
292
|
+
|
|
293
|
+
# Save audio for notifications
|
|
294
|
+
par-tts "Build finished" --output /tmp/notify.mp3 --no-play
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Claude Code Output Style
|
|
298
|
+
|
|
299
|
+
This project includes a **TTS Summary** output style for Claude Code that provides audio announcements when tasks are completed. This creates a personalized audio feedback experience where Claude announces what it has accomplished.
|
|
300
|
+
|
|
301
|
+
#### Features
|
|
302
|
+
|
|
303
|
+
- Automatic audio summary at the end of every Claude Code response
|
|
304
|
+
- Personalized messages addressing you by name
|
|
305
|
+
- Focus on outcomes and user benefits
|
|
306
|
+
- Natural, conversational language
|
|
307
|
+
|
|
308
|
+
#### Installation
|
|
309
|
+
|
|
310
|
+
The easiest way to install the output style is using the built-in CLI command:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Interactive installation (prompts for your name)
|
|
314
|
+
par-tts-install-style
|
|
315
|
+
|
|
316
|
+
# Non-interactive with name specified
|
|
317
|
+
par-tts-install-style --name "YourName"
|
|
318
|
+
|
|
319
|
+
# Force overwrite if already installed
|
|
320
|
+
par-tts-install-style --name "YourName" --force
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This command will:
|
|
324
|
+
1. Copy the TTS Summary output style to `~/.claude/output-styles/tts-summary.md`
|
|
325
|
+
2. Update `~/.claude/settings.json` with the required `Bash(par-tts:*)` permission
|
|
326
|
+
3. Personalize the output style with your name
|
|
327
|
+
|
|
328
|
+
#### Prerequisites
|
|
329
|
+
|
|
330
|
+
**Important**: Before using this output style, ensure:
|
|
331
|
+
|
|
332
|
+
1. `par-cli-tts` is installed (see [Installation](#installation))
|
|
333
|
+
2. The `install-claude-style` command has been run (automatically grants permissions)
|
|
334
|
+
|
|
335
|
+
If you prefer manual installation, you can:
|
|
336
|
+
1. Copy `.claude/output-styles/tts-summary.md` to `~/.claude/output-styles/`
|
|
337
|
+
2. Add the following to `~/.claude/settings.json`:
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"permissions": {
|
|
341
|
+
"allow": [
|
|
342
|
+
"Bash(par-tts:*)"
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### Usage
|
|
349
|
+
|
|
350
|
+
Activate the output style using the `/output-style` command in Claude Code:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
/output-style tts-summary
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Once activated, Claude will automatically announce completed tasks with audio feedback.
|
|
357
|
+
|
|
358
|
+
#### Customization
|
|
359
|
+
|
|
360
|
+
Edit `~/.claude/output-styles/tts-summary.md` to personalize the experience:
|
|
361
|
+
|
|
362
|
+
1. **Change your name** - Find the `USER_NAME` variable and update it:
|
|
363
|
+
```markdown
|
|
364
|
+
## Variables
|
|
365
|
+
- **USER_NAME**: YourNameHere
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
2. **Update the heading** - Search for "Paul" and replace with your name:
|
|
369
|
+
```markdown
|
|
370
|
+
## Audio Summary for YourNameHere
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
3. **Customize the TTS command** - Use a different voice or provider:
|
|
374
|
+
```markdown
|
|
375
|
+
par-tts "YourNameHere, task completed." --voice nova --provider openai
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
4. **Adjust message style** - Modify the Communication Guidelines section to change how Claude speaks to you
|
|
379
|
+
|
|
198
380
|
## Configuration
|
|
199
381
|
|
|
200
382
|
### Configuration File (Recommended)
|
|
@@ -218,6 +400,10 @@ provider: kokoro-onnx
|
|
|
218
400
|
# Default voice
|
|
219
401
|
voice: Rachel
|
|
220
402
|
|
|
403
|
+
# API keys (optional - can also be set via environment variables)
|
|
404
|
+
# elevenlabs_api_key: your-elevenlabs-api-key-here
|
|
405
|
+
# openai_api_key: your-openai-api-key-here
|
|
406
|
+
|
|
221
407
|
# Output settings
|
|
222
408
|
output_dir: ~/Documents/audio
|
|
223
409
|
keep_temp: false
|
|
@@ -638,6 +824,9 @@ par-cli-tts/
|
|
|
638
824
|
├── docs/
|
|
639
825
|
│ ├── ARCHITECTURE.md # System architecture documentation
|
|
640
826
|
│ └── CLAUDE.md # Development guidelines
|
|
827
|
+
├── .claude/
|
|
828
|
+
│ └── output-styles/
|
|
829
|
+
│ └── tts-summary.md # Claude Code TTS output style
|
|
641
830
|
├── .env.example # Example environment file
|
|
642
831
|
├── pyproject.toml # Project configuration
|
|
643
832
|
├── Makefile # Development commands
|
|
@@ -676,6 +865,8 @@ par-cli-tts/
|
|
|
676
865
|
- Check system volume settings
|
|
677
866
|
- Try adjusting `--volume` flag
|
|
678
867
|
- On Linux, verify audio subsystem (ALSA/PulseAudio) is working
|
|
868
|
+
- On Windows, install ffplay (`choco install ffmpeg`) for best results
|
|
869
|
+
- On Windows without external players, the PowerShell fallback will be used
|
|
679
870
|
|
|
680
871
|
6. **Slow Response Times**
|
|
681
872
|
- Voice previews are cached after first use
|
|
@@ -63,6 +63,7 @@ Wiki = "https://github.com/paulrobello/par-cli-tts/wiki"
|
|
|
63
63
|
[project.scripts]
|
|
64
64
|
par-tts = "src.tts_cli:app"
|
|
65
65
|
par-tts-kokoro = "src.kokoro_cli:app"
|
|
66
|
+
par-tts-install-style = "src.install_claude_style:app"
|
|
66
67
|
|
|
67
68
|
[build-system]
|
|
68
69
|
requires = [
|
|
@@ -99,11 +100,13 @@ packages = [
|
|
|
99
100
|
include = [
|
|
100
101
|
"py.typed",
|
|
101
102
|
"**/*.py",
|
|
103
|
+
".claude/**/*",
|
|
102
104
|
]
|
|
103
105
|
|
|
104
106
|
[tool.hatch.build.targets.sdist]
|
|
105
107
|
include = [
|
|
106
108
|
"src",
|
|
109
|
+
".claude",
|
|
107
110
|
"LICENSE",
|
|
108
111
|
"README.md",
|
|
109
112
|
"pyproject.toml",
|
|
@@ -18,6 +18,10 @@ class ConfigFile(BaseModel):
|
|
|
18
18
|
voice: str | None = Field(None, description="Default voice")
|
|
19
19
|
model: str | None = Field(None, description="Default model")
|
|
20
20
|
|
|
21
|
+
# API keys (optional - can also be set via environment variables)
|
|
22
|
+
elevenlabs_api_key: str | None = Field(None, description="ElevenLabs API key")
|
|
23
|
+
openai_api_key: str | None = Field(None, description="OpenAI API key")
|
|
24
|
+
|
|
21
25
|
# Output settings
|
|
22
26
|
output_dir: str | None = Field(None, description="Default output directory")
|
|
23
27
|
output_format: str | None = Field(None, description="Default output format")
|
|
@@ -102,6 +106,10 @@ class ConfigManager:
|
|
|
102
106
|
"# Default model",
|
|
103
107
|
"# model: eleven_monolingual_v1",
|
|
104
108
|
"",
|
|
109
|
+
"# API keys (optional - can also be set via environment variables)",
|
|
110
|
+
"# elevenlabs_api_key: your-elevenlabs-api-key-here",
|
|
111
|
+
"# openai_api_key: your-openai-api-key-here",
|
|
112
|
+
"",
|
|
105
113
|
"# Output settings",
|
|
106
114
|
"# output_dir: ~/Documents/audio",
|
|
107
115
|
"# output_format: mp3",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Install the TTS Summary output style for Claude Code.
|
|
4
|
+
|
|
5
|
+
This is a separate CLI entry point to avoid conflicts with the main
|
|
6
|
+
par-tts command's argument parsing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Any
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.prompt import Prompt
|
|
16
|
+
|
|
17
|
+
from src.console import console
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
help="Install TTS Summary output style for Claude Code",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def install(
|
|
27
|
+
user_name: Annotated[
|
|
28
|
+
str | None,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"-n",
|
|
31
|
+
"--name",
|
|
32
|
+
help="Your name for personalized audio summaries",
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
force: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"-f",
|
|
39
|
+
"--force",
|
|
40
|
+
help="Overwrite existing output style if it exists",
|
|
41
|
+
),
|
|
42
|
+
] = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Install the TTS Summary output style for Claude Code.
|
|
46
|
+
|
|
47
|
+
This command installs the TTS Summary output style to your Claude Code
|
|
48
|
+
configuration and grants permission for Claude to run par-tts commands.
|
|
49
|
+
After installation, activate it in Claude Code with: /output-style tts-summary
|
|
50
|
+
"""
|
|
51
|
+
# Define paths using cross-platform home directory
|
|
52
|
+
claude_dir = Path.home() / ".claude"
|
|
53
|
+
output_styles_dir = claude_dir / "output-styles"
|
|
54
|
+
settings_file = claude_dir / "settings.json"
|
|
55
|
+
output_style_file = output_styles_dir / "tts-summary.md"
|
|
56
|
+
|
|
57
|
+
# Get the bundled output style from the package
|
|
58
|
+
# Try multiple locations for cross-platform compatibility
|
|
59
|
+
possible_paths = [
|
|
60
|
+
# Installed package location (relative to this file)
|
|
61
|
+
Path(__file__).parent.parent / ".claude" / "output-styles" / "tts-summary.md",
|
|
62
|
+
# Current working directory (for development)
|
|
63
|
+
Path.cwd() / ".claude" / "output-styles" / "tts-summary.md",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
bundled_style = None
|
|
67
|
+
for path in possible_paths:
|
|
68
|
+
if path.exists():
|
|
69
|
+
bundled_style = path
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if not bundled_style:
|
|
73
|
+
console.print("[red]Error: Could not find bundled output style file.[/red]")
|
|
74
|
+
console.print("[dim]Searched locations:[/dim]")
|
|
75
|
+
for path in possible_paths:
|
|
76
|
+
console.print(f" [dim]- {path}[/dim]")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
# Prompt for user name if not provided
|
|
80
|
+
if not user_name:
|
|
81
|
+
user_name = Prompt.ask(
|
|
82
|
+
"[cyan]Enter your name for personalized audio summaries[/cyan]",
|
|
83
|
+
default="User",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Create directories if they don't exist
|
|
87
|
+
try:
|
|
88
|
+
output_styles_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
console.print(f"[dim]Ensured directory exists: {output_styles_dir}[/dim]")
|
|
90
|
+
except PermissionError:
|
|
91
|
+
console.print(f"[red]Error: Permission denied creating directory: {output_styles_dir}[/red]")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
console.print(f"[red]Error creating directory: {e}[/red]")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
|
|
97
|
+
# Check if output style already exists
|
|
98
|
+
if output_style_file.exists() and not force:
|
|
99
|
+
console.print("[yellow]Output style already exists at:[/yellow]", output_style_file)
|
|
100
|
+
console.print("[dim]Use --force to overwrite[/dim]")
|
|
101
|
+
else:
|
|
102
|
+
# Copy and customize the output style
|
|
103
|
+
console.print("[cyan]Installing TTS Summary output style...[/cyan]")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
content = bundled_style.read_text(encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
# Customize the user name in the content
|
|
109
|
+
content = re.sub(
|
|
110
|
+
r"\*\*USER_NAME\*\*:\s*\w+",
|
|
111
|
+
f"**USER_NAME**: {user_name}",
|
|
112
|
+
content,
|
|
113
|
+
)
|
|
114
|
+
content = re.sub(
|
|
115
|
+
r"## Audio Summary for \w+",
|
|
116
|
+
f"## Audio Summary for {user_name}",
|
|
117
|
+
content,
|
|
118
|
+
)
|
|
119
|
+
content = re.sub(
|
|
120
|
+
r"Address \w+ directly",
|
|
121
|
+
f"Address {user_name} directly",
|
|
122
|
+
content,
|
|
123
|
+
)
|
|
124
|
+
content = re.sub(
|
|
125
|
+
r"Paul,",
|
|
126
|
+
f"{user_name},",
|
|
127
|
+
content,
|
|
128
|
+
)
|
|
129
|
+
content = re.sub(
|
|
130
|
+
r'"Paul,',
|
|
131
|
+
f'"{user_name},',
|
|
132
|
+
content,
|
|
133
|
+
)
|
|
134
|
+
content = re.sub(
|
|
135
|
+
r"\[Bash tool call: par-tts \"[^\"]*Paul",
|
|
136
|
+
f'[Bash tool call: par-tts "{user_name}',
|
|
137
|
+
content,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
output_style_file.write_text(content, encoding="utf-8")
|
|
141
|
+
console.print(f"[green]✓ Installed output style to: {output_style_file}[/green]")
|
|
142
|
+
except PermissionError:
|
|
143
|
+
console.print(f"[red]Error: Permission denied writing to: {output_style_file}[/red]")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f"[red]Failed to copy output style: {e}[/red]")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
# Update settings.json with permission
|
|
150
|
+
console.print("[cyan]Updating Claude Code permissions...[/cyan]")
|
|
151
|
+
|
|
152
|
+
settings: dict[str, Any] = {}
|
|
153
|
+
if settings_file.exists():
|
|
154
|
+
try:
|
|
155
|
+
file_content = settings_file.read_text(encoding="utf-8")
|
|
156
|
+
if file_content.strip():
|
|
157
|
+
settings = json.loads(file_content)
|
|
158
|
+
console.print("[dim]Loaded existing settings.json[/dim]")
|
|
159
|
+
else:
|
|
160
|
+
console.print("[dim]settings.json is empty, creating new configuration[/dim]")
|
|
161
|
+
settings = {}
|
|
162
|
+
except json.JSONDecodeError as e:
|
|
163
|
+
console.print(f"[yellow]Warning: Could not parse existing settings.json: {e}[/yellow]")
|
|
164
|
+
console.print("[yellow]Creating backup and new configuration[/yellow]")
|
|
165
|
+
# Create backup
|
|
166
|
+
backup_file = settings_file.with_suffix(".json.backup")
|
|
167
|
+
try:
|
|
168
|
+
settings_file.rename(backup_file)
|
|
169
|
+
console.print(f"[dim]Backup created: {backup_file}[/dim]")
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
settings = {}
|
|
173
|
+
except PermissionError:
|
|
174
|
+
console.print(f"[red]Error: Permission denied reading: {settings_file}[/red]")
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
# Ensure permissions structure exists (handle missing or null values)
|
|
178
|
+
if "permissions" not in settings or settings["permissions"] is None:
|
|
179
|
+
settings["permissions"] = {}
|
|
180
|
+
if "allow" not in settings["permissions"] or settings["permissions"]["allow"] is None:
|
|
181
|
+
settings["permissions"]["allow"] = []
|
|
182
|
+
|
|
183
|
+
# Ensure allow is a list (handle edge cases)
|
|
184
|
+
if not isinstance(settings["permissions"]["allow"], list):
|
|
185
|
+
console.print("[yellow]Warning: permissions.allow is not a list, resetting[/yellow]")
|
|
186
|
+
settings["permissions"]["allow"] = []
|
|
187
|
+
|
|
188
|
+
# Add par-tts permission if not already present (avoid duplicates)
|
|
189
|
+
par_tts_permission = "Bash(par-tts:*)"
|
|
190
|
+
if par_tts_permission not in settings["permissions"]["allow"]:
|
|
191
|
+
settings["permissions"]["allow"].append(par_tts_permission)
|
|
192
|
+
console.print(f"[green]✓ Added permission: {par_tts_permission}[/green]")
|
|
193
|
+
else:
|
|
194
|
+
console.print(f"[dim]Permission already exists: {par_tts_permission}[/dim]")
|
|
195
|
+
|
|
196
|
+
# Write updated settings
|
|
197
|
+
try:
|
|
198
|
+
settings_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
199
|
+
console.print(f"[green]✓ Updated settings: {settings_file}[/green]")
|
|
200
|
+
except PermissionError:
|
|
201
|
+
console.print(f"[red]Error: Permission denied writing to: {settings_file}[/red]")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
console.print(f"[red]Failed to update settings.json: {e}[/red]")
|
|
205
|
+
raise typer.Exit(1)
|
|
206
|
+
|
|
207
|
+
# Success message
|
|
208
|
+
console.print()
|
|
209
|
+
console.print("[bold green]Installation complete![/bold green]")
|
|
210
|
+
console.print()
|
|
211
|
+
console.print("[cyan]To activate the output style in Claude Code, run:[/cyan]")
|
|
212
|
+
console.print(" [bold]/output-style tts-summary[/bold]")
|
|
213
|
+
console.print()
|
|
214
|
+
console.print(f"[dim]Your name is set to: {user_name}[/dim]")
|
|
215
|
+
console.print(f"[dim]To change your name, edit: {output_style_file}[/dim]")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
app()
|
|
@@ -7,12 +7,14 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
with warnings.catch_warnings():
|
|
9
9
|
warnings.filterwarnings("ignore", message="Core Pydantic V1 functionality", category=UserWarning)
|
|
10
|
-
from elevenlabs import VoiceSettings
|
|
10
|
+
from elevenlabs import VoiceSettings
|
|
11
11
|
from elevenlabs.client import ElevenLabs
|
|
12
|
+
from elevenlabs.play import save
|
|
12
13
|
|
|
13
14
|
from src.defaults import DEFAULT_ELEVENLABS_VOICE
|
|
14
15
|
from src.http_client import create_http_client
|
|
15
16
|
from src.providers.base import TTSProvider, Voice
|
|
17
|
+
from src.utils import play_audio_bytes
|
|
16
18
|
from src.voice_cache import VoiceCache, resolve_voice_identifier
|
|
17
19
|
|
|
18
20
|
|
|
@@ -152,20 +154,14 @@ class ElevenLabsProvider(TTSProvider):
|
|
|
152
154
|
|
|
153
155
|
def play_audio(self, audio_data: bytes | Iterator[bytes], volume: float = 1.0) -> None:
|
|
154
156
|
"""
|
|
155
|
-
Play audio data.
|
|
157
|
+
Play audio data with volume control.
|
|
156
158
|
|
|
157
159
|
Args:
|
|
158
160
|
audio_data: Audio data to play.
|
|
159
161
|
volume: Volume level (0.0 = silent, 1.0 = normal, 2.0 = double volume).
|
|
160
|
-
Note: ElevenLabs play() doesn't support volume, will use system default.
|
|
161
162
|
"""
|
|
162
|
-
#
|
|
163
|
-
if isinstance(audio_data, bytes):
|
|
164
|
-
|
|
165
|
-
else:
|
|
166
|
-
# Convert iterator to bytes for playback
|
|
167
|
-
audio_bytes = b"".join(audio_data)
|
|
168
|
-
play(audio_bytes)
|
|
163
|
+
# Convert iterator to bytes if needed
|
|
164
|
+
if not isinstance(audio_data, bytes):
|
|
165
|
+
audio_data = b"".join(audio_data)
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
# Consider using system players with volume support if needed
|
|
167
|
+
play_audio_bytes(audio_data, volume=volume, suffix=".mp3")
|
|
@@ -28,48 +28,60 @@ from src.providers import PROVIDERS, TTSProvider
|
|
|
28
28
|
app = typer.Typer(help="Text-to-speech command line tool with multiple provider support")
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def get_api_key(provider: str) -> str | None:
|
|
31
|
+
def get_api_key(provider: str, config_file: Any = None) -> str | None:
|
|
32
32
|
"""
|
|
33
|
-
Get API key for the specified provider from environment.
|
|
33
|
+
Get API key for the specified provider from config file or environment.
|
|
34
34
|
|
|
35
35
|
Args:
|
|
36
36
|
provider: Provider name (elevenlabs, openai, kokoro-onnx).
|
|
37
|
+
config_file: Optional config file with API keys.
|
|
37
38
|
|
|
38
39
|
Returns:
|
|
39
40
|
API key string or None for providers that don't need one.
|
|
40
41
|
|
|
41
42
|
Raises:
|
|
42
|
-
SystemExit: If API key is not found
|
|
43
|
+
SystemExit: If API key is not found anywhere.
|
|
43
44
|
"""
|
|
44
45
|
# kokoro-onnx doesn't need an API key
|
|
45
46
|
if provider == "kokoro-onnx":
|
|
46
47
|
return None
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
49
|
+
# Map provider to config file field and environment variable
|
|
50
|
+
key_map = {
|
|
51
|
+
"elevenlabs": ("elevenlabs_api_key", "ELEVENLABS_API_KEY"),
|
|
52
|
+
"openai": ("openai_api_key", "OPENAI_API_KEY"),
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
if not env_var:
|
|
55
|
+
if provider not in key_map:
|
|
55
56
|
handle_error(f"Unknown provider '{provider}'", ErrorType.INVALID_PROVIDER)
|
|
56
57
|
return None # For type checker, never reached
|
|
57
58
|
|
|
59
|
+
config_field, env_var = key_map[provider]
|
|
60
|
+
|
|
61
|
+
# Check config file first
|
|
62
|
+
if config_file:
|
|
63
|
+
api_key = getattr(config_file, config_field, None)
|
|
64
|
+
if api_key:
|
|
65
|
+
return api_key
|
|
66
|
+
|
|
67
|
+
# Fall back to environment variable
|
|
58
68
|
api_key = os.getenv(env_var)
|
|
59
|
-
if not api_key
|
|
69
|
+
if not api_key:
|
|
60
70
|
handle_error(
|
|
61
|
-
f"{env_var} not found. Please set {env_var} in your
|
|
71
|
+
f"{env_var} not found. Please set {env_var} in your config file or environment",
|
|
72
|
+
ErrorType.MISSING_API_KEY,
|
|
62
73
|
)
|
|
63
74
|
|
|
64
75
|
return api_key
|
|
65
76
|
|
|
66
77
|
|
|
67
|
-
def create_provider(provider_name: str, **kwargs: Any) -> TTSProvider:
|
|
78
|
+
def create_provider(provider_name: str, config_file: Any = None, **kwargs: Any) -> TTSProvider:
|
|
68
79
|
"""
|
|
69
80
|
Create a TTS provider instance.
|
|
70
81
|
|
|
71
82
|
Args:
|
|
72
83
|
provider_name: Name of the provider.
|
|
84
|
+
config_file: Optional config file with API keys.
|
|
73
85
|
**kwargs: Additional provider configuration.
|
|
74
86
|
|
|
75
87
|
Returns:
|
|
@@ -83,7 +95,7 @@ def create_provider(provider_name: str, **kwargs: Any) -> TTSProvider:
|
|
|
83
95
|
f"Unknown provider '{provider_name}'. Available: {', '.join(PROVIDERS.keys())}", ErrorType.INVALID_PROVIDER
|
|
84
96
|
)
|
|
85
97
|
|
|
86
|
-
api_key = get_api_key(provider_name)
|
|
98
|
+
api_key = get_api_key(provider_name, config_file)
|
|
87
99
|
validate_api_key(api_key, provider_name)
|
|
88
100
|
provider_class = PROVIDERS[provider_name]
|
|
89
101
|
|
|
@@ -477,7 +489,10 @@ def handle_speech_generation(
|
|
|
477
489
|
|
|
478
490
|
if play_audio:
|
|
479
491
|
console.print("[cyan]Playing audio...[/cyan]")
|
|
480
|
-
|
|
492
|
+
# Read from saved file since iterator was consumed
|
|
493
|
+
with open(output_path, "rb") as f:
|
|
494
|
+
audio_bytes = f.read()
|
|
495
|
+
tts_provider.play_audio(audio_bytes, volume=volume)
|
|
481
496
|
else:
|
|
482
497
|
if play_audio:
|
|
483
498
|
console.print("[cyan]Playing audio...[/cyan]")
|
|
@@ -496,7 +511,10 @@ def handle_speech_generation(
|
|
|
496
511
|
tts_provider.save_audio(audio_data, tmp_path)
|
|
497
512
|
|
|
498
513
|
try:
|
|
499
|
-
|
|
514
|
+
# Read from saved file since iterator was consumed
|
|
515
|
+
with open(tmp_path, "rb") as f:
|
|
516
|
+
audio_bytes = f.read()
|
|
517
|
+
tts_provider.play_audio(audio_bytes, volume=volume)
|
|
500
518
|
|
|
501
519
|
if keep_temp or temp_dir:
|
|
502
520
|
console.print(f"[green]Audio saved to: {tmp_path}[/green]")
|
|
@@ -543,14 +561,14 @@ def main(
|
|
|
543
561
|
str | None, typer.Argument(help="Text to convert to speech. Use '-' for stdin, '@filename' to read from file")
|
|
544
562
|
] = None,
|
|
545
563
|
provider: Annotated[
|
|
546
|
-
str,
|
|
564
|
+
str | None,
|
|
547
565
|
typer.Option(
|
|
548
566
|
"-P",
|
|
549
567
|
"--provider",
|
|
550
568
|
help="TTS provider to use (elevenlabs, openai, kokoro-onnx)",
|
|
551
569
|
envvar="TTS_PROVIDER",
|
|
552
570
|
),
|
|
553
|
-
] =
|
|
571
|
+
] = None,
|
|
554
572
|
voice: Annotated[
|
|
555
573
|
str | None,
|
|
556
574
|
typer.Option(
|
|
@@ -754,7 +772,7 @@ def main(
|
|
|
754
772
|
|
|
755
773
|
# Apply config file defaults (CLI args override these)
|
|
756
774
|
if config_file:
|
|
757
|
-
provider = provider or config_file.provider
|
|
775
|
+
provider = provider or config_file.provider
|
|
758
776
|
voice = voice or config_file.voice
|
|
759
777
|
model = model or config_file.model
|
|
760
778
|
output = output or (Path(config_file.output_dir) / "output.mp3" if config_file.output_dir else None)
|
|
@@ -771,6 +789,10 @@ def main(
|
|
|
771
789
|
)
|
|
772
790
|
debug = debug or config_file.debug or False
|
|
773
791
|
|
|
792
|
+
# Apply default provider if still not set
|
|
793
|
+
if not provider:
|
|
794
|
+
provider = DEFAULT_PROVIDER
|
|
795
|
+
|
|
774
796
|
# Store debug mode globally for error handler
|
|
775
797
|
sys._debug_mode = debug # type: ignore
|
|
776
798
|
|
|
@@ -794,7 +816,7 @@ def main(
|
|
|
794
816
|
return
|
|
795
817
|
|
|
796
818
|
# Create provider
|
|
797
|
-
tts_provider = create_provider(provider)
|
|
819
|
+
tts_provider = create_provider(provider, config_file)
|
|
798
820
|
|
|
799
821
|
# Handle cache management operations (ElevenLabs only)
|
|
800
822
|
if refresh_cache or clear_cache_samples:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Utility functions for PAR CLI TTS."""
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
import tempfile
|
|
@@ -138,8 +139,7 @@ def play_audio_with_player(file_path: Path, volume: float = 1.0) -> None:
|
|
|
138
139
|
# afplay supports volume flag (-v)
|
|
139
140
|
subprocess.run(["afplay", "-v", str(volume), str(file_path)], check=True)
|
|
140
141
|
elif sys.platform == "win32": # Windows
|
|
141
|
-
|
|
142
|
-
subprocess.run(["start", "", str(file_path)], shell=True, check=True)
|
|
142
|
+
_play_audio_windows(file_path, volume)
|
|
143
143
|
else: # Linux and others
|
|
144
144
|
# Try common audio players with volume support
|
|
145
145
|
players_with_volume = [
|
|
@@ -160,6 +160,154 @@ def play_audio_with_player(file_path: Path, volume: float = 1.0) -> None:
|
|
|
160
160
|
raise RuntimeError("No audio player found. Install aplay, paplay, ffplay, or mpg123.")
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def _find_windows_audio_player() -> str | None:
|
|
164
|
+
"""Find an available audio player on Windows.
|
|
165
|
+
|
|
166
|
+
Checks for ffplay, VLC, and mpg123 in that order.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Name of the found player, or None if no player is available.
|
|
170
|
+
"""
|
|
171
|
+
# Prefer ffplay (comes with ffmpeg, most reliable)
|
|
172
|
+
if shutil.which("ffplay"):
|
|
173
|
+
return "ffplay"
|
|
174
|
+
|
|
175
|
+
# VLC media player
|
|
176
|
+
vlc_paths = [
|
|
177
|
+
r"C:\Program Files\VideoLAN\VLC\vlc.exe",
|
|
178
|
+
r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
|
|
179
|
+
]
|
|
180
|
+
for vlc_path in vlc_paths:
|
|
181
|
+
if Path(vlc_path).exists():
|
|
182
|
+
return "vlc"
|
|
183
|
+
|
|
184
|
+
# mpg123 for Windows
|
|
185
|
+
if shutil.which("mpg123"):
|
|
186
|
+
return "mpg123"
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _play_with_powershell(file_path: Path, volume: float, timeout: int = 60) -> None:
|
|
192
|
+
"""Play audio using PowerShell MediaPlayer COM object.
|
|
193
|
+
|
|
194
|
+
This is a fallback for Windows when no external player is available.
|
|
195
|
+
Uses the Windows built-in MediaPlayer COM object for reliable playback.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
file_path: Path to the audio file to play.
|
|
199
|
+
volume: Volume level (0.0 = silent, 1.0 = normal, 2.0 = double).
|
|
200
|
+
Note: MediaPlayer volume range is 0.0 to 1.0, values above 1.0
|
|
201
|
+
are capped at 1.0.
|
|
202
|
+
timeout: Maximum playback time in seconds.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
RuntimeError: If PowerShell execution fails.
|
|
206
|
+
"""
|
|
207
|
+
# MediaPlayer volume is 0.0 to 1.0, cap values above 1.0
|
|
208
|
+
media_volume = min(volume, 1.0)
|
|
209
|
+
|
|
210
|
+
# PowerShell script using MediaPlayer COM object
|
|
211
|
+
ps_script = f"""
|
|
212
|
+
$file = "{file_path}"
|
|
213
|
+
$volume = {media_volume}
|
|
214
|
+
|
|
215
|
+
$player = New-Object -ComObject MediaPlayer.MediaPlayer
|
|
216
|
+
$player.Open($file)
|
|
217
|
+
$player.Volume = $volume
|
|
218
|
+
|
|
219
|
+
# Wait for media to be ready
|
|
220
|
+
while ($player.ReadyState -lt 3) {{
|
|
221
|
+
Start-Sleep -Milliseconds 100
|
|
222
|
+
}}
|
|
223
|
+
|
|
224
|
+
# Get duration and wait for playback
|
|
225
|
+
$duration = $player.Duration
|
|
226
|
+
if ($duration -gt 0) {{
|
|
227
|
+
$player.Play()
|
|
228
|
+
Start-Sleep -Seconds $duration
|
|
229
|
+
}} else {{
|
|
230
|
+
# If duration not available, wait up to {timeout} seconds
|
|
231
|
+
$player.Play()
|
|
232
|
+
Start-Sleep -Seconds {timeout}
|
|
233
|
+
}}
|
|
234
|
+
|
|
235
|
+
$player.Close()
|
|
236
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($player) | Out-Null
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
subprocess.run(
|
|
241
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
|
|
242
|
+
check=True,
|
|
243
|
+
timeout=timeout + 10, # Extra buffer for startup/shutdown
|
|
244
|
+
)
|
|
245
|
+
except subprocess.TimeoutExpired as e:
|
|
246
|
+
raise RuntimeError(f"Audio playback timed out after {timeout} seconds") from e
|
|
247
|
+
except subprocess.CalledProcessError as e:
|
|
248
|
+
raise RuntimeError(f"PowerShell audio playback failed: {e}") from e
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _play_audio_windows(file_path: Path, volume: float = 1.0) -> None:
|
|
252
|
+
"""Play audio on Windows with volume control.
|
|
253
|
+
|
|
254
|
+
Tries external players first (ffplay, VLC, mpg123), then falls back
|
|
255
|
+
to PowerShell MediaPlayer COM object.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
file_path: Path to the audio file to play.
|
|
259
|
+
volume: Volume level (0.0 = silent, 1.0 = normal, 2.0 = double).
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
RuntimeError: If no audio playback method succeeds.
|
|
263
|
+
"""
|
|
264
|
+
player = _find_windows_audio_player()
|
|
265
|
+
|
|
266
|
+
if player == "ffplay":
|
|
267
|
+
# ffplay uses 0-100 volume scale
|
|
268
|
+
subprocess.run(
|
|
269
|
+
[
|
|
270
|
+
"ffplay",
|
|
271
|
+
"-volume",
|
|
272
|
+
str(int(volume * 100)),
|
|
273
|
+
"-nodisp",
|
|
274
|
+
"-autoexit",
|
|
275
|
+
"-hide_banner",
|
|
276
|
+
"-loglevel",
|
|
277
|
+
"error",
|
|
278
|
+
str(file_path),
|
|
279
|
+
],
|
|
280
|
+
check=True,
|
|
281
|
+
)
|
|
282
|
+
elif player == "vlc":
|
|
283
|
+
# VLC volume scale: 0-256 where 256 is 100%
|
|
284
|
+
# Higher values possible but 512 is 200%
|
|
285
|
+
vlc_path = r"C:\Program Files\VideoLAN\VLC\vlc.exe"
|
|
286
|
+
if not Path(vlc_path).exists():
|
|
287
|
+
vlc_path = r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe"
|
|
288
|
+
subprocess.run(
|
|
289
|
+
[
|
|
290
|
+
vlc_path,
|
|
291
|
+
"--intf",
|
|
292
|
+
"dummy",
|
|
293
|
+
"--play-and-exit",
|
|
294
|
+
"--volume",
|
|
295
|
+
str(int(volume * 256)),
|
|
296
|
+
str(file_path),
|
|
297
|
+
],
|
|
298
|
+
check=True,
|
|
299
|
+
)
|
|
300
|
+
elif player == "mpg123":
|
|
301
|
+
# mpg123 uses scale factor (32768 = normal)
|
|
302
|
+
subprocess.run(
|
|
303
|
+
["mpg123", "-f", str(int(volume * 32768)), str(file_path)],
|
|
304
|
+
check=True,
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
# Fallback to PowerShell MediaPlayer
|
|
308
|
+
_play_with_powershell(file_path, volume)
|
|
309
|
+
|
|
310
|
+
|
|
163
311
|
def play_audio_bytes(audio_data: bytes, volume: float = 1.0, suffix: str = ".mp3") -> None:
|
|
164
312
|
"""Play audio data from bytes using system player.
|
|
165
313
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|