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.
Files changed (25) hide show
  1. par_cli_tts-0.4.2/.claude/output-styles/tts-summary.md +78 -0
  2. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/.gitignore +1 -0
  3. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/PKG-INFO +195 -4
  4. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/README.md +194 -3
  5. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/pyproject.toml +3 -0
  6. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/__init__.py +1 -1
  7. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/config_file.py +8 -0
  8. par_cli_tts-0.4.2/src/install_claude_style.py +219 -0
  9. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/elevenlabs.py +8 -12
  10. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/tts_cli.py +40 -18
  11. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/utils.py +150 -2
  12. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/LICENSE +0 -0
  13. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/config.py +0 -0
  14. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/console.py +0 -0
  15. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/defaults.py +0 -0
  16. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/errors.py +0 -0
  17. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/http_client.py +0 -0
  18. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/kokoro_cli.py +0 -0
  19. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/model_downloader.py +0 -0
  20. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/__init__.py +0 -0
  21. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/base.py +0 -0
  22. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/kokoro_onnx.py +0 -0
  23. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/providers/openai.py +0 -0
  24. {par_cli_tts-0.4.0 → par_cli_tts-0.4.2}/src/py.typed +0 -0
  25. {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.
@@ -269,3 +269,4 @@ junit.xml
269
269
  # Documentation builds
270
270
  docs/_build/
271
271
  docs/.doctrees/
272
+ settings.local.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: par-cli-tts
3
- Version: 0.4.0
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
  ![Arch x86-63 | ARM | AppleSilicon](https://img.shields.io/badge/arch-x86--64%20%7C%20ARM%20%7C%20AppleSilicon-blue)
69
69
 
70
70
  ![MIT License](https://img.shields.io/badge/license-MIT-green.svg)
71
- ![Version](https://img.shields.io/badge/version-0.4.0-green.svg)
71
+ ![Version](https://img.shields.io/badge/version-0.4.1-green.svg)
72
72
  ![Development Status](https://img.shields.io/badge/status-stable-green.svg)
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
  [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](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) with platform-specific support
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) with platform-specific support
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
  ![Arch x86-63 | ARM | AppleSilicon](https://img.shields.io/badge/arch-x86--64%20%7C%20ARM%20%7C%20AppleSilicon-blue)
6
6
 
7
7
  ![MIT License](https://img.shields.io/badge/license-MIT-green.svg)
8
- ![Version](https://img.shields.io/badge/version-0.4.0-green.svg)
8
+ ![Version](https://img.shields.io/badge/version-0.4.1-green.svg)
9
9
  ![Development Status](https://img.shields.io/badge/status-stable-green.svg)
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
  [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](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) with platform-specific support
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) with platform-specific support
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",
@@ -1,3 +1,3 @@
1
1
  """PAR CLI TTS - Command line text-to-speech tool."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.2"
@@ -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, play, save
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
- # For play, we need to convert iterator to bytes
163
- if isinstance(audio_data, bytes):
164
- play(audio_data)
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
- # Note: ElevenLabs play() function doesn't support volume control
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 in environment.
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
- env_var_map = {
49
- "elevenlabs": "ELEVENLABS_API_KEY",
50
- "openai": "OPENAI_API_KEY",
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
- env_var = env_var_map.get(provider)
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 and provider != "kokoro-onnx":
69
+ if not api_key:
60
70
  handle_error(
61
- f"{env_var} not found. Please set {env_var} in your .env file or environment", ErrorType.MISSING_API_KEY
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
- tts_provider.play_audio(audio_data, volume=volume)
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
- tts_provider.play_audio(audio_data, volume=volume)
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
- ] = DEFAULT_PROVIDER,
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 or DEFAULT_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
- # Windows doesn't have native volume control for start command
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