voice-mode 2.26.0__py3-none-any.whl → 2.27.0__py3-none-any.whl

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 (80) hide show
  1. voice_mode/__version__.py +1 -1
  2. voice_mode/cli.py +611 -3
  3. voice_mode/config.py +11 -3
  4. voice_mode/frontend/.next/BUILD_ID +1 -1
  5. voice_mode/frontend/.next/app-build-manifest.json +5 -5
  6. voice_mode/frontend/.next/app-path-routes-manifest.json +1 -1
  7. voice_mode/frontend/.next/build-manifest.json +3 -3
  8. voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
  9. voice_mode/frontend/.next/next-server.js.nft.json +1 -1
  10. voice_mode/frontend/.next/prerender-manifest.json +1 -1
  11. voice_mode/frontend/.next/required-server-files.json +1 -1
  12. voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
  13. voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. voice_mode/frontend/.next/server/app/_not-found.html +1 -1
  15. voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
  16. voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
  17. voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
  18. voice_mode/frontend/.next/server/app/index.html +1 -1
  19. voice_mode/frontend/.next/server/app/index.rsc +2 -2
  20. voice_mode/frontend/.next/server/app/page.js +2 -2
  21. voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
  22. voice_mode/frontend/.next/server/app-paths-manifest.json +1 -1
  23. voice_mode/frontend/.next/server/chunks/994.js +1 -1
  24. voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
  25. voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
  26. voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
  27. voice_mode/frontend/.next/server/pages/404.html +1 -1
  28. voice_mode/frontend/.next/server/pages/500.html +1 -1
  29. voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
  30. voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
  31. voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
  32. voice_mode/frontend/.next/standalone/.next/app-path-routes-manifest.json +1 -1
  33. voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
  34. voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
  35. voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
  36. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  37. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
  39. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  40. voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
  41. voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
  42. voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
  43. voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
  44. voice_mode/frontend/.next/standalone/.next/server/app/page.js +2 -2
  45. voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. voice_mode/frontend/.next/standalone/.next/server/app-paths-manifest.json +1 -1
  47. voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +1 -1
  48. voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
  50. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
  51. voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
  52. voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
  53. voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  54. voice_mode/frontend/.next/standalone/server.js +1 -1
  55. voice_mode/frontend/.next/static/chunks/app/{layout-0e969b20634a3137.js → layout-08be62ed6e344292.js} +1 -1
  56. voice_mode/frontend/.next/static/chunks/app/page-80fc72669f25298f.js +1 -0
  57. voice_mode/frontend/.next/static/chunks/{main-app-b9e128659aafd50e.js → main-app-413f77c1f2c53e3f.js} +1 -1
  58. voice_mode/frontend/.next/trace +43 -43
  59. voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
  60. voice_mode/frontend/.next/types/app/layout.ts +1 -1
  61. voice_mode/frontend/.next/types/app/page.ts +1 -1
  62. voice_mode/frontend/package-lock.json +8 -8
  63. voice_mode/resources/configuration.py +8 -4
  64. voice_mode/resources/whisper_models.py +10 -13
  65. voice_mode/templates/systemd/voicemode-frontend.service +1 -1
  66. voice_mode/tools/configuration_management.py +7 -2
  67. voice_mode/tools/converse.py +31 -0
  68. voice_mode/tools/services/kokoro/install.py +3 -2
  69. voice_mode/tools/services/whisper/__init__.py +13 -0
  70. voice_mode/tools/services/whisper/install.py +3 -2
  71. voice_mode/tools/services/whisper/list_models.py +70 -0
  72. voice_mode/tools/services/whisper/list_models_tool.py +65 -0
  73. voice_mode/tools/services/whisper/models.py +274 -0
  74. {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/METADATA +1 -1
  75. {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/RECORD +79 -75
  76. voice_mode/frontend/.next/static/chunks/app/page-db597c111ebcc19f.js +0 -1
  77. /voice_mode/frontend/.next/static/{uvJyMdD1IAhgbf_LCTQE6 → wQ5pxzPmwjlzdUfJwSjMg}/_buildManifest.js +0 -0
  78. /voice_mode/frontend/.next/static/{uvJyMdD1IAhgbf_LCTQE6 → wQ5pxzPmwjlzdUfJwSjMg}/_ssgManifest.js +0 -0
  79. {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/WHEEL +0 -0
  80. {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/entry_points.txt +0 -0
voice_mode/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  # This file is automatically updated by 'make release'
2
2
  # Do not edit manually
3
- __version__ = "2.26.0"
3
+ __version__ = "2.27.0"
voice_mode/cli.py CHANGED
@@ -7,6 +7,7 @@ import os
7
7
  import warnings
8
8
  import click
9
9
 
10
+
10
11
  # Suppress known deprecation warnings for better user experience
11
12
  # These apply to both CLI commands and MCP server operation
12
13
  # They can be shown with VOICEMODE_DEBUG=true or --debug flag
@@ -420,12 +421,183 @@ def uninstall(remove_models, remove_all_data):
420
421
  click.echo(f" Details: {result['details']}")
421
422
 
422
423
 
423
- @whisper.command("download-model")
424
+ @whisper.group("model")
425
+ def whisper_model():
426
+ """Manage Whisper models.
427
+
428
+ Subcommands:
429
+ active - Show or set the active model
430
+ install - Download and install models
431
+ remove - Remove installed models
432
+ """
433
+ pass
434
+
435
+
436
+ @whisper_model.command("active")
437
+ @click.argument('model_name', required=False)
438
+ def whisper_model_active(model_name):
439
+ """Show or set the active Whisper model.
440
+
441
+ Without arguments: Shows the current active model
442
+ With MODEL_NAME: Sets the active model (updates VOICEMODE_WHISPER_MODEL)
443
+ """
444
+ from voice_mode.tools.services.whisper.models import (
445
+ get_current_model,
446
+ WHISPER_MODELS,
447
+ is_model_installed,
448
+ set_current_model
449
+ )
450
+ import os
451
+ import subprocess
452
+
453
+ if model_name:
454
+ # Set model mode
455
+ if model_name not in WHISPER_MODELS:
456
+ click.echo(f"Error: '{model_name}' is not a valid model.", err=True)
457
+ click.echo("\nAvailable models:", err=True)
458
+ for name in WHISPER_MODELS.keys():
459
+ click.echo(f" - {name}", err=True)
460
+ return
461
+
462
+ # Check if model is installed
463
+ if not is_model_installed(model_name):
464
+ click.echo(f"Error: Model '{model_name}' is not installed.", err=True)
465
+ click.echo(f"Install it with: voice-mode whisper model install {model_name}", err=True)
466
+ raise click.Abort()
467
+
468
+ # Get previous model
469
+ previous_model = get_current_model()
470
+
471
+ # Update the configuration file
472
+ set_current_model(model_name)
473
+
474
+ click.echo(f"✓ Active model set to: {model_name}")
475
+ if previous_model != model_name:
476
+ click.echo(f" (was: {previous_model})")
477
+
478
+ # Check if whisper service is running
479
+ try:
480
+ result = subprocess.run(['pgrep', '-f', 'whisper-server'], capture_output=True)
481
+ if result.returncode == 0:
482
+ # Service is running
483
+ click.echo(f"\n⚠️ Please restart the whisper service for changes to take effect:")
484
+ click.echo(f" {click.style('voice-mode whisper restart', fg='yellow', bold=True)}")
485
+ else:
486
+ click.echo(f"\nWhisper service is not running. Start it with:")
487
+ click.echo(f" voice-mode whisper start")
488
+ click.echo(f"(or restart the whisper service if it's managed by systemd/launchd)")
489
+ except:
490
+ click.echo(f"\nPlease restart the whisper service for changes to take effect:")
491
+ click.echo(f" voice-mode whisper restart")
492
+
493
+ else:
494
+ # Show current model
495
+ current = get_current_model()
496
+
497
+ # Check if current model is installed
498
+ installed = is_model_installed(current)
499
+ status = click.style("[✓ Installed]", fg="green") if installed else click.style("[Not installed]", fg="red")
500
+
501
+ # Get model info
502
+ model_info = WHISPER_MODELS.get(current, {})
503
+
504
+ click.echo(f"\nActive Whisper model: {click.style(current, fg='yellow', bold=True)} {status}")
505
+ if model_info:
506
+ click.echo(f" Size: {model_info.get('size_mb', 'Unknown')} MB")
507
+ click.echo(f" Languages: {model_info.get('languages', 'Unknown')}")
508
+ click.echo(f" Description: {model_info.get('description', 'Unknown')}")
509
+
510
+ # Check what model the service is actually using
511
+ try:
512
+ result = subprocess.run(['pgrep', '-f', 'whisper-server'], capture_output=True)
513
+ if result.returncode == 0:
514
+ # Service is running, could check its actual model here
515
+ click.echo(f"\nWhisper service status: {click.style('Running', fg='green')}")
516
+ except:
517
+ pass
518
+
519
+ click.echo(f"\nTo change: voice-mode whisper model active <model-name>")
520
+ click.echo(f"To list all models: voice-mode whisper models")
521
+
522
+
523
+ @whisper.command("models")
524
+ def whisper_models():
525
+ """List available Whisper models and their installation status."""
526
+ from voice_mode.tools.services.whisper.models import (
527
+ WHISPER_MODELS,
528
+ get_model_directory,
529
+ get_current_model,
530
+ is_model_installed,
531
+ get_installed_models,
532
+ format_size
533
+ )
534
+
535
+ model_dir = get_model_directory()
536
+ current_model = get_current_model()
537
+ installed_models = get_installed_models()
538
+
539
+ # Calculate totals
540
+ total_installed_size = sum(
541
+ WHISPER_MODELS[m]["size_mb"] for m in installed_models
542
+ )
543
+ total_available_size = sum(
544
+ m["size_mb"] for m in WHISPER_MODELS.values()
545
+ )
546
+
547
+ # Print header
548
+ click.echo("\nWhisper Models:")
549
+ click.echo("")
550
+
551
+ # Print models table
552
+ for model_name, info in WHISPER_MODELS.items():
553
+ # Check status
554
+ is_installed = is_model_installed(model_name)
555
+ is_current = model_name == current_model
556
+
557
+ # Format status
558
+ if is_current:
559
+ status = click.style("→", fg="yellow", bold=True)
560
+ model_display = click.style(f"{model_name:15}", fg="yellow", bold=True)
561
+ else:
562
+ status = " "
563
+ model_display = f"{model_name:15}"
564
+
565
+ # Format installation status
566
+ if is_installed:
567
+ install_status = click.style("[✓ Installed]", fg="green")
568
+ else:
569
+ install_status = click.style("[ Download ]", fg="bright_black")
570
+
571
+ # Format size
572
+ size_str = format_size(info["size_mb"]).rjust(8)
573
+
574
+ # Format languages
575
+ lang_str = f"{info['languages']:20}"
576
+
577
+ # Format description
578
+ desc = info['description']
579
+ if is_current:
580
+ desc += " (Currently selected)"
581
+ desc = click.style(desc, fg="yellow")
582
+
583
+ # Print row
584
+ click.echo(f"{status} {model_display} {install_status:14} {size_str} {lang_str} {desc}")
585
+
586
+ # Print footer
587
+ click.echo("")
588
+ click.echo(f"Models directory: {model_dir}")
589
+ click.echo(f"Total size: {format_size(total_installed_size)} installed / {format_size(total_available_size)} available")
590
+ click.echo("")
591
+ click.echo("To download a model: voice-mode whisper model install <model-name>")
592
+ click.echo("To set default model: voice-mode whisper model <model-name>")
593
+
594
+
595
+ @whisper_model.command("install")
424
596
  @click.argument('model', default='large-v2')
425
597
  @click.option('--force', '-f', is_flag=True, help='Re-download even if model exists')
426
598
  @click.option('--skip-core-ml', is_flag=True, help='Skip Core ML conversion on Apple Silicon')
427
- def download_model_cmd(model, force, skip_core_ml):
428
- """Download Whisper model(s) with optional Core ML conversion.
599
+ def whisper_model_install(model, force, skip_core_ml):
600
+ """Install Whisper model(s) with optional Core ML conversion.
429
601
 
430
602
  MODEL can be a model name (e.g., 'large-v2'), 'all' to download all models,
431
603
  or omitted to use the default (large-v2).
@@ -472,6 +644,74 @@ def download_model_cmd(model, force, skip_core_ml):
472
644
  click.echo(result)
473
645
 
474
646
 
647
+ @whisper_model.command("remove")
648
+ @click.argument('model')
649
+ @click.option('--force', '-f', is_flag=True, help='Remove without confirmation')
650
+ def whisper_model_remove(model, force):
651
+ """Remove an installed Whisper model.
652
+
653
+ MODEL is the name of the model to remove (e.g., 'large-v2').
654
+ """
655
+ from voice_mode.tools.services.whisper.models import (
656
+ WHISPER_MODELS,
657
+ is_model_installed,
658
+ get_model_directory,
659
+ get_current_model
660
+ )
661
+ import os
662
+
663
+ # Validate model name
664
+ if model not in WHISPER_MODELS:
665
+ click.echo(f"Error: '{model}' is not a valid model.", err=True)
666
+ click.echo("\nAvailable models:", err=True)
667
+ for name in WHISPER_MODELS.keys():
668
+ click.echo(f" - {name}", err=True)
669
+ ctx.exit(1)
670
+
671
+ # Check if model is installed
672
+ if not is_model_installed(model):
673
+ click.echo(f"Model '{model}' is not installed.")
674
+ return
675
+
676
+ # Check if it's the current model
677
+ current = get_current_model()
678
+ if model == current:
679
+ click.echo(f"Warning: '{model}' is the currently selected model.", err=True)
680
+ if not force:
681
+ if not click.confirm("Do you still want to remove it?"):
682
+ return
683
+
684
+ # Get model path
685
+ model_dir = get_model_directory()
686
+ model_info = WHISPER_MODELS[model]
687
+ model_path = model_dir / model_info["filename"]
688
+
689
+ # Also check for Core ML models
690
+ coreml_path = model_dir / f"ggml-{model}-encoder.mlmodelc"
691
+
692
+ # Confirm removal if not forced
693
+ if not force:
694
+ size_mb = model_info["size_mb"]
695
+ if not click.confirm(f"Remove {model} ({size_mb} MB)?"):
696
+ return
697
+
698
+ # Remove the model file
699
+ try:
700
+ if model_path.exists():
701
+ os.remove(model_path)
702
+ click.echo(f"✓ Removed model: {model}")
703
+
704
+ # Remove Core ML model if exists
705
+ if coreml_path.exists():
706
+ import shutil
707
+ shutil.rmtree(coreml_path)
708
+ click.echo(f"✓ Removed Core ML model: {model}")
709
+
710
+ click.echo(f"\nModel '{model}' has been removed.")
711
+ except Exception as e:
712
+ click.echo(f"Error removing model: {e}", err=True)
713
+
714
+
475
715
  # LiveKit service commands
476
716
  @livekit.command()
477
717
  def status():
@@ -897,6 +1137,150 @@ def config_set(key, value):
897
1137
  click.echo(result)
898
1138
 
899
1139
 
1140
+ # Shell completion group
1141
+ @voice_mode_main_cli.group()
1142
+ def completion():
1143
+ """Generate shell completion scripts for voice-mode."""
1144
+ pass
1145
+
1146
+
1147
+ @completion.command("bash")
1148
+ def completion_bash():
1149
+ """Generate bash completion script.
1150
+
1151
+ Add this to your ~/.bashrc:
1152
+
1153
+ eval "$(_VOICE_MODE_COMPLETE=bash_source voice-mode)"
1154
+
1155
+ Or for better performance, generate the script once:
1156
+
1157
+ _VOICE_MODE_COMPLETE=bash_source voice-mode > ~/.voice-mode-complete.bash
1158
+ echo '. ~/.voice-mode-complete.bash' >> ~/.bashrc
1159
+ """
1160
+ # Output the instructions directly since the environment variable method
1161
+ # needs to be run from the shell itself
1162
+ click.echo("# Bash completion for voice-mode")
1163
+ click.echo("# Add this to your ~/.bashrc:")
1164
+ click.echo("")
1165
+ click.echo('eval "$(_VOICE_MODE_COMPLETE=bash_source voice-mode)"')
1166
+ click.echo("")
1167
+ click.echo("# Or for better performance, generate and save the script:")
1168
+ click.echo("# _VOICE_MODE_COMPLETE=bash_source voice-mode > ~/.voice-mode-complete.bash")
1169
+ click.echo("# Then add to ~/.bashrc:")
1170
+ click.echo("# . ~/.voice-mode-complete.bash")
1171
+
1172
+
1173
+ @completion.command("zsh")
1174
+ def completion_zsh():
1175
+ """Generate zsh completion script.
1176
+
1177
+ Add this to your ~/.zshrc:
1178
+
1179
+ eval "$(_VOICE_MODE_COMPLETE=zsh_source voice-mode)"
1180
+
1181
+ Or for better performance, generate the script once:
1182
+
1183
+ _VOICE_MODE_COMPLETE=zsh_source voice-mode > ~/.voice-mode-complete.zsh
1184
+ echo '. ~/.voice-mode-complete.zsh' >> ~/.zshrc
1185
+ """
1186
+ # Output the instructions directly
1187
+ click.echo("# Zsh completion for voice-mode")
1188
+ click.echo("# Add this to your ~/.zshrc:")
1189
+ click.echo("")
1190
+ click.echo('eval "$(_VOICE_MODE_COMPLETE=zsh_source voice-mode)"')
1191
+ click.echo("")
1192
+ click.echo("# Or for better performance, generate and save the script:")
1193
+ click.echo("# _VOICE_MODE_COMPLETE=zsh_source voice-mode > ~/.voice-mode-complete.zsh")
1194
+ click.echo("# Then add to ~/.zshrc:")
1195
+ click.echo("# . ~/.voice-mode-complete.zsh")
1196
+
1197
+
1198
+ @completion.command("fish")
1199
+ def completion_fish():
1200
+ """Generate fish completion script.
1201
+
1202
+ Add this to ~/.config/fish/completions/voice-mode.fish:
1203
+
1204
+ _VOICE_MODE_COMPLETE=fish_source voice-mode | source
1205
+
1206
+ Or save it to a file:
1207
+
1208
+ _VOICE_MODE_COMPLETE=fish_source voice-mode > ~/.config/fish/completions/voice-mode.fish
1209
+ """
1210
+ # Output the instructions directly
1211
+ click.echo("# Fish completion for voice-mode")
1212
+ click.echo("# Save this to ~/.config/fish/completions/voice-mode.fish:")
1213
+ click.echo("")
1214
+ click.echo("_VOICE_MODE_COMPLETE=fish_source voice-mode | source")
1215
+ click.echo("")
1216
+ click.echo("# Or run this command to save it:")
1217
+ click.echo("# _VOICE_MODE_COMPLETE=fish_source voice-mode > ~/.config/fish/completions/voice-mode.fish")
1218
+
1219
+
1220
+ @completion.command("install")
1221
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish', 'auto']), default='auto', help='Shell type to install for')
1222
+ def completion_install(shell):
1223
+ """Show installation instructions for shell completion.
1224
+
1225
+ This command displays the steps to enable tab completion
1226
+ for voice-mode in your shell.
1227
+ """
1228
+ # Detect shell if auto
1229
+ if shell == 'auto':
1230
+ shell_env = os.environ.get('SHELL', '')
1231
+ if 'bash' in shell_env:
1232
+ shell = 'bash'
1233
+ elif 'zsh' in shell_env:
1234
+ shell = 'zsh'
1235
+ elif 'fish' in shell_env:
1236
+ shell = 'fish'
1237
+ else:
1238
+ click.echo("Could not detect shell type. Showing instructions for all shells.")
1239
+ click.echo("")
1240
+
1241
+ # Show all instructions
1242
+ click.echo("=== Bash ===")
1243
+ click.echo("Add to ~/.bashrc:")
1244
+ click.echo(' eval "$(_VOICE_MODE_COMPLETE=bash_source voice-mode)"')
1245
+ click.echo("")
1246
+
1247
+ click.echo("=== Zsh ===")
1248
+ click.echo("Add to ~/.zshrc:")
1249
+ click.echo(' eval "$(_VOICE_MODE_COMPLETE=zsh_source voice-mode)"')
1250
+ click.echo("")
1251
+
1252
+ click.echo("=== Fish ===")
1253
+ click.echo("Run this command:")
1254
+ click.echo(' _VOICE_MODE_COMPLETE=fish_source voice-mode > ~/.config/fish/completions/voice-mode.fish')
1255
+ return
1256
+
1257
+ click.echo(f"To enable tab completion for voice-mode in {shell}:")
1258
+ click.echo("")
1259
+
1260
+ if shell == 'bash':
1261
+ click.echo("Add this line to your ~/.bashrc:")
1262
+ click.echo(' eval "$(_VOICE_MODE_COMPLETE=bash_source voice-mode)"')
1263
+ click.echo("")
1264
+ click.echo("Or for better performance, generate the script once:")
1265
+ click.echo(" _VOICE_MODE_COMPLETE=bash_source voice-mode > ~/.voice-mode-complete.bash")
1266
+ click.echo(" echo '. ~/.voice-mode-complete.bash' >> ~/.bashrc")
1267
+ elif shell == 'zsh':
1268
+ click.echo("Add this line to your ~/.zshrc:")
1269
+ click.echo(' eval "$(_VOICE_MODE_COMPLETE=zsh_source voice-mode)"')
1270
+ click.echo("")
1271
+ click.echo("Or for better performance, generate the script once:")
1272
+ click.echo(" _VOICE_MODE_COMPLETE=zsh_source voice-mode > ~/.voice-mode-complete.zsh")
1273
+ click.echo(" echo '. ~/.voice-mode-complete.zsh' >> ~/.zshrc")
1274
+ elif shell == 'fish':
1275
+ click.echo("Run this command:")
1276
+ click.echo(' _VOICE_MODE_COMPLETE=fish_source voice-mode > ~/.config/fish/completions/voice-mode.fish')
1277
+ click.echo("")
1278
+ click.echo("Fish will automatically load the completion from this location.")
1279
+
1280
+ click.echo("")
1281
+ click.echo("After making these changes, restart your shell or source the config file.")
1282
+
1283
+
900
1284
  # Diagnostics group
901
1285
  @voice_mode_main_cli.group()
902
1286
  def diag():
@@ -1156,3 +1540,227 @@ def converse(message, wait, duration, min_duration, transport, room_name, voice,
1156
1540
  asyncio.run(run_conversation())
1157
1541
 
1158
1542
 
1543
+ # Version command
1544
+ @voice_mode_main_cli.command()
1545
+ def version():
1546
+ """Show Voice Mode version and check for updates."""
1547
+ import requests
1548
+ from importlib.metadata import version as get_version, PackageNotFoundError
1549
+
1550
+ try:
1551
+ current_version = get_version("voice-mode")
1552
+ except PackageNotFoundError:
1553
+ # Fallback for development installations
1554
+ current_version = "development"
1555
+
1556
+ click.echo(f"Voice Mode version: {current_version}")
1557
+
1558
+ # Check for updates if not in development mode
1559
+ if current_version != "development":
1560
+ try:
1561
+ response = requests.get(
1562
+ "https://pypi.org/pypi/voice-mode/json",
1563
+ timeout=2
1564
+ )
1565
+ if response.status_code == 200:
1566
+ latest_version = response.json()["info"]["version"]
1567
+
1568
+ # Simple version comparison (works for semantic versioning)
1569
+ if latest_version != current_version:
1570
+ click.echo(f"Latest version: {latest_version} available")
1571
+ click.echo("Run 'voicemode update' to update")
1572
+ else:
1573
+ click.echo("You are running the latest version")
1574
+ except (requests.RequestException, KeyError, ValueError):
1575
+ # Fail silently if we can't check for updates
1576
+ pass
1577
+
1578
+
1579
+ # Update command
1580
+ @voice_mode_main_cli.command()
1581
+ @click.option('--force', is_flag=True, help='Force reinstall even if already up to date')
1582
+ def update(force):
1583
+ """Update Voice Mode to the latest version."""
1584
+ import subprocess
1585
+ import requests
1586
+ from importlib.metadata import version as get_version, PackageNotFoundError
1587
+
1588
+ try:
1589
+ current_version = get_version("voice-mode")
1590
+ except PackageNotFoundError:
1591
+ current_version = "development"
1592
+
1593
+ if not force and current_version != "development":
1594
+ # Check if update is needed
1595
+ try:
1596
+ response = requests.get(
1597
+ "https://pypi.org/pypi/voice-mode/json",
1598
+ timeout=2
1599
+ )
1600
+ if response.status_code == 200:
1601
+ latest_version = response.json()["info"]["version"]
1602
+ if latest_version == current_version:
1603
+ click.echo(f"Already running the latest version ({current_version})")
1604
+ return
1605
+ except (requests.RequestException, KeyError, ValueError):
1606
+ # Continue with update if we can't check
1607
+ pass
1608
+
1609
+ click.echo("Updating Voice Mode to the latest version...")
1610
+
1611
+ # Try UV first, fall back to pip
1612
+ try:
1613
+ # Check if UV is available
1614
+ result = subprocess.run(
1615
+ ["uv", "--version"],
1616
+ capture_output=True,
1617
+ text=True,
1618
+ check=False
1619
+ )
1620
+
1621
+ if result.returncode == 0:
1622
+ # Use UV for update
1623
+ result = subprocess.run(
1624
+ ["uv", "pip", "install", "--upgrade", "voice-mode"],
1625
+ capture_output=True,
1626
+ text=True
1627
+ )
1628
+ if result.returncode == 0:
1629
+ # Get new version
1630
+ try:
1631
+ new_version = get_version("voice-mode")
1632
+ click.echo(f"✅ Successfully updated to version {new_version}")
1633
+ except PackageNotFoundError:
1634
+ click.echo("✅ Successfully updated Voice Mode")
1635
+ else:
1636
+ click.echo(f"❌ Update failed: {result.stderr}")
1637
+ click.echo("Try running: uv pip install --upgrade voice-mode")
1638
+ else:
1639
+ # Fall back to pip
1640
+ result = subprocess.run(
1641
+ [sys.executable, "-m", "pip", "install", "--upgrade", "voice-mode"],
1642
+ capture_output=True,
1643
+ text=True
1644
+ )
1645
+ if result.returncode == 0:
1646
+ try:
1647
+ new_version = get_version("voice-mode")
1648
+ click.echo(f"✅ Successfully updated to version {new_version}")
1649
+ except PackageNotFoundError:
1650
+ click.echo("✅ Successfully updated Voice Mode")
1651
+ else:
1652
+ click.echo(f"❌ Update failed: {result.stderr}")
1653
+ click.echo("Try running: pip install --upgrade voice-mode")
1654
+
1655
+ except FileNotFoundError as e:
1656
+ click.echo(f"❌ Update failed: {e}")
1657
+ click.echo("Please install UV or pip and try again")
1658
+
1659
+
1660
+ # Completions command
1661
+ @voice_mode_main_cli.command()
1662
+ @click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish']))
1663
+ @click.option('--install', is_flag=True, help='Install completion script to the appropriate location')
1664
+ def completions(shell, install):
1665
+ """Generate or install shell completion scripts.
1666
+
1667
+ Examples:
1668
+ voicemode completions bash # Output bash completion to stdout
1669
+ voicemode completions bash --install # Install to ~/.bash_completion.d/
1670
+ voicemode completions zsh --install # Install to ~/.zfunc/
1671
+ voicemode completions fish --install # Install to ~/.config/fish/completions/
1672
+ """
1673
+ from pathlib import Path
1674
+
1675
+ # Generate completion scripts based on shell type
1676
+ if shell == 'bash':
1677
+ completion_script = '''# bash completion for voicemode
1678
+ _voicemode_completion() {
1679
+ local IFS=$'\\n'
1680
+ local response
1681
+
1682
+ response=$(env _VOICEMODE_COMPLETE=bash_complete COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD voicemode 2>/dev/null)
1683
+
1684
+ for completion in $response; do
1685
+ IFS=',' read type value <<< "$completion"
1686
+
1687
+ if [[ $type == 'plain' ]]; then
1688
+ COMPREPLY+=("$value")
1689
+ elif [[ $type == 'file' ]]; then
1690
+ COMPREPLY+=("$value")
1691
+ elif [[ $type == 'dir' ]]; then
1692
+ COMPREPLY+=("$value")
1693
+ fi
1694
+ done
1695
+
1696
+ return 0
1697
+ }
1698
+
1699
+ complete -o default -F _voicemode_completion voicemode
1700
+ '''
1701
+
1702
+ elif shell == 'zsh':
1703
+ completion_script = '''#compdef voicemode
1704
+ # zsh completion for voicemode
1705
+
1706
+ _voicemode() {
1707
+ local -a response
1708
+ response=(${(f)"$(env _VOICEMODE_COMPLETE=zsh_complete COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) voicemode 2>/dev/null)"})
1709
+
1710
+ for completion in $response; do
1711
+ IFS=',' read type value <<< "$completion"
1712
+ compadd -U -- "$value"
1713
+ done
1714
+ }
1715
+
1716
+ compdef _voicemode voicemode
1717
+ '''
1718
+
1719
+ elif shell == 'fish':
1720
+ completion_script = '''# fish completion for voicemode
1721
+ function __fish_voicemode_complete
1722
+ set -l response (env _VOICEMODE_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) voicemode 2>/dev/null)
1723
+
1724
+ for completion in $response
1725
+ echo $completion
1726
+ end
1727
+ end
1728
+
1729
+ complete -c voicemode -f -a '(__fish_voicemode_complete)'
1730
+ '''
1731
+
1732
+ if install:
1733
+ # Define installation locations for each shell
1734
+ locations = {
1735
+ 'bash': '~/.bash_completion.d/voicemode',
1736
+ 'zsh': '~/.zfunc/_voicemode',
1737
+ 'fish': '~/.config/fish/completions/voicemode.fish'
1738
+ }
1739
+
1740
+ install_path = Path(locations[shell]).expanduser()
1741
+ install_path.parent.mkdir(parents=True, exist_ok=True)
1742
+
1743
+ # Write completion script to file
1744
+ install_path.write_text(completion_script)
1745
+ click.echo(f"✅ Installed {shell} completions to {install_path}")
1746
+
1747
+ # Provide shell-specific instructions
1748
+ if shell == 'bash':
1749
+ click.echo("\nTo activate now, run:")
1750
+ click.echo(f" source {install_path}")
1751
+ click.echo("\nTo activate permanently, add to ~/.bashrc:")
1752
+ click.echo(f" source {install_path}")
1753
+ elif shell == 'zsh':
1754
+ click.echo("\nTo activate now, run:")
1755
+ click.echo(" autoload -U compinit && compinit")
1756
+ click.echo("\nMake sure ~/.zfunc is in your fpath (add to ~/.zshrc):")
1757
+ click.echo(" fpath=(~/.zfunc $fpath)")
1758
+ elif shell == 'fish':
1759
+ click.echo("\nCompletions will be active in new fish sessions.")
1760
+ click.echo("To activate now, run:")
1761
+ click.echo(f" source {install_path}")
1762
+ else:
1763
+ # Output completion script to stdout
1764
+ click.echo(completion_script)
1765
+
1766
+
voice_mode/config.py CHANGED
@@ -16,8 +16,16 @@ from datetime import datetime
16
16
  # ==================== ENVIRONMENT CONFIGURATION ====================
17
17
 
18
18
  def load_voicemode_env():
19
- """Load configuration from .voicemode.env file if it exists, creating a default if not."""
20
- config_path = Path.home() / ".voicemode" / ".voicemode.env"
19
+ """Load configuration from voicemode.env file if it exists, creating a default if not."""
20
+ # Try new filename first
21
+ config_path = Path.home() / ".voicemode" / "voicemode.env"
22
+
23
+ # Backwards compatibility: check for old filename if new doesn't exist
24
+ if not config_path.exists():
25
+ old_path = Path.home() / ".voicemode" / ".voicemode.env"
26
+ if old_path.exists():
27
+ config_path = old_path
28
+ print(f"Warning: Using deprecated .voicemode.env - please rename to voicemode.env")
21
29
 
22
30
  if not config_path.exists():
23
31
  # Create default template
@@ -234,7 +242,7 @@ LIVEKIT_API_SECRET = os.getenv("LIVEKIT_API_SECRET", "secret")
234
242
  WHISPER_MODEL = os.getenv("VOICEMODE_WHISPER_MODEL", "large-v2")
235
243
  WHISPER_PORT = int(os.getenv("VOICEMODE_WHISPER_PORT", "2022"))
236
244
  WHISPER_LANGUAGE = os.getenv("VOICEMODE_WHISPER_LANGUAGE", "auto")
237
- WHISPER_MODEL_PATH = expand_path(os.getenv("VOICEMODE_WHISPER_MODEL_PATH", str(BASE_DIR / "models" / "whisper")))
245
+ WHISPER_MODEL_PATH = expand_path(os.getenv("VOICEMODE_WHISPER_MODEL_PATH", str(Path.home() / ".voicemode" / "services" / "whisper" / "models")))
238
246
 
239
247
  # ==================== KOKORO CONFIGURATION ====================
240
248
 
@@ -1 +1 @@
1
- uvJyMdD1IAhgbf_LCTQE6
1
+ wQ5pxzPmwjlzdUfJwSjMg