voice-mode 2.20.1__tar.gz → 2.21.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {voice_mode-2.20.1 → voice_mode-2.21.0}/CHANGELOG.md +2 -0
  2. {voice_mode-2.20.1 → voice_mode-2.21.0}/PKG-INFO +1 -1
  3. {voice_mode-2.20.1 → voice_mode-2.21.0}/pyproject.toml +0 -1
  4. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/__version__.py +1 -1
  5. voice_mode-2.21.0/voice_mode/cli.py +593 -0
  6. voice_mode-2.20.1/voice_mode/cli.py +0 -28
  7. {voice_mode-2.20.1 → voice_mode-2.21.0}/.gitignore +0 -0
  8. {voice_mode-2.20.1 → voice_mode-2.21.0}/README.md +0 -0
  9. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/__init__.py +0 -0
  10. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/__main__.py +0 -0
  11. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/cli_commands/__init__.py +0 -0
  12. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/cli_commands/exchanges.py +0 -0
  13. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/config.py +0 -0
  14. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/conversation_logger.py +0 -0
  15. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/core.py +0 -0
  16. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/data/versions.json +0 -0
  17. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/__init__.py +0 -0
  18. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/conversations.py +0 -0
  19. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/filters.py +0 -0
  20. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/formatters.py +0 -0
  21. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/models.py +0 -0
  22. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/reader.py +0 -0
  23. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/exchanges/stats.py +0 -0
  24. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/prompts/README.md +0 -0
  25. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/prompts/__init__.py +0 -0
  26. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/prompts/converse.py +0 -0
  27. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/prompts/release_notes.py +0 -0
  28. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/prompts/services.py +0 -0
  29. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/provider_discovery.py +0 -0
  30. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/providers.py +0 -0
  31. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/__init__.py +0 -0
  32. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/audio_files.py +0 -0
  33. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/changelog.py +0 -0
  34. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/configuration.py +0 -0
  35. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/statistics.py +0 -0
  36. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/version.py +0 -0
  37. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/resources/whisper_models.py +0 -0
  38. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/server.py +0 -0
  39. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/shared.py +0 -0
  40. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/simple_failover.py +0 -0
  41. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/statistics.py +0 -0
  42. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/streaming.py +0 -0
  43. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
  44. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
  45. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
  46. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
  47. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
  48. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
  49. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/__init__.py +0 -0
  50. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/configuration_management.py +0 -0
  51. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/converse.py +0 -0
  52. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/dependencies.py +0 -0
  53. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/devices.py +0 -0
  54. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/diagnostics.py +0 -0
  55. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/providers.py +0 -0
  56. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/service.py +0 -0
  57. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/kokoro/install.py +0 -0
  58. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
  59. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/list_versions.py +0 -0
  60. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/version_info.py +0 -0
  61. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/whisper/download_model.py +0 -0
  62. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/whisper/install.py +0 -0
  63. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/services/whisper/uninstall.py +0 -0
  64. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/statistics.py +0 -0
  65. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/tools/voice_registry.py +0 -0
  66. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/__init__.py +0 -0
  67. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/audio_diagnostics.py +0 -0
  68. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/event_logger.py +0 -0
  69. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/ffmpeg_check.py +0 -0
  70. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/format_migration.py +0 -0
  71. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/gpu_detection.py +0 -0
  72. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/migration_helpers.py +0 -0
  73. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/services/common.py +0 -0
  74. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/services/kokoro_helpers.py +0 -0
  75. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/services/whisper_helpers.py +0 -0
  76. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/utils/version_helpers.py +0 -0
  77. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/version.py +0 -0
  78. {voice_mode-2.20.1 → voice_mode-2.21.0}/voice_mode/voice_preferences.py +0 -0
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.21.0] - 2025-08-13
11
+
10
12
  ## [2.20.1] - 2025-08-11
11
13
 
12
14
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-mode
3
- Version: 2.20.1
3
+ Version: 2.21.0
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"]
@@ -1,3 +1,3 @@
1
1
  # This file is automatically updated by 'make release'
2
2
  # Do not edit manually
3
- __version__ = "2.20.1"
3
+ __version__ = "2.21.0"
@@ -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