voice-mode 2.20.1__tar.gz → 2.21.1__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.
- {voice_mode-2.20.1 → voice_mode-2.21.1}/CHANGELOG.md +16 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/PKG-INFO +1 -1
- {voice_mode-2.20.1 → voice_mode-2.21.1}/pyproject.toml +0 -1
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/__version__.py +1 -1
- voice_mode-2.21.1/voice_mode/cli.py +593 -0
- voice_mode-2.20.1/voice_mode/cli.py +0 -28
- {voice_mode-2.20.1 → voice_mode-2.21.1}/.gitignore +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/README.md +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/__main__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/cli_commands/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/cli_commands/exchanges.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/config.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/conversation_logger.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/core.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/data/versions.json +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/conversations.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/filters.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/formatters.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/models.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/reader.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/exchanges/stats.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/prompts/README.md +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/prompts/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/prompts/converse.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/prompts/release_notes.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/prompts/services.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/provider_discovery.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/providers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/audio_files.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/changelog.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/configuration.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/statistics.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/version.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/resources/whisper_models.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/server.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/shared.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/simple_failover.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/statistics.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/streaming.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/configuration_management.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/converse.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/dependencies.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/devices.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/diagnostics.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/providers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/service.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/kokoro/install.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/list_versions.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/version_info.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/whisper/download_model.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/whisper/install.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/services/whisper/uninstall.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/statistics.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/tools/voice_registry.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/__init__.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/audio_diagnostics.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/event_logger.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/ffmpeg_check.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/format_migration.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/gpu_detection.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/migration_helpers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/services/common.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/services/kokoro_helpers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/services/whisper_helpers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/utils/version_helpers.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/version.py +0 -0
- {voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/voice_preferences.py +0 -0
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [2.21.1] - 2025-08-13
|
11
|
+
|
12
|
+
- Late update to changelog for release 2.21.0
|
13
|
+
|
14
|
+
## [2.21.0] - 2025-08-13
|
15
|
+
|
16
|
+
### Added
|
17
|
+
- **CLI service commands** - New subcommands for managing Whisper and Kokoro services
|
18
|
+
- `voice-mode service whisper start/stop/status/restart/enable/disable/logs`
|
19
|
+
- `voice-mode service kokoro start/stop/status/restart/enable/disable/logs`
|
20
|
+
- `voice-mode service whisper update-service-files` - Update systemd/launchd service files
|
21
|
+
- `voice-mode service kokoro update-service-files` - Update systemd/launchd service files
|
22
|
+
- Unified interface for controlling both STT and TTS services
|
23
|
+
- Direct access to service management without needing MCP client
|
24
|
+
- Consistent behavior across Linux (systemd) and macOS (launchd)
|
25
|
+
|
10
26
|
## [2.20.1] - 2025-08-11
|
11
27
|
|
12
28
|
### Fixed
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: voice-mode
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.21.1
|
4
4
|
Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
|
5
5
|
Project-URL: Homepage, https://github.com/mbailey/voicemode
|
6
6
|
Project-URL: Repository, https://github.com/mbailey/voicemode
|
@@ -90,7 +90,6 @@ Issues = "https://github.com/mbailey/voicemode/issues"
|
|
90
90
|
[project.scripts]
|
91
91
|
voice-mode = "voice_mode.cli:voice_mode"
|
92
92
|
voicemode = "voice_mode.cli:voice_mode"
|
93
|
-
voice-mode-cli = "voice_mode.cli:cli"
|
94
93
|
|
95
94
|
[tool.hatch.build.targets.wheel]
|
96
95
|
packages = ["voice_mode"]
|
@@ -0,0 +1,593 @@
|
|
1
|
+
"""
|
2
|
+
CLI entry points for voice-mode package.
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
import sys
|
6
|
+
import click
|
7
|
+
from .server import main as voice_mode_main
|
8
|
+
|
9
|
+
|
10
|
+
# Service management CLI - runs MCP server by default, subcommands override
|
11
|
+
@click.group(invoke_without_command=True)
|
12
|
+
@click.version_option()
|
13
|
+
@click.help_option('-h', '--help', help='Show this message and exit')
|
14
|
+
@click.pass_context
|
15
|
+
def voice_mode_main_cli(ctx):
|
16
|
+
"""Voice Mode - MCP server and service management.
|
17
|
+
|
18
|
+
Without arguments, starts the MCP server.
|
19
|
+
With subcommands, executes service management operations.
|
20
|
+
"""
|
21
|
+
if ctx.invoked_subcommand is None:
|
22
|
+
# No subcommand - run MCP server
|
23
|
+
voice_mode_main()
|
24
|
+
|
25
|
+
|
26
|
+
def voice_mode() -> None:
|
27
|
+
"""Entry point for voicemode command - starts the MCP server or runs subcommands."""
|
28
|
+
voice_mode_main_cli()
|
29
|
+
|
30
|
+
|
31
|
+
# Service group commands
|
32
|
+
@voice_mode_main_cli.group()
|
33
|
+
def kokoro():
|
34
|
+
"""Manage Kokoro TTS service."""
|
35
|
+
pass
|
36
|
+
|
37
|
+
|
38
|
+
@voice_mode_main_cli.group()
|
39
|
+
def whisper():
|
40
|
+
"""Manage Whisper STT service."""
|
41
|
+
pass
|
42
|
+
|
43
|
+
|
44
|
+
# Import service functions
|
45
|
+
from voice_mode.tools.service import (
|
46
|
+
status_service, start_service, stop_service, restart_service,
|
47
|
+
enable_service, disable_service, view_logs, update_service_files
|
48
|
+
)
|
49
|
+
|
50
|
+
# Import install/uninstall functions
|
51
|
+
from voice_mode.tools.services.kokoro.install import kokoro_install
|
52
|
+
from voice_mode.tools.services.kokoro.uninstall import kokoro_uninstall
|
53
|
+
from voice_mode.tools.services.whisper.install import whisper_install
|
54
|
+
from voice_mode.tools.services.whisper.uninstall import whisper_uninstall
|
55
|
+
from voice_mode.tools.services.whisper.download_model import download_model
|
56
|
+
|
57
|
+
# Import configuration management functions
|
58
|
+
from voice_mode.tools.configuration_management import update_config, list_config_keys
|
59
|
+
|
60
|
+
# Import diagnostic functions - extract the actual async functions from the tools
|
61
|
+
from voice_mode.tools.diagnostics import voice_mode_info
|
62
|
+
from voice_mode.tools.devices import check_audio_devices
|
63
|
+
from voice_mode.tools.voice_registry import voice_registry
|
64
|
+
from voice_mode.tools.dependencies import check_audio_dependencies
|
65
|
+
|
66
|
+
|
67
|
+
# Kokoro service commands
|
68
|
+
@kokoro.command()
|
69
|
+
def status():
|
70
|
+
"""Show Kokoro service status."""
|
71
|
+
result = asyncio.run(status_service("kokoro"))
|
72
|
+
click.echo(result)
|
73
|
+
|
74
|
+
|
75
|
+
@kokoro.command()
|
76
|
+
def start():
|
77
|
+
"""Start Kokoro service."""
|
78
|
+
result = asyncio.run(start_service("kokoro"))
|
79
|
+
click.echo(result)
|
80
|
+
|
81
|
+
|
82
|
+
@kokoro.command()
|
83
|
+
def stop():
|
84
|
+
"""Stop Kokoro service."""
|
85
|
+
result = asyncio.run(stop_service("kokoro"))
|
86
|
+
click.echo(result)
|
87
|
+
|
88
|
+
|
89
|
+
@kokoro.command()
|
90
|
+
def restart():
|
91
|
+
"""Restart Kokoro service."""
|
92
|
+
result = asyncio.run(restart_service("kokoro"))
|
93
|
+
click.echo(result)
|
94
|
+
|
95
|
+
|
96
|
+
@kokoro.command()
|
97
|
+
def enable():
|
98
|
+
"""Enable Kokoro service to start at boot/login."""
|
99
|
+
result = asyncio.run(enable_service("kokoro"))
|
100
|
+
click.echo(result)
|
101
|
+
|
102
|
+
|
103
|
+
@kokoro.command()
|
104
|
+
def disable():
|
105
|
+
"""Disable Kokoro service from starting at boot/login."""
|
106
|
+
result = asyncio.run(disable_service("kokoro"))
|
107
|
+
click.echo(result)
|
108
|
+
|
109
|
+
|
110
|
+
@kokoro.command()
|
111
|
+
@click.option('--lines', '-n', default=50, help='Number of log lines to show')
|
112
|
+
def logs(lines):
|
113
|
+
"""View Kokoro service logs."""
|
114
|
+
result = asyncio.run(view_logs("kokoro", lines))
|
115
|
+
click.echo(result)
|
116
|
+
|
117
|
+
|
118
|
+
@kokoro.command("update-service-files")
|
119
|
+
def kokoro_update_service_files():
|
120
|
+
"""Update Kokoro service files to latest version."""
|
121
|
+
result = asyncio.run(update_service_files("kokoro"))
|
122
|
+
click.echo(result)
|
123
|
+
|
124
|
+
|
125
|
+
@kokoro.command()
|
126
|
+
def health():
|
127
|
+
"""Check Kokoro health endpoint."""
|
128
|
+
import subprocess
|
129
|
+
try:
|
130
|
+
result = subprocess.run(
|
131
|
+
["curl", "-s", "http://127.0.0.1:8880/health"],
|
132
|
+
capture_output=True, text=True, timeout=5
|
133
|
+
)
|
134
|
+
if result.returncode == 0:
|
135
|
+
import json
|
136
|
+
try:
|
137
|
+
health_data = json.loads(result.stdout)
|
138
|
+
click.echo("✅ Kokoro is responding")
|
139
|
+
click.echo(f" Status: {health_data.get('status', 'unknown')}")
|
140
|
+
if 'uptime' in health_data:
|
141
|
+
click.echo(f" Uptime: {health_data['uptime']}")
|
142
|
+
except json.JSONDecodeError:
|
143
|
+
click.echo("✅ Kokoro is responding (non-JSON response)")
|
144
|
+
else:
|
145
|
+
click.echo("❌ Kokoro not responding on port 8880")
|
146
|
+
except subprocess.TimeoutExpired:
|
147
|
+
click.echo("❌ Kokoro health check timed out")
|
148
|
+
except Exception as e:
|
149
|
+
click.echo(f"❌ Health check failed: {e}")
|
150
|
+
|
151
|
+
|
152
|
+
@kokoro.command()
|
153
|
+
@click.option('--install-dir', help='Directory to install kokoro-fastapi')
|
154
|
+
@click.option('--port', default=8880, help='Port to configure for the service')
|
155
|
+
@click.option('--force', '-f', is_flag=True, help='Force reinstall even if already installed')
|
156
|
+
@click.option('--version', default='latest', help='Version to install (default: latest)')
|
157
|
+
@click.option('--auto-enable/--no-auto-enable', default=None, help='Enable service at boot/login')
|
158
|
+
def install(install_dir, port, force, version, auto_enable):
|
159
|
+
"""Install kokoro-fastapi TTS service."""
|
160
|
+
result = asyncio.run(kokoro_install.fn(
|
161
|
+
install_dir=install_dir,
|
162
|
+
port=port,
|
163
|
+
force_reinstall=force,
|
164
|
+
version=version,
|
165
|
+
auto_enable=auto_enable
|
166
|
+
))
|
167
|
+
|
168
|
+
if result.get('success'):
|
169
|
+
if result.get('already_installed'):
|
170
|
+
click.echo(f"✅ Kokoro already installed at {result['install_path']}")
|
171
|
+
click.echo(f" Version: {result.get('version', 'unknown')}")
|
172
|
+
else:
|
173
|
+
click.echo("✅ Kokoro installed successfully!")
|
174
|
+
click.echo(f" Install path: {result['install_path']}")
|
175
|
+
click.echo(f" Version: {result.get('version', 'unknown')}")
|
176
|
+
|
177
|
+
if result.get('enabled'):
|
178
|
+
click.echo(" Auto-start: Enabled")
|
179
|
+
|
180
|
+
if result.get('migration_message'):
|
181
|
+
click.echo(f"\n{result['migration_message']}")
|
182
|
+
else:
|
183
|
+
click.echo(f"❌ Installation failed: {result.get('error', 'Unknown error')}")
|
184
|
+
if result.get('details'):
|
185
|
+
click.echo(f" Details: {result['details']}")
|
186
|
+
|
187
|
+
|
188
|
+
@kokoro.command()
|
189
|
+
@click.option('--remove-models', is_flag=True, help='Also remove downloaded Kokoro models')
|
190
|
+
@click.option('--remove-all-data', is_flag=True, help='Remove all Kokoro data including logs and cache')
|
191
|
+
@click.confirmation_option(prompt='Are you sure you want to uninstall Kokoro?')
|
192
|
+
def uninstall(remove_models, remove_all_data):
|
193
|
+
"""Uninstall kokoro-fastapi service and optionally remove data."""
|
194
|
+
result = asyncio.run(kokoro_uninstall.fn(
|
195
|
+
remove_models=remove_models,
|
196
|
+
remove_all_data=remove_all_data
|
197
|
+
))
|
198
|
+
|
199
|
+
if result.get('success'):
|
200
|
+
click.echo("✅ Kokoro uninstalled successfully!")
|
201
|
+
|
202
|
+
if result.get('service_stopped'):
|
203
|
+
click.echo(" Service stopped")
|
204
|
+
if result.get('service_disabled'):
|
205
|
+
click.echo(" Service disabled")
|
206
|
+
if result.get('install_removed'):
|
207
|
+
click.echo(f" Installation removed: {result['install_path']}")
|
208
|
+
if result.get('models_removed'):
|
209
|
+
click.echo(" Models removed")
|
210
|
+
if result.get('data_removed'):
|
211
|
+
click.echo(" All data removed")
|
212
|
+
|
213
|
+
if result.get('warnings'):
|
214
|
+
click.echo("\n⚠️ Warnings:")
|
215
|
+
for warning in result['warnings']:
|
216
|
+
click.echo(f" - {warning}")
|
217
|
+
else:
|
218
|
+
click.echo(f"❌ Uninstall failed: {result.get('error', 'Unknown error')}")
|
219
|
+
if result.get('details'):
|
220
|
+
click.echo(f" Details: {result['details']}")
|
221
|
+
|
222
|
+
|
223
|
+
# Whisper service commands
|
224
|
+
@whisper.command()
|
225
|
+
def status():
|
226
|
+
"""Show Whisper service status."""
|
227
|
+
result = asyncio.run(status_service("whisper"))
|
228
|
+
click.echo(result)
|
229
|
+
|
230
|
+
|
231
|
+
@whisper.command()
|
232
|
+
def start():
|
233
|
+
"""Start Whisper service."""
|
234
|
+
result = asyncio.run(start_service("whisper"))
|
235
|
+
click.echo(result)
|
236
|
+
|
237
|
+
|
238
|
+
@whisper.command()
|
239
|
+
def stop():
|
240
|
+
"""Stop Whisper service."""
|
241
|
+
result = asyncio.run(stop_service("whisper"))
|
242
|
+
click.echo(result)
|
243
|
+
|
244
|
+
|
245
|
+
@whisper.command()
|
246
|
+
def restart():
|
247
|
+
"""Restart Whisper service."""
|
248
|
+
result = asyncio.run(restart_service("whisper"))
|
249
|
+
click.echo(result)
|
250
|
+
|
251
|
+
|
252
|
+
@whisper.command()
|
253
|
+
def enable():
|
254
|
+
"""Enable Whisper service to start at boot/login."""
|
255
|
+
result = asyncio.run(enable_service("whisper"))
|
256
|
+
click.echo(result)
|
257
|
+
|
258
|
+
|
259
|
+
@whisper.command()
|
260
|
+
def disable():
|
261
|
+
"""Disable Whisper service from starting at boot/login."""
|
262
|
+
result = asyncio.run(disable_service("whisper"))
|
263
|
+
click.echo(result)
|
264
|
+
|
265
|
+
|
266
|
+
@whisper.command()
|
267
|
+
@click.option('--lines', '-n', default=50, help='Number of log lines to show')
|
268
|
+
def logs(lines):
|
269
|
+
"""View Whisper service logs."""
|
270
|
+
result = asyncio.run(view_logs("whisper", lines))
|
271
|
+
click.echo(result)
|
272
|
+
|
273
|
+
|
274
|
+
@whisper.command("update-service-files")
|
275
|
+
def whisper_update_service_files():
|
276
|
+
"""Update Whisper service files to latest version."""
|
277
|
+
result = asyncio.run(update_service_files("whisper"))
|
278
|
+
click.echo(result)
|
279
|
+
|
280
|
+
|
281
|
+
@whisper.command()
|
282
|
+
def health():
|
283
|
+
"""Check Whisper health endpoint."""
|
284
|
+
import subprocess
|
285
|
+
try:
|
286
|
+
result = subprocess.run(
|
287
|
+
["curl", "-s", "http://127.0.0.1:8090/health"],
|
288
|
+
capture_output=True, text=True, timeout=5
|
289
|
+
)
|
290
|
+
if result.returncode == 0:
|
291
|
+
import json
|
292
|
+
try:
|
293
|
+
health_data = json.loads(result.stdout)
|
294
|
+
click.echo("✅ Whisper is responding")
|
295
|
+
click.echo(f" Status: {health_data.get('status', 'unknown')}")
|
296
|
+
if 'uptime' in health_data:
|
297
|
+
click.echo(f" Uptime: {health_data['uptime']}")
|
298
|
+
except json.JSONDecodeError:
|
299
|
+
click.echo("✅ Whisper is responding (non-JSON response)")
|
300
|
+
else:
|
301
|
+
click.echo("❌ Whisper not responding on port 8090")
|
302
|
+
except subprocess.TimeoutExpired:
|
303
|
+
click.echo("❌ Whisper health check timed out")
|
304
|
+
except Exception as e:
|
305
|
+
click.echo(f"❌ Health check failed: {e}")
|
306
|
+
|
307
|
+
|
308
|
+
@whisper.command()
|
309
|
+
@click.option('--install-dir', help='Directory to install whisper.cpp')
|
310
|
+
@click.option('--model', default='large-v2', help='Whisper model to download (default: large-v2)')
|
311
|
+
@click.option('--use-gpu/--no-gpu', default=None, help='Enable GPU support if available')
|
312
|
+
@click.option('--force', '-f', is_flag=True, help='Force reinstall even if already installed')
|
313
|
+
@click.option('--version', default='latest', help='Version to install (default: latest)')
|
314
|
+
@click.option('--auto-enable/--no-auto-enable', default=None, help='Enable service at boot/login')
|
315
|
+
def install(install_dir, model, use_gpu, force, version, auto_enable):
|
316
|
+
"""Install whisper.cpp STT service with automatic system detection."""
|
317
|
+
result = asyncio.run(whisper_install.fn(
|
318
|
+
install_dir=install_dir,
|
319
|
+
model=model,
|
320
|
+
use_gpu=use_gpu,
|
321
|
+
force_reinstall=force,
|
322
|
+
version=version,
|
323
|
+
auto_enable=auto_enable
|
324
|
+
))
|
325
|
+
|
326
|
+
if result.get('success'):
|
327
|
+
if result.get('already_installed'):
|
328
|
+
click.echo(f"✅ Whisper already installed at {result['install_path']}")
|
329
|
+
click.echo(f" Version: {result.get('version', 'unknown')}")
|
330
|
+
else:
|
331
|
+
click.echo("✅ Whisper installed successfully!")
|
332
|
+
click.echo(f" Install path: {result['install_path']}")
|
333
|
+
click.echo(f" Version: {result.get('version', 'unknown')}")
|
334
|
+
|
335
|
+
if result.get('gpu_enabled'):
|
336
|
+
click.echo(" GPU support: Enabled")
|
337
|
+
if result.get('model_downloaded'):
|
338
|
+
click.echo(f" Model: {result.get('model', 'unknown')}")
|
339
|
+
if result.get('enabled'):
|
340
|
+
click.echo(" Auto-start: Enabled")
|
341
|
+
|
342
|
+
if result.get('migration_message'):
|
343
|
+
click.echo(f"\n{result['migration_message']}")
|
344
|
+
|
345
|
+
if result.get('next_steps'):
|
346
|
+
click.echo("\nNext steps:")
|
347
|
+
for step in result['next_steps']:
|
348
|
+
click.echo(f" - {step}")
|
349
|
+
else:
|
350
|
+
click.echo(f"❌ Installation failed: {result.get('error', 'Unknown error')}")
|
351
|
+
if result.get('details'):
|
352
|
+
click.echo(f" Details: {result['details']}")
|
353
|
+
|
354
|
+
|
355
|
+
@whisper.command()
|
356
|
+
@click.option('--remove-models', is_flag=True, help='Also remove downloaded Whisper models')
|
357
|
+
@click.option('--remove-all-data', is_flag=True, help='Remove all Whisper data including logs and transcriptions')
|
358
|
+
@click.confirmation_option(prompt='Are you sure you want to uninstall Whisper?')
|
359
|
+
def uninstall(remove_models, remove_all_data):
|
360
|
+
"""Uninstall whisper.cpp and optionally remove models and data."""
|
361
|
+
result = asyncio.run(whisper_uninstall.fn(
|
362
|
+
remove_models=remove_models,
|
363
|
+
remove_all_data=remove_all_data
|
364
|
+
))
|
365
|
+
|
366
|
+
if result.get('success'):
|
367
|
+
click.echo("✅ Whisper uninstalled successfully!")
|
368
|
+
|
369
|
+
if result.get('service_stopped'):
|
370
|
+
click.echo(" Service stopped")
|
371
|
+
if result.get('service_disabled'):
|
372
|
+
click.echo(" Service disabled")
|
373
|
+
if result.get('install_removed'):
|
374
|
+
click.echo(f" Installation removed: {result['install_path']}")
|
375
|
+
if result.get('models_removed'):
|
376
|
+
click.echo(" Models removed")
|
377
|
+
if result.get('data_removed'):
|
378
|
+
click.echo(" All data removed")
|
379
|
+
|
380
|
+
if result.get('warnings'):
|
381
|
+
click.echo("\n⚠️ Warnings:")
|
382
|
+
for warning in result['warnings']:
|
383
|
+
click.echo(f" - {warning}")
|
384
|
+
else:
|
385
|
+
click.echo(f"❌ Uninstall failed: {result.get('error', 'Unknown error')}")
|
386
|
+
if result.get('details'):
|
387
|
+
click.echo(f" Details: {result['details']}")
|
388
|
+
|
389
|
+
|
390
|
+
@whisper.command("download-model")
|
391
|
+
@click.argument('model', default='large-v2')
|
392
|
+
@click.option('--force', '-f', is_flag=True, help='Re-download even if model exists')
|
393
|
+
@click.option('--skip-core-ml', is_flag=True, help='Skip Core ML conversion on Apple Silicon')
|
394
|
+
def download_model_cmd(model, force, skip_core_ml):
|
395
|
+
"""Download Whisper model(s) with optional Core ML conversion.
|
396
|
+
|
397
|
+
MODEL can be a model name (e.g., 'large-v2'), 'all' to download all models,
|
398
|
+
or omitted to use the default (large-v2).
|
399
|
+
|
400
|
+
Available models: tiny, tiny.en, base, base.en, small, small.en,
|
401
|
+
medium, medium.en, large-v1, large-v2, large-v3, large-v3-turbo
|
402
|
+
"""
|
403
|
+
import json
|
404
|
+
result = asyncio.run(download_model.fn(
|
405
|
+
model=model,
|
406
|
+
force_download=force,
|
407
|
+
skip_core_ml=skip_core_ml
|
408
|
+
))
|
409
|
+
|
410
|
+
try:
|
411
|
+
# Parse JSON response
|
412
|
+
data = json.loads(result)
|
413
|
+
if data.get('success'):
|
414
|
+
click.echo("✅ Model download completed!")
|
415
|
+
|
416
|
+
if 'results' in data:
|
417
|
+
for model_result in data['results']:
|
418
|
+
click.echo(f"\n📦 {model_result['model']}:")
|
419
|
+
if model_result.get('already_exists') and not force:
|
420
|
+
click.echo(" Already downloaded")
|
421
|
+
else:
|
422
|
+
click.echo(" Downloaded successfully")
|
423
|
+
|
424
|
+
if model_result.get('core_ml_converted'):
|
425
|
+
click.echo(" Core ML: Converted")
|
426
|
+
elif model_result.get('core_ml_exists'):
|
427
|
+
click.echo(" Core ML: Already exists")
|
428
|
+
|
429
|
+
if 'models_dir' in data:
|
430
|
+
click.echo(f"\nModels location: {data['models_dir']}")
|
431
|
+
else:
|
432
|
+
click.echo(f"❌ Download failed: {data.get('error', 'Unknown error')}")
|
433
|
+
if 'available_models' in data:
|
434
|
+
click.echo("\nAvailable models:")
|
435
|
+
for m in data['available_models']:
|
436
|
+
click.echo(f" - {m}")
|
437
|
+
except json.JSONDecodeError:
|
438
|
+
click.echo(result)
|
439
|
+
|
440
|
+
|
441
|
+
# Configuration management group
|
442
|
+
@voice_mode_main_cli.group()
|
443
|
+
def config():
|
444
|
+
"""Manage voice-mode configuration."""
|
445
|
+
pass
|
446
|
+
|
447
|
+
|
448
|
+
@config.command("list")
|
449
|
+
def config_list():
|
450
|
+
"""List all configuration keys with their descriptions."""
|
451
|
+
result = asyncio.run(list_config_keys.fn())
|
452
|
+
click.echo(result)
|
453
|
+
|
454
|
+
|
455
|
+
@config.command("get")
|
456
|
+
@click.argument('key')
|
457
|
+
def config_get(key):
|
458
|
+
"""Get a configuration value."""
|
459
|
+
import os
|
460
|
+
from pathlib import Path
|
461
|
+
|
462
|
+
# Read from the env file
|
463
|
+
env_file = Path.home() / ".voicemode" / "voicemode.env"
|
464
|
+
if not env_file.exists():
|
465
|
+
click.echo(f"❌ Configuration file not found: {env_file}")
|
466
|
+
return
|
467
|
+
|
468
|
+
# Look for the key
|
469
|
+
found = False
|
470
|
+
with open(env_file, 'r') as f:
|
471
|
+
for line in f:
|
472
|
+
line = line.strip()
|
473
|
+
if line.startswith('#') or not line:
|
474
|
+
continue
|
475
|
+
if '=' in line:
|
476
|
+
k, v = line.split('=', 1)
|
477
|
+
if k.strip() == key:
|
478
|
+
click.echo(f"{key}={v.strip()}")
|
479
|
+
found = True
|
480
|
+
break
|
481
|
+
|
482
|
+
if not found:
|
483
|
+
# Check environment variable
|
484
|
+
env_value = os.getenv(key)
|
485
|
+
if env_value is not None:
|
486
|
+
click.echo(f"{key}={env_value} (from environment)")
|
487
|
+
else:
|
488
|
+
click.echo(f"❌ Configuration key not found: {key}")
|
489
|
+
click.echo("Run 'voice-mode config list' to see available keys")
|
490
|
+
|
491
|
+
|
492
|
+
@config.command("set")
|
493
|
+
@click.argument('key')
|
494
|
+
@click.argument('value')
|
495
|
+
def config_set(key, value):
|
496
|
+
"""Set a configuration value."""
|
497
|
+
result = asyncio.run(update_config.fn(key, value))
|
498
|
+
click.echo(result)
|
499
|
+
|
500
|
+
|
501
|
+
# Diagnostics group
|
502
|
+
@voice_mode_main_cli.group()
|
503
|
+
def diag():
|
504
|
+
"""Diagnostic tools for voice-mode."""
|
505
|
+
pass
|
506
|
+
|
507
|
+
|
508
|
+
@diag.command()
|
509
|
+
def info():
|
510
|
+
"""Show voice-mode installation information."""
|
511
|
+
result = asyncio.run(voice_mode_info.fn())
|
512
|
+
click.echo(result)
|
513
|
+
|
514
|
+
|
515
|
+
@diag.command()
|
516
|
+
def devices():
|
517
|
+
"""List available audio input and output devices."""
|
518
|
+
result = asyncio.run(check_audio_devices.fn())
|
519
|
+
click.echo(result)
|
520
|
+
|
521
|
+
|
522
|
+
@diag.command()
|
523
|
+
def registry():
|
524
|
+
"""Show voice provider registry with all discovered endpoints."""
|
525
|
+
result = asyncio.run(voice_registry.fn())
|
526
|
+
click.echo(result)
|
527
|
+
|
528
|
+
|
529
|
+
@diag.command()
|
530
|
+
def dependencies():
|
531
|
+
"""Check system audio dependencies and provide installation guidance."""
|
532
|
+
import json
|
533
|
+
result = asyncio.run(check_audio_dependencies.fn())
|
534
|
+
|
535
|
+
if isinstance(result, dict):
|
536
|
+
# Format the dictionary output nicely
|
537
|
+
click.echo("System Audio Dependencies Check")
|
538
|
+
click.echo("=" * 50)
|
539
|
+
|
540
|
+
click.echo(f"\nPlatform: {result.get('platform', 'Unknown')}")
|
541
|
+
|
542
|
+
if 'packages' in result:
|
543
|
+
click.echo("\nSystem Packages:")
|
544
|
+
for pkg, status in result['packages'].items():
|
545
|
+
symbol = "✅" if status else "❌"
|
546
|
+
click.echo(f" {symbol} {pkg}")
|
547
|
+
|
548
|
+
if 'missing_packages' in result and result['missing_packages']:
|
549
|
+
click.echo("\n❌ Missing Packages:")
|
550
|
+
for pkg in result['missing_packages']:
|
551
|
+
click.echo(f" - {pkg}")
|
552
|
+
if 'install_command' in result:
|
553
|
+
click.echo(f"\nInstall with: {result['install_command']}")
|
554
|
+
|
555
|
+
if 'pulseaudio' in result:
|
556
|
+
pa = result['pulseaudio']
|
557
|
+
click.echo(f"\nPulseAudio Status: {'✅ Running' if pa.get('running') else '❌ Not running'}")
|
558
|
+
if pa.get('version'):
|
559
|
+
click.echo(f" Version: {pa['version']}")
|
560
|
+
|
561
|
+
if 'diagnostics' in result and result['diagnostics']:
|
562
|
+
click.echo("\nDiagnostics:")
|
563
|
+
for diag in result['diagnostics']:
|
564
|
+
click.echo(f" - {diag}")
|
565
|
+
|
566
|
+
if 'recommendations' in result and result['recommendations']:
|
567
|
+
click.echo("\nRecommendations:")
|
568
|
+
for rec in result['recommendations']:
|
569
|
+
click.echo(f" - {rec}")
|
570
|
+
else:
|
571
|
+
# Fallback for string output
|
572
|
+
click.echo(str(result))
|
573
|
+
|
574
|
+
|
575
|
+
# Legacy CLI for voice-mode-cli command
|
576
|
+
@click.group()
|
577
|
+
@click.version_option()
|
578
|
+
@click.help_option('-h', '--help')
|
579
|
+
def cli():
|
580
|
+
"""Voice Mode CLI - Manage conversations, view logs, and analyze voice interactions."""
|
581
|
+
pass
|
582
|
+
|
583
|
+
|
584
|
+
# Import subcommand groups
|
585
|
+
from voice_mode.cli_commands import exchanges as exchanges_cmd
|
586
|
+
|
587
|
+
# Add subcommands to legacy CLI
|
588
|
+
cli.add_command(exchanges_cmd.exchanges)
|
589
|
+
|
590
|
+
# Add exchanges to main CLI
|
591
|
+
voice_mode_main_cli.add_command(exchanges_cmd.exchanges)
|
592
|
+
|
593
|
+
|
@@ -1,28 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
CLI entry points for voice-mode package.
|
3
|
-
"""
|
4
|
-
import sys
|
5
|
-
import click
|
6
|
-
from .server import main as voice_mode_main
|
7
|
-
|
8
|
-
def voice_mode() -> None:
|
9
|
-
"""Entry point for voicemode command - starts the MCP server."""
|
10
|
-
voice_mode_main()
|
11
|
-
|
12
|
-
|
13
|
-
# New Click-based CLI
|
14
|
-
@click.group()
|
15
|
-
@click.version_option()
|
16
|
-
@click.help_option('-h', '--help')
|
17
|
-
def cli():
|
18
|
-
"""Voice Mode CLI - Manage conversations, view logs, and analyze voice interactions."""
|
19
|
-
pass
|
20
|
-
|
21
|
-
|
22
|
-
# Import subcommand groups
|
23
|
-
from voice_mode.cli_commands import exchanges as exchanges_cmd
|
24
|
-
|
25
|
-
# Add subcommands
|
26
|
-
cli.add_command(exchanges_cmd.exchanges)
|
27
|
-
|
28
|
-
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/com.voicemode.kokoro.plist
RENAMED
File without changes
|
{voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/launchd/com.voicemode.whisper.plist
RENAMED
File without changes
|
File without changes
|
File without changes
|
{voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/systemd/voicemode-kokoro.service
RENAMED
File without changes
|
{voice_mode-2.20.1 → voice_mode-2.21.1}/voice_mode/templates/systemd/voicemode-whisper.service
RENAMED
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
|
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
|
File without changes
|
File without changes
|
File without changes
|