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.
- voice_mode/__version__.py +1 -1
- voice_mode/cli.py +611 -3
- voice_mode/config.py +11 -3
- voice_mode/frontend/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/app-path-routes-manifest.json +1 -1
- voice_mode/frontend/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
- voice_mode/frontend/.next/next-server.js.nft.json +1 -1
- voice_mode/frontend/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/server/app/page.js +2 -2
- voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/app-paths-manifest.json +1 -1
- voice_mode/frontend/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/page.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/server.js +1 -1
- voice_mode/frontend/.next/static/chunks/app/{layout-0e969b20634a3137.js → layout-08be62ed6e344292.js} +1 -1
- voice_mode/frontend/.next/static/chunks/app/page-80fc72669f25298f.js +1 -0
- voice_mode/frontend/.next/static/chunks/{main-app-b9e128659aafd50e.js → main-app-413f77c1f2c53e3f.js} +1 -1
- voice_mode/frontend/.next/trace +43 -43
- voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
- voice_mode/frontend/.next/types/app/layout.ts +1 -1
- voice_mode/frontend/.next/types/app/page.ts +1 -1
- voice_mode/frontend/package-lock.json +8 -8
- voice_mode/resources/configuration.py +8 -4
- voice_mode/resources/whisper_models.py +10 -13
- voice_mode/templates/systemd/voicemode-frontend.service +1 -1
- voice_mode/tools/configuration_management.py +7 -2
- voice_mode/tools/converse.py +31 -0
- voice_mode/tools/services/kokoro/install.py +3 -2
- voice_mode/tools/services/whisper/__init__.py +13 -0
- voice_mode/tools/services/whisper/install.py +3 -2
- voice_mode/tools/services/whisper/list_models.py +70 -0
- voice_mode/tools/services/whisper/list_models_tool.py +65 -0
- voice_mode/tools/services/whisper/models.py +274 -0
- {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/METADATA +1 -1
- {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/RECORD +79 -75
- voice_mode/frontend/.next/static/chunks/app/page-db597c111ebcc19f.js +0 -1
- /voice_mode/frontend/.next/static/{uvJyMdD1IAhgbf_LCTQE6 → wQ5pxzPmwjlzdUfJwSjMg}/_buildManifest.js +0 -0
- /voice_mode/frontend/.next/static/{uvJyMdD1IAhgbf_LCTQE6 → wQ5pxzPmwjlzdUfJwSjMg}/_ssgManifest.js +0 -0
- {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/WHEEL +0 -0
- {voice_mode-2.26.0.dist-info → voice_mode-2.27.0.dist-info}/entry_points.txt +0 -0
voice_mode/__version__.py
CHANGED
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.
|
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
|
428
|
-
"""
|
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
|
20
|
-
|
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(
|
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
|
-
|
1
|
+
wQ5pxzPmwjlzdUfJwSjMg
|