voice-mode 2.27.0__py3-none-any.whl → 2.28.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 (83) hide show
  1. voice_mode/__version__.py +1 -1
  2. voice_mode/cli.py +152 -37
  3. voice_mode/cli_commands/exchanges.py +6 -0
  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 +3 -3
  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 +3 -3
  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-08be62ed6e344292.js → layout-a9d79fcaeb3295f5.js} +1 -1
  56. voice_mode/frontend/.next/static/chunks/app/page-011e46e13f394b9b.js +1 -0
  57. voice_mode/frontend/.next/static/chunks/{main-app-413f77c1f2c53e3f.js → main-app-b03681837de4dca6.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 +6 -6
  63. voice_mode/tools/converse.py +44 -24
  64. voice_mode/tools/service.py +30 -3
  65. voice_mode/tools/services/kokoro/install.py +1 -1
  66. voice_mode/tools/services/whisper/__init__.py +15 -5
  67. voice_mode/tools/services/whisper/install.py +40 -9
  68. voice_mode/tools/services/whisper/list_models.py +14 -14
  69. voice_mode/tools/services/whisper/model_active.py +54 -0
  70. voice_mode/tools/services/whisper/model_benchmark.py +159 -0
  71. voice_mode/tools/services/whisper/{download_model.py → model_install.py} +72 -11
  72. voice_mode/tools/services/whisper/model_remove.py +36 -0
  73. voice_mode/tools/services/whisper/models.py +225 -26
  74. voice_mode/utils/services/whisper_helpers.py +206 -19
  75. voice_mode/utils/services/whisper_version.py +138 -0
  76. {voice_mode-2.27.0.dist-info → voice_mode-2.28.0.dist-info}/METADATA +5 -1
  77. {voice_mode-2.27.0.dist-info → voice_mode-2.28.0.dist-info}/RECORD +81 -78
  78. voice_mode/frontend/.next/static/chunks/app/page-80fc72669f25298f.js +0 -1
  79. voice_mode/tools/services/whisper/list_models_tool.py +0 -65
  80. /voice_mode/frontend/.next/static/{wQ5pxzPmwjlzdUfJwSjMg → cSCYUZbU1EJR-gEGqdoa-}/_buildManifest.js +0 -0
  81. /voice_mode/frontend/.next/static/{wQ5pxzPmwjlzdUfJwSjMg → cSCYUZbU1EJR-gEGqdoa-}/_ssgManifest.js +0 -0
  82. {voice_mode-2.27.0.dist-info → voice_mode-2.28.0.dist-info}/WHEEL +0 -0
  83. {voice_mode-2.27.0.dist-info → voice_mode-2.28.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.27.0"
3
+ __version__ = "2.28.0"
voice_mode/cli.py CHANGED
@@ -58,18 +58,21 @@ def voice_mode() -> None:
58
58
 
59
59
  # Service group commands
60
60
  @voice_mode_main_cli.group()
61
+ @click.help_option('-h', '--help', help='Show this message and exit')
61
62
  def kokoro():
62
63
  """Manage Kokoro TTS service."""
63
64
  pass
64
65
 
65
66
 
66
67
  @voice_mode_main_cli.group()
68
+ @click.help_option('-h', '--help', help='Show this message and exit')
67
69
  def whisper():
68
70
  """Manage Whisper STT service."""
69
71
  pass
70
72
 
71
73
 
72
74
  @voice_mode_main_cli.group()
75
+ @click.help_option('-h', '--help', help='Show this message and exit')
73
76
  def livekit():
74
77
  """Manage LiveKit RTC service."""
75
78
  pass
@@ -128,6 +131,7 @@ def disable():
128
131
 
129
132
 
130
133
  @kokoro.command()
134
+ @click.help_option('-h', '--help')
131
135
  @click.option('--lines', '-n', default=50, help='Number of log lines to show')
132
136
  def logs(lines):
133
137
  """View Kokoro service logs."""
@@ -172,6 +176,7 @@ def health():
172
176
 
173
177
 
174
178
  @kokoro.command()
179
+ @click.help_option('-h', '--help')
175
180
  @click.option('--install-dir', help='Directory to install kokoro-fastapi')
176
181
  @click.option('--port', default=8880, help='Port to configure for the service')
177
182
  @click.option('--force', '-f', is_flag=True, help='Force reinstall even if already installed')
@@ -209,6 +214,7 @@ def install(install_dir, port, force, version, auto_enable):
209
214
 
210
215
 
211
216
  @kokoro.command()
217
+ @click.help_option('-h', '--help')
212
218
  @click.option('--remove-models', is_flag=True, help='Also remove downloaded Kokoro models')
213
219
  @click.option('--remove-all-data', is_flag=True, help='Remove all Kokoro data including logs and cache')
214
220
  @click.confirmation_option(prompt='Are you sure you want to uninstall Kokoro?')
@@ -294,6 +300,7 @@ def disable():
294
300
 
295
301
 
296
302
  @whisper.command()
303
+ @click.help_option('-h', '--help')
297
304
  @click.option('--lines', '-n', default=50, help='Number of log lines to show')
298
305
  def logs(lines):
299
306
  """View Whisper service logs."""
@@ -316,7 +323,7 @@ def health():
316
323
  import subprocess
317
324
  try:
318
325
  result = subprocess.run(
319
- ["curl", "-s", "http://127.0.0.1:8090/health"],
326
+ ["curl", "-s", "http://127.0.0.1:2022/health"],
320
327
  capture_output=True, text=True, timeout=5
321
328
  )
322
329
  if result.returncode == 0:
@@ -330,7 +337,7 @@ def health():
330
337
  except json.JSONDecodeError:
331
338
  click.echo("✅ Whisper is responding (non-JSON response)")
332
339
  else:
333
- click.echo("❌ Whisper not responding on port 8090")
340
+ click.echo("❌ Whisper not responding on port 2022")
334
341
  except subprocess.TimeoutExpired:
335
342
  click.echo("❌ Whisper health check timed out")
336
343
  except Exception as e:
@@ -338,6 +345,7 @@ def health():
338
345
 
339
346
 
340
347
  @whisper.command()
348
+ @click.help_option('-h', '--help')
341
349
  @click.option('--install-dir', help='Directory to install whisper.cpp')
342
350
  @click.option('--model', default='large-v2', help='Whisper model to download (default: large-v2)')
343
351
  @click.option('--use-gpu/--no-gpu', default=None, help='Enable GPU support if available')
@@ -386,6 +394,7 @@ def install(install_dir, model, use_gpu, force, version, auto_enable):
386
394
 
387
395
 
388
396
  @whisper.command()
397
+ @click.help_option('-h', '--help')
389
398
  @click.option('--remove-models', is_flag=True, help='Also remove downloaded Whisper models')
390
399
  @click.option('--remove-all-data', is_flag=True, help='Remove all Whisper data including logs and transcriptions')
391
400
  @click.confirmation_option(prompt='Are you sure you want to uninstall Whisper?')
@@ -422,6 +431,7 @@ def uninstall(remove_models, remove_all_data):
422
431
 
423
432
 
424
433
  @whisper.group("model")
434
+ @click.help_option('-h', '--help', help='Show this message and exit')
425
435
  def whisper_model():
426
436
  """Manage Whisper models.
427
437
 
@@ -434,6 +444,7 @@ def whisper_model():
434
444
 
435
445
 
436
446
  @whisper_model.command("active")
447
+ @click.help_option('-h', '--help')
437
448
  @click.argument('model_name', required=False)
438
449
  def whisper_model_active(model_name):
439
450
  """Show or set the active Whisper model.
@@ -442,34 +453,34 @@ def whisper_model_active(model_name):
442
453
  With MODEL_NAME: Sets the active model (updates VOICEMODE_WHISPER_MODEL)
443
454
  """
444
455
  from voice_mode.tools.services.whisper.models import (
445
- get_current_model,
446
- WHISPER_MODELS,
447
- is_model_installed,
448
- set_current_model
456
+ get_active_model,
457
+ WHISPER_MODEL_REGISTRY,
458
+ is_whisper_model_installed,
459
+ set_active_model
449
460
  )
450
461
  import os
451
462
  import subprocess
452
463
 
453
464
  if model_name:
454
465
  # Set model mode
455
- if model_name not in WHISPER_MODELS:
466
+ if model_name not in WHISPER_MODEL_REGISTRY:
456
467
  click.echo(f"Error: '{model_name}' is not a valid model.", err=True)
457
468
  click.echo("\nAvailable models:", err=True)
458
- for name in WHISPER_MODELS.keys():
469
+ for name in WHISPER_MODEL_REGISTRY.keys():
459
470
  click.echo(f" - {name}", err=True)
460
471
  return
461
472
 
462
473
  # Check if model is installed
463
- if not is_model_installed(model_name):
474
+ if not is_whisper_model_installed(model_name):
464
475
  click.echo(f"Error: Model '{model_name}' is not installed.", err=True)
465
476
  click.echo(f"Install it with: voice-mode whisper model install {model_name}", err=True)
466
477
  raise click.Abort()
467
478
 
468
479
  # Get previous model
469
- previous_model = get_current_model()
480
+ previous_model = get_active_model()
470
481
 
471
482
  # Update the configuration file
472
- set_current_model(model_name)
483
+ set_active_model(model_name)
473
484
 
474
485
  click.echo(f"✓ Active model set to: {model_name}")
475
486
  if previous_model != model_name:
@@ -492,14 +503,14 @@ def whisper_model_active(model_name):
492
503
 
493
504
  else:
494
505
  # Show current model
495
- current = get_current_model()
506
+ current = get_active_model()
496
507
 
497
508
  # Check if current model is installed
498
- installed = is_model_installed(current)
509
+ installed = is_whisper_model_installed(current)
499
510
  status = click.style("[✓ Installed]", fg="green") if installed else click.style("[Not installed]", fg="red")
500
511
 
501
512
  # Get model info
502
- model_info = WHISPER_MODELS.get(current, {})
513
+ model_info = WHISPER_MODEL_REGISTRY.get(current, {})
503
514
 
504
515
  click.echo(f"\nActive Whisper model: {click.style(current, fg='yellow', bold=True)} {status}")
505
516
  if model_info:
@@ -524,24 +535,25 @@ def whisper_model_active(model_name):
524
535
  def whisper_models():
525
536
  """List available Whisper models and their installation status."""
526
537
  from voice_mode.tools.services.whisper.models import (
527
- WHISPER_MODELS,
538
+ WHISPER_MODEL_REGISTRY,
528
539
  get_model_directory,
529
- get_current_model,
530
- is_model_installed,
531
- get_installed_models,
532
- format_size
540
+ get_active_model,
541
+ is_whisper_model_installed,
542
+ get_installed_whisper_models,
543
+ format_size,
544
+ has_whisper_coreml_model
533
545
  )
534
546
 
535
547
  model_dir = get_model_directory()
536
- current_model = get_current_model()
537
- installed_models = get_installed_models()
548
+ current_model = get_active_model()
549
+ installed_models = get_installed_whisper_models()
538
550
 
539
551
  # Calculate totals
540
552
  total_installed_size = sum(
541
- WHISPER_MODELS[m]["size_mb"] for m in installed_models
553
+ WHISPER_MODEL_REGISTRY[m]["size_mb"] for m in installed_models
542
554
  )
543
555
  total_available_size = sum(
544
- m["size_mb"] for m in WHISPER_MODELS.values()
556
+ m["size_mb"] for m in WHISPER_MODEL_REGISTRY.values()
545
557
  )
546
558
 
547
559
  # Print header
@@ -549,9 +561,9 @@ def whisper_models():
549
561
  click.echo("")
550
562
 
551
563
  # Print models table
552
- for model_name, info in WHISPER_MODELS.items():
564
+ for model_name, info in WHISPER_MODEL_REGISTRY.items():
553
565
  # Check status
554
- is_installed = is_model_installed(model_name)
566
+ is_installed = is_whisper_model_installed(model_name)
555
567
  is_current = model_name == current_model
556
568
 
557
569
  # Format status
@@ -564,7 +576,11 @@ def whisper_models():
564
576
 
565
577
  # Format installation status
566
578
  if is_installed:
567
- install_status = click.style("[✓ Installed]", fg="green")
579
+ # Check for Core ML model
580
+ if has_whisper_coreml_model(model_name):
581
+ install_status = click.style("[✓ Installed+ML]", fg="green")
582
+ else:
583
+ install_status = click.style("[✓ Installed]", fg="green")
568
584
  else:
569
585
  install_status = click.style("[ Download ]", fg="bright_black")
570
586
 
@@ -581,7 +597,7 @@ def whisper_models():
581
597
  desc = click.style(desc, fg="yellow")
582
598
 
583
599
  # Print row
584
- click.echo(f"{status} {model_display} {install_status:14} {size_str} {lang_str} {desc}")
600
+ click.echo(f"{status} {model_display} {install_status:18} {size_str} {lang_str} {desc}")
585
601
 
586
602
  # Print footer
587
603
  click.echo("")
@@ -593,6 +609,7 @@ def whisper_models():
593
609
 
594
610
 
595
611
  @whisper_model.command("install")
612
+ @click.help_option('-h', '--help')
596
613
  @click.argument('model', default='large-v2')
597
614
  @click.option('--force', '-f', is_flag=True, help='Re-download even if model exists')
598
615
  @click.option('--skip-core-ml', is_flag=True, help='Skip Core ML conversion on Apple Silicon')
@@ -606,8 +623,11 @@ def whisper_model_install(model, force, skip_core_ml):
606
623
  medium, medium.en, large-v1, large-v2, large-v3, large-v3-turbo
607
624
  """
608
625
  import json
609
- from voice_mode.tools.services.whisper.download_model import download_model
610
- result = asyncio.run(download_model.fn(
626
+ import voice_mode.tools.services.whisper.model_install as install_module
627
+ # Get the actual function from the MCP tool wrapper
628
+ tool = install_module.whisper_model_install
629
+ install_func = tool.fn if hasattr(tool, 'fn') else tool
630
+ result = asyncio.run(install_func(
611
631
  model=model,
612
632
  force_download=force,
613
633
  skip_core_ml=skip_core_ml
@@ -645,6 +665,7 @@ def whisper_model_install(model, force, skip_core_ml):
645
665
 
646
666
 
647
667
  @whisper_model.command("remove")
668
+ @click.help_option('-h', '--help')
648
669
  @click.argument('model')
649
670
  @click.option('--force', '-f', is_flag=True, help='Remove without confirmation')
650
671
  def whisper_model_remove(model, force):
@@ -653,28 +674,28 @@ def whisper_model_remove(model, force):
653
674
  MODEL is the name of the model to remove (e.g., 'large-v2').
654
675
  """
655
676
  from voice_mode.tools.services.whisper.models import (
656
- WHISPER_MODELS,
657
- is_model_installed,
677
+ WHISPER_MODEL_REGISTRY,
678
+ is_whisper_model_installed,
658
679
  get_model_directory,
659
- get_current_model
680
+ get_active_model
660
681
  )
661
682
  import os
662
683
 
663
684
  # Validate model name
664
- if model not in WHISPER_MODELS:
685
+ if model not in WHISPER_MODEL_REGISTRY:
665
686
  click.echo(f"Error: '{model}' is not a valid model.", err=True)
666
687
  click.echo("\nAvailable models:", err=True)
667
- for name in WHISPER_MODELS.keys():
688
+ for name in WHISPER_MODEL_REGISTRY.keys():
668
689
  click.echo(f" - {name}", err=True)
669
690
  ctx.exit(1)
670
691
 
671
692
  # Check if model is installed
672
- if not is_model_installed(model):
693
+ if not is_whisper_model_installed(model):
673
694
  click.echo(f"Model '{model}' is not installed.")
674
695
  return
675
696
 
676
697
  # Check if it's the current model
677
- current = get_current_model()
698
+ current = get_active_model()
678
699
  if model == current:
679
700
  click.echo(f"Warning: '{model}' is the currently selected model.", err=True)
680
701
  if not force:
@@ -683,7 +704,7 @@ def whisper_model_remove(model, force):
683
704
 
684
705
  # Get model path
685
706
  model_dir = get_model_directory()
686
- model_info = WHISPER_MODELS[model]
707
+ model_info = WHISPER_MODEL_REGISTRY[model]
687
708
  model_path = model_dir / model_info["filename"]
688
709
 
689
710
  # Also check for Core ML models
@@ -712,6 +733,83 @@ def whisper_model_remove(model, force):
712
733
  click.echo(f"Error removing model: {e}", err=True)
713
734
 
714
735
 
736
+ @whisper_model.command("benchmark")
737
+ @click.help_option('-h', '--help')
738
+ @click.option('--models', default='installed', help='Models to benchmark: installed, all, or comma-separated list')
739
+ @click.option('--sample', help='Audio file to use for benchmarking')
740
+ @click.option('--runs', default=1, help='Number of benchmark runs per model')
741
+ def whisper_model_benchmark_cmd(models, sample, runs):
742
+ """Benchmark Whisper model performance.
743
+
744
+ Runs performance tests on specified models to help choose the optimal model
745
+ for your use case based on speed vs accuracy trade-offs.
746
+ """
747
+ from voice_mode.tools.services.whisper.model_benchmark import whisper_model_benchmark
748
+
749
+ # Parse models parameter
750
+ if ',' in models:
751
+ model_list = [m.strip() for m in models.split(',')]
752
+ else:
753
+ model_list = models
754
+
755
+ # Run benchmark
756
+ result = asyncio.run(whisper_model_benchmark(
757
+ models=model_list,
758
+ sample_file=sample,
759
+ runs=runs
760
+ ))
761
+
762
+ if not result.get('success'):
763
+ click.echo(f"❌ Benchmark failed: {result.get('error', 'Unknown error')}", err=True)
764
+ return
765
+
766
+ # Display results
767
+ click.echo("\n" + "="*60)
768
+ click.echo("Whisper Model Benchmark Results")
769
+ click.echo("="*60)
770
+
771
+ if result.get('sample_file'):
772
+ click.echo(f"Sample: {result['sample_file']}")
773
+ if result.get('runs_per_model') > 1:
774
+ click.echo(f"Runs per model: {result['runs_per_model']} (showing best)")
775
+ click.echo("")
776
+
777
+ # Display benchmark table
778
+ click.echo(f"{'Model':<20} {'Load (ms)':<12} {'Encode (ms)':<12} {'Total (ms)':<12} {'Speed':<10}")
779
+ click.echo("-"*70)
780
+
781
+ for bench in result.get('benchmarks', []):
782
+ if bench.get('success'):
783
+ model = bench['model']
784
+ load_time = f"{bench.get('load_time_ms', 0):.1f}"
785
+ encode_time = f"{bench.get('encode_time_ms', 0):.1f}"
786
+ total_time = f"{bench.get('total_time_ms', 0):.1f}"
787
+ rtf = f"{bench.get('real_time_factor', 0):.1f}x"
788
+
789
+ # Highlight fastest model
790
+ if bench['model'] == result.get('fastest_model'):
791
+ model = click.style(model, fg='green', bold=True)
792
+ rtf = click.style(rtf, fg='green', bold=True)
793
+
794
+ click.echo(f"{model:<20} {load_time:<12} {encode_time:<12} {total_time:<12} {rtf:<10}")
795
+ else:
796
+ click.echo(f"{bench['model']:<20} {'Failed':<12} {bench.get('error', 'Unknown error')}")
797
+
798
+ # Display recommendations
799
+ if result.get('recommendations'):
800
+ click.echo("\nRecommendations:")
801
+ for rec in result['recommendations']:
802
+ click.echo(f" • {rec}")
803
+
804
+ # Summary
805
+ if result.get('fastest_model'):
806
+ click.echo(f"\nFastest model: {click.style(result['fastest_model'], fg='yellow', bold=True)}")
807
+ click.echo(f"Processing time: {result.get('fastest_time_ms', 'N/A')} ms")
808
+
809
+ click.echo("\nNote: Speed values show real-time factor (higher is better)")
810
+ click.echo(" 1.0x = real-time, 10x = 10 times faster than real-time")
811
+
812
+
715
813
  # LiveKit service commands
716
814
  @livekit.command()
717
815
  def status():
@@ -762,6 +860,7 @@ def disable():
762
860
 
763
861
 
764
862
  @livekit.command()
863
+ @click.help_option('-h', '--help')
765
864
  @click.option('--lines', '-n', default=50, help='Number of log lines to show')
766
865
  def logs(lines):
767
866
  """View LiveKit service logs."""
@@ -785,6 +884,7 @@ def update():
785
884
 
786
885
 
787
886
  @livekit.command()
887
+ @click.help_option('-h', '--help')
788
888
  @click.option('--install-dir', help='Directory to install LiveKit')
789
889
  @click.option('--port', default=7880, help='Port for LiveKit server (default: 7880)')
790
890
  @click.option('--force', '-f', is_flag=True, help='Force reinstall even if already installed')
@@ -825,6 +925,7 @@ def install(install_dir, port, force, version, auto_enable):
825
925
 
826
926
 
827
927
  @livekit.command()
928
+ @click.help_option('-h', '--help')
828
929
  @click.option('--remove-config', is_flag=True, help='Also remove LiveKit configuration files')
829
930
  @click.option('--remove-all-data', is_flag=True, help='Remove all LiveKit data including logs')
830
931
  @click.confirmation_option(prompt='Are you sure you want to uninstall LiveKit?')
@@ -854,12 +955,14 @@ def uninstall(remove_config, remove_all_data):
854
955
 
855
956
  # LiveKit frontend subcommands
856
957
  @livekit.group()
958
+ @click.help_option('-h', '--help', help='Show this message and exit')
857
959
  def frontend():
858
960
  """Manage LiveKit Voice Assistant Frontend."""
859
961
  pass
860
962
 
861
963
 
862
964
  @frontend.command("install")
965
+ @click.help_option('-h', '--help')
863
966
  @click.option('--auto-enable/--no-auto-enable', default=None, help='Enable service after installation (default: from config)')
864
967
  def frontend_install(auto_enable):
865
968
  """Install and setup LiveKit Voice Assistant Frontend."""
@@ -887,6 +990,7 @@ def frontend_install(auto_enable):
887
990
 
888
991
 
889
992
  @frontend.command("start")
993
+ @click.help_option('-h', '--help')
890
994
  @click.option('--port', default=3000, help='Port to run frontend on (default: 3000)')
891
995
  @click.option('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)')
892
996
  def frontend_start(port, host):
@@ -967,6 +1071,7 @@ def frontend_open():
967
1071
 
968
1072
 
969
1073
  @frontend.command("logs")
1074
+ @click.help_option('-h', '--help')
970
1075
  @click.option("--lines", "-n", default=50, help="Number of lines to show (default: 50)")
971
1076
  @click.option("--follow", "-f", is_flag=True, help="Follow log output (tail -f)")
972
1077
  def frontend_logs(lines, follow):
@@ -1020,6 +1125,7 @@ def frontend_disable():
1020
1125
 
1021
1126
 
1022
1127
  @frontend.command("build")
1128
+ @click.help_option('-h', '--help')
1023
1129
  @click.option('--force', '-f', is_flag=True, help='Force rebuild even if build exists')
1024
1130
  def frontend_build(force):
1025
1131
  """Build frontend for production (requires Node.js)."""
@@ -1077,6 +1183,7 @@ def frontend_build(force):
1077
1183
 
1078
1184
  # Configuration management group
1079
1185
  @voice_mode_main_cli.group()
1186
+ @click.help_option('-h', '--help', help='Show this message and exit')
1080
1187
  def config():
1081
1188
  """Manage voice-mode configuration."""
1082
1189
  pass
@@ -1091,6 +1198,7 @@ def config_list():
1091
1198
 
1092
1199
 
1093
1200
  @config.command("get")
1201
+ @click.help_option('-h', '--help')
1094
1202
  @click.argument('key')
1095
1203
  def config_get(key):
1096
1204
  """Get a configuration value."""
@@ -1128,6 +1236,7 @@ def config_get(key):
1128
1236
 
1129
1237
 
1130
1238
  @config.command("set")
1239
+ @click.help_option('-h', '--help')
1131
1240
  @click.argument('key')
1132
1241
  @click.argument('value')
1133
1242
  def config_set(key, value):
@@ -1139,6 +1248,7 @@ def config_set(key, value):
1139
1248
 
1140
1249
  # Shell completion group
1141
1250
  @voice_mode_main_cli.group()
1251
+ @click.help_option('-h', '--help', help='Show this message and exit')
1142
1252
  def completion():
1143
1253
  """Generate shell completion scripts for voice-mode."""
1144
1254
  pass
@@ -1218,6 +1328,7 @@ def completion_fish():
1218
1328
 
1219
1329
 
1220
1330
  @completion.command("install")
1331
+ @click.help_option('-h', '--help')
1221
1332
  @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish', 'auto']), default='auto', help='Shell type to install for')
1222
1333
  def completion_install(shell):
1223
1334
  """Show installation instructions for shell completion.
@@ -1283,6 +1394,7 @@ def completion_install(shell):
1283
1394
 
1284
1395
  # Diagnostics group
1285
1396
  @voice_mode_main_cli.group()
1397
+ @click.help_option('-h', '--help', help='Show this message and exit')
1286
1398
  def diag():
1287
1399
  """Diagnostic tools for voice-mode."""
1288
1400
  pass
@@ -1380,6 +1492,7 @@ voice_mode_main_cli.add_command(exchanges_cmd.exchanges)
1380
1492
 
1381
1493
  # Converse command - direct voice conversation from CLI
1382
1494
  @voice_mode_main_cli.command()
1495
+ @click.help_option('-h', '--help')
1383
1496
  @click.option('--message', '-m', default="Hello! How can I help you today?", help='Initial message to speak')
1384
1497
  @click.option('--wait/--no-wait', default=True, help='Wait for response after speaking')
1385
1498
  @click.option('--duration', '-d', type=float, default=30.0, help='Listen duration in seconds')
@@ -1578,6 +1691,7 @@ def version():
1578
1691
 
1579
1692
  # Update command
1580
1693
  @voice_mode_main_cli.command()
1694
+ @click.help_option('-h', '--help')
1581
1695
  @click.option('--force', is_flag=True, help='Force reinstall even if already up to date')
1582
1696
  def update(force):
1583
1697
  """Update Voice Mode to the latest version."""
@@ -1659,6 +1773,7 @@ def update(force):
1659
1773
 
1660
1774
  # Completions command
1661
1775
  @voice_mode_main_cli.command()
1776
+ @click.help_option('-h', '--help')
1662
1777
  @click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish']))
1663
1778
  @click.option('--install', is_flag=True, help='Install completion script to the appropriate location')
1664
1779
  def completions(shell, install):
@@ -20,12 +20,14 @@ from voice_mode.exchanges import (
20
20
 
21
21
 
22
22
  @click.group()
23
+ @click.help_option('-h', '--help', help='Show this message and exit')
23
24
  def exchanges():
24
25
  """Manage and view conversation exchange logs."""
25
26
  pass
26
27
 
27
28
 
28
29
  @exchanges.command()
30
+ @click.help_option('-h', '--help')
29
31
  @click.option('-f', '--format',
30
32
  type=click.Choice(['simple', 'pretty', 'json', 'raw']),
31
33
  default='simple',
@@ -86,6 +88,7 @@ def tail(format, stt, tts, full, no_color, date, transport, provider):
86
88
 
87
89
 
88
90
  @exchanges.command()
91
+ @click.help_option('-h', '--help')
89
92
  @click.option('-n', '--lines', type=int, default=20,
90
93
  help='Number of exchanges to show')
91
94
  @click.option('-c', '--conversation', help='Show specific conversation')
@@ -140,6 +143,7 @@ def view(lines, conversation, today, yesterday, date, format, reverse, no_color)
140
143
 
141
144
 
142
145
  @exchanges.command()
146
+ @click.help_option('-h', '--help')
143
147
  @click.argument('query')
144
148
  @click.option('-n', '--max-results', type=int, default=50,
145
149
  help='Maximum results to show')
@@ -221,6 +225,7 @@ def search(query, max_results, days, exchange_type, regex, ignore_case,
221
225
 
222
226
 
223
227
  @exchanges.command()
228
+ @click.help_option('-h', '--help')
224
229
  @click.option('-d', '--days', type=int, help='Stats for last N days')
225
230
  @click.option('--by-hour', is_flag=True, help='Group by hour')
226
231
  @click.option('--by-provider', is_flag=True, help='Group by provider')
@@ -333,6 +338,7 @@ def stats(days, by_hour, by_provider, by_transport, timing, conversations,
333
338
 
334
339
 
335
340
  @exchanges.command()
341
+ @click.help_option('-h', '--help')
336
342
  @click.option('-c', '--conversation', help='Export specific conversation')
337
343
  @click.option('-d', '--date', type=click.DateTime(formats=['%Y-%m-%d']),
338
344
  help='Export date range')
@@ -1 +1 @@
1
- wQ5pxzPmwjlzdUfJwSjMg
1
+ cSCYUZbU1EJR-gEGqdoa-
@@ -4,25 +4,25 @@
4
4
  "static/chunks/webpack-0ea9b80f19935b70.js",
5
5
  "static/chunks/fd9d1056-af324d327b243cf1.js",
6
6
  "static/chunks/117-40bc79a2b97edb21.js",
7
- "static/chunks/main-app-413f77c1f2c53e3f.js",
7
+ "static/chunks/main-app-b03681837de4dca6.js",
8
8
  "static/chunks/app/_not-found/page-5011050e402ab9c8.js"
9
9
  ],
10
10
  "/layout": [
11
11
  "static/chunks/webpack-0ea9b80f19935b70.js",
12
12
  "static/chunks/fd9d1056-af324d327b243cf1.js",
13
13
  "static/chunks/117-40bc79a2b97edb21.js",
14
- "static/chunks/main-app-413f77c1f2c53e3f.js",
14
+ "static/chunks/main-app-b03681837de4dca6.js",
15
15
  "static/css/a2f49a47752b5010.css",
16
- "static/chunks/app/layout-08be62ed6e344292.js"
16
+ "static/chunks/app/layout-a9d79fcaeb3295f5.js"
17
17
  ],
18
18
  "/page": [
19
19
  "static/chunks/webpack-0ea9b80f19935b70.js",
20
20
  "static/chunks/fd9d1056-af324d327b243cf1.js",
21
21
  "static/chunks/117-40bc79a2b97edb21.js",
22
- "static/chunks/main-app-413f77c1f2c53e3f.js",
22
+ "static/chunks/main-app-b03681837de4dca6.js",
23
23
  "static/chunks/144d3bae-2d5f122b82426d88.js",
24
24
  "static/chunks/471-bd4b96a33883dfa2.js",
25
- "static/chunks/app/page-80fc72669f25298f.js"
25
+ "static/chunks/app/page-011e46e13f394b9b.js"
26
26
  ]
27
27
  }
28
28
  }
@@ -1 +1 @@
1
- {"/_not-found/page":"/_not-found","/page":"/","/favicon.ico/route":"/favicon.ico","/api/connection-details/route":"/api/connection-details"}
1
+ {"/_not-found/page":"/_not-found","/favicon.ico/route":"/favicon.ico","/page":"/","/api/connection-details/route":"/api/connection-details"}
@@ -5,14 +5,14 @@
5
5
  "devFiles": [],
6
6
  "ampDevFiles": [],
7
7
  "lowPriorityFiles": [
8
- "static/wQ5pxzPmwjlzdUfJwSjMg/_buildManifest.js",
9
- "static/wQ5pxzPmwjlzdUfJwSjMg/_ssgManifest.js"
8
+ "static/cSCYUZbU1EJR-gEGqdoa-/_buildManifest.js",
9
+ "static/cSCYUZbU1EJR-gEGqdoa-/_ssgManifest.js"
10
10
  ],
11
11
  "rootMainFiles": [
12
12
  "static/chunks/webpack-0ea9b80f19935b70.js",
13
13
  "static/chunks/fd9d1056-af324d327b243cf1.js",
14
14
  "static/chunks/117-40bc79a2b97edb21.js",
15
- "static/chunks/main-app-413f77c1f2c53e3f.js"
15
+ "static/chunks/main-app-b03681837de4dca6.js"
16
16
  ],
17
17
  "pages": {
18
18
  "/_app": [