voice-mode 4.4.0__tar.gz → 4.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voice_mode-4.4.0 → voice_mode-4.5.0}/CHANGELOG.md +33 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/PKG-INFO +5 -2
- {voice_mode-4.4.0 → voice_mode-4.5.0}/README.md +4 -1
- {voice_mode-4.4.0 → voice_mode-4.5.0}/pyproject.toml +8 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/__version__.py +1 -1
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli.py +79 -3
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/transcribe.py +7 -6
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/config.py +1 -1
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/conversation_logger.py +6 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/core.py +9 -2
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/provider_discovery.py +55 -79
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/providers.py +61 -45
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/simple_failover.py +41 -12
- voice_mode-4.5.0/voice_mode/tools/__init__.py +158 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/converse.py +148 -337
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/diagnostics.py +2 -1
- voice_mode-4.5.0/voice_mode/tools/voice_registry.py +62 -0
- voice_mode-4.4.0/voice_mode/tools/__init__.py +0 -50
- voice_mode-4.4.0/voice_mode/tools/voice_registry.py +0 -66
- {voice_mode-4.4.0 → voice_mode-4.5.0}/.gitignore +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/build_hooks.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/__main__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/claude.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/exchanges.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/hook.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/cli_commands/pronounce_commands.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/data/default_pronunciation.yaml +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/data/versions.json +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/conversations.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/filters.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/formatters.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/models.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/reader.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/exchanges/stats.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/README.md +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/app/api/connection-details/route.ts +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/app/favicon.ico +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/app/globals.css +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/app/layout.tsx +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/app/page.tsx +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/components/CloseIcon.tsx +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/components/NoAgentNotification.tsx +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/components/TranscriptionView.tsx +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/hooks/useCombinedTranscriptions.ts +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/hooks/useLocalMicTrack.ts +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/next-env.d.ts +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/next.config.mjs +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/package-lock.json +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/package.json +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/pnpm-lock.yaml +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/postcss.config.mjs +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/tailwind.config.ts +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/frontend/tsconfig.json +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/prompts/README.md +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/prompts/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/prompts/converse.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/prompts/release_notes.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/prompts/services.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/pronounce.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/audio_files.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/changelog.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/configuration.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/statistics.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/version.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/resources/whisper_models.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/server.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/shared.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/statistics.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/statistics_tracking.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/streaming.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/com.voicemode.frontend.plist +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/com.voicemode.livekit.plist +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/scripts/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/scripts/start-whisper-server.sh +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/systemd/voicemode-frontend.service +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/systemd/voicemode-livekit.service +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/configuration_management.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/dependencies.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/devices.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/pronounce.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/providers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/service.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/kokoro/install.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/list_versions.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/livekit/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/livekit/frontend.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/livekit/install.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/livekit/production_server.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/livekit/uninstall.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/version_info.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/install.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/list_models.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/model_active.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/model_benchmark.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/model_install.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/model_remove.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/models.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/services/whisper/uninstall.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/sound_fonts/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/sound_fonts/audio_player.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/sound_fonts/hook_handler.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/sound_fonts/player.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/statistics.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/transcription/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/transcription/backends.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/transcription/core.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/transcription/formats.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/tools/transcription/types.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/__init__.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/audio_diagnostics.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/event_logger.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/ffmpeg_check.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/format_migration.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/gpu_detection.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/migration_helpers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/common.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/coreml_setup.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/kokoro_helpers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/livekit_helpers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/whisper_helpers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/services/whisper_version.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/utils/version_helpers.py +0 -0
- {voice_mode-4.4.0 → voice_mode-4.5.0}/voice_mode/version.py +0 -0
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [4.5.0] - 2025-09-18
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- **Enhanced STT Logging**
|
14
|
+
- Add comprehensive logging for speech-to-text operations
|
15
|
+
- Log provider selection and fallback attempts
|
16
|
+
- Include transcription details and provider info in logs
|
17
|
+
|
18
|
+
- **Configuration Management**
|
19
|
+
- Add `voicemode config edit` command for easy configuration file editing
|
20
|
+
- Support custom editor selection via --editor flag
|
21
|
+
- Automatically open configuration file in default editor
|
22
|
+
|
23
|
+
- **Tool Environment Variables**
|
24
|
+
- Replace VOICEMODE_TOOLS with VOICEMODE_TOOLS_ENABLED and VOICEMODE_TOOLS_DISABLED
|
25
|
+
- Allow fine-grained control over tool availability
|
26
|
+
- Support comma-separated lists for enabling/disabling specific tools
|
27
|
+
|
28
|
+
### Changed
|
29
|
+
- **Provider Selection Architecture**
|
30
|
+
- Consolidate dual provider selection systems into single simple failover approach
|
31
|
+
- Remove SIMPLE_FAILOVER configuration - simple failover is now the only mode
|
32
|
+
- Simplify get_tts_config and get_stt_config to use direct configuration
|
33
|
+
- Eliminate ~400 lines of unused provider registry selection logic
|
34
|
+
- Provider registry now only stores endpoint info without complex selection
|
35
|
+
|
36
|
+
### Fixed
|
37
|
+
- Disable OpenAI client retries for local endpoints to avoid delays
|
38
|
+
- Fix logger name consistency (voicemode vs voice-mode) for STT logging
|
39
|
+
- Prevent test_installers from killing running voice services during tests
|
40
|
+
- Update tests to work with refactored provider system
|
41
|
+
- Resolve test failures related to new environment variables
|
42
|
+
|
10
43
|
## [4.4.0] - 2025-09-10
|
11
44
|
|
12
45
|
### Added
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: voice-mode
|
3
|
-
Version: 4.
|
3
|
+
Version: 4.5.0
|
4
4
|
Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
|
5
5
|
Project-URL: Homepage, https://github.com/mbailey/voicemode
|
6
6
|
Project-URL: Repository, https://github.com/mbailey/voicemode
|
@@ -261,9 +261,12 @@ claude mcp add --scope user voice-mode uvx voice-mode
|
|
261
261
|
# Using Claude Code with Nix (NixOS)
|
262
262
|
claude mcp add voice-mode nix run github:mbailey/voicemode
|
263
263
|
|
264
|
-
# Using UV
|
264
|
+
# Using UV (recommended)
|
265
265
|
uvx voice-mode
|
266
266
|
|
267
|
+
# For cleanest experience with UV (no deprecation warnings):
|
268
|
+
UV_PYTHON=python3.13 uvx voice-mode
|
269
|
+
|
267
270
|
# Using pip
|
268
271
|
pip install voice-mode
|
269
272
|
|
@@ -183,9 +183,12 @@ claude mcp add --scope user voice-mode uvx voice-mode
|
|
183
183
|
# Using Claude Code with Nix (NixOS)
|
184
184
|
claude mcp add voice-mode nix run github:mbailey/voicemode
|
185
185
|
|
186
|
-
# Using UV
|
186
|
+
# Using UV (recommended)
|
187
187
|
uvx voice-mode
|
188
188
|
|
189
|
+
# For cleanest experience with UV (no deprecation warnings):
|
190
|
+
UV_PYTHON=python3.13 uvx voice-mode
|
191
|
+
|
189
192
|
# Using pip
|
190
193
|
pip install voice-mode
|
191
194
|
|
@@ -5,6 +5,8 @@ import asyncio
|
|
5
5
|
import sys
|
6
6
|
import os
|
7
7
|
import warnings
|
8
|
+
import subprocess
|
9
|
+
import shutil
|
8
10
|
import click
|
9
11
|
|
10
12
|
|
@@ -30,10 +32,12 @@ if not os.environ.get('VOICEMODE_DEBUG', '').lower() in ('true', '1', 'yes'):
|
|
30
32
|
@click.version_option()
|
31
33
|
@click.help_option('-h', '--help', help='Show this message and exit')
|
32
34
|
@click.option('--debug', is_flag=True, help='Enable debug mode and show all warnings')
|
35
|
+
@click.option('--tools-enabled', help='Comma-separated list of tools to enable (whitelist)')
|
36
|
+
@click.option('--tools-disabled', help='Comma-separated list of tools to disable (blacklist)')
|
33
37
|
@click.pass_context
|
34
|
-
def voice_mode_main_cli(ctx, debug):
|
38
|
+
def voice_mode_main_cli(ctx, debug, tools_enabled, tools_disabled):
|
35
39
|
"""Voice Mode - MCP server and service management.
|
36
|
-
|
40
|
+
|
37
41
|
Without arguments, starts the MCP server.
|
38
42
|
With subcommands, executes service management operations.
|
39
43
|
"""
|
@@ -44,7 +48,13 @@ def voice_mode_main_cli(ctx, debug):
|
|
44
48
|
# Re-enable INFO logging
|
45
49
|
import logging
|
46
50
|
logging.getLogger("voice-mode").setLevel(logging.INFO)
|
47
|
-
|
51
|
+
|
52
|
+
# Set environment variables from CLI args
|
53
|
+
if tools_enabled:
|
54
|
+
os.environ['VOICEMODE_TOOLS_ENABLED'] = tools_enabled
|
55
|
+
if tools_disabled:
|
56
|
+
os.environ['VOICEMODE_TOOLS_DISABLED'] = tools_disabled
|
57
|
+
|
48
58
|
if ctx.invoked_subcommand is None:
|
49
59
|
# No subcommand - run MCP server
|
50
60
|
# Note: warnings are already suppressed at module level unless debug is enabled
|
@@ -1277,6 +1287,72 @@ def config_set(key, value):
|
|
1277
1287
|
click.echo(result)
|
1278
1288
|
|
1279
1289
|
|
1290
|
+
@config.command("edit")
|
1291
|
+
@click.help_option('-h', '--help')
|
1292
|
+
@click.option('--editor', help='Editor to use (overrides $EDITOR)')
|
1293
|
+
def config_edit(editor):
|
1294
|
+
"""Open the configuration file in your default editor.
|
1295
|
+
|
1296
|
+
Opens ~/.voicemode/voicemode.env in your configured editor.
|
1297
|
+
Uses $EDITOR environment variable by default, or you can specify with --editor.
|
1298
|
+
|
1299
|
+
Examples:
|
1300
|
+
voicemode config edit # Use $EDITOR
|
1301
|
+
voicemode config edit --editor vim
|
1302
|
+
voicemode config edit --editor "code --wait"
|
1303
|
+
"""
|
1304
|
+
from pathlib import Path
|
1305
|
+
|
1306
|
+
# Find the config file
|
1307
|
+
config_path = Path.home() / ".voicemode" / "voicemode.env"
|
1308
|
+
|
1309
|
+
# Create default config if it doesn't exist
|
1310
|
+
if not config_path.exists():
|
1311
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
1312
|
+
from voice_mode.config import load_voicemode_env
|
1313
|
+
# This will create the default config
|
1314
|
+
load_voicemode_env()
|
1315
|
+
|
1316
|
+
# Determine which editor to use
|
1317
|
+
if editor:
|
1318
|
+
editor_cmd = editor
|
1319
|
+
else:
|
1320
|
+
# Try environment variables in order of preference
|
1321
|
+
editor_cmd = (
|
1322
|
+
os.environ.get('EDITOR') or
|
1323
|
+
os.environ.get('VISUAL') or
|
1324
|
+
shutil.which('nano') or
|
1325
|
+
shutil.which('vim') or
|
1326
|
+
shutil.which('vi')
|
1327
|
+
)
|
1328
|
+
|
1329
|
+
if not editor_cmd:
|
1330
|
+
click.echo("❌ No editor found. Please set $EDITOR or use --editor")
|
1331
|
+
click.echo(" Example: export EDITOR=vim")
|
1332
|
+
click.echo(" Or use: voicemode config edit --editor vim")
|
1333
|
+
return
|
1334
|
+
|
1335
|
+
# Handle complex editor commands (e.g., "code --wait")
|
1336
|
+
if ' ' in editor_cmd:
|
1337
|
+
import shlex
|
1338
|
+
cmd_parts = shlex.split(editor_cmd)
|
1339
|
+
cmd = cmd_parts + [str(config_path)]
|
1340
|
+
else:
|
1341
|
+
cmd = [editor_cmd, str(config_path)]
|
1342
|
+
|
1343
|
+
# Open the editor
|
1344
|
+
try:
|
1345
|
+
click.echo(f"Opening {config_path} in {editor_cmd}...")
|
1346
|
+
subprocess.run(cmd, check=True)
|
1347
|
+
click.echo("✅ Configuration file edited successfully")
|
1348
|
+
click.echo("\nChanges will take effect when voicemode is restarted.")
|
1349
|
+
except subprocess.CalledProcessError:
|
1350
|
+
click.echo(f"❌ Editor exited with an error")
|
1351
|
+
except FileNotFoundError:
|
1352
|
+
click.echo(f"❌ Editor not found: {editor_cmd}")
|
1353
|
+
click.echo(" Please check that the editor is installed and in your PATH")
|
1354
|
+
|
1355
|
+
|
1280
1356
|
# Diagnostics group
|
1281
1357
|
@voice_mode_main_cli.group()
|
1282
1358
|
@click.help_option('-h', '--help', help='Show this message and exit')
|
@@ -6,12 +6,6 @@ import asyncio
|
|
6
6
|
from pathlib import Path
|
7
7
|
from typing import Optional
|
8
8
|
|
9
|
-
from voice_mode.tools.transcription import (
|
10
|
-
transcribe_audio,
|
11
|
-
TranscriptionBackend,
|
12
|
-
OutputFormat
|
13
|
-
)
|
14
|
-
|
15
9
|
|
16
10
|
@click.group()
|
17
11
|
def transcribe():
|
@@ -61,6 +55,13 @@ def audio_command(
|
|
61
55
|
voice-mode transcribe audio spanish.mp3 --language es --backend whisperx
|
62
56
|
"""
|
63
57
|
async def run():
|
58
|
+
# Import here to avoid loading tools at module level
|
59
|
+
from voice_mode.tools.transcription import (
|
60
|
+
transcribe_audio,
|
61
|
+
TranscriptionBackend,
|
62
|
+
OutputFormat
|
63
|
+
)
|
64
|
+
|
64
65
|
# Perform transcription
|
65
66
|
result = await transcribe_audio(
|
66
67
|
audio_file=audio_file,
|
@@ -253,7 +253,7 @@ PREFER_LOCAL = os.getenv("VOICEMODE_PREFER_LOCAL", "true").lower() in ("true", "
|
|
253
253
|
ALWAYS_TRY_LOCAL = os.getenv("VOICEMODE_ALWAYS_TRY_LOCAL", "true").lower() in ("true", "1", "yes", "on")
|
254
254
|
|
255
255
|
# Use simple failover without health checks
|
256
|
-
|
256
|
+
# Simple failover is now the only mode - configuration removed
|
257
257
|
|
258
258
|
# Auto-start configuration
|
259
259
|
AUTO_START_KOKORO = os.getenv("VOICEMODE_AUTO_START_KOKORO", "").lower() in ("true", "1", "yes", "on")
|
@@ -189,6 +189,9 @@ class ConversationLogger:
|
|
189
189
|
"timing": kwargs.get("timing"),
|
190
190
|
"silence_detection": kwargs.get("silence_detection"),
|
191
191
|
"error": kwargs.get("error"),
|
192
|
+
# Fallback information
|
193
|
+
"is_fallback": kwargs.get("is_fallback"),
|
194
|
+
"fallback_reason": kwargs.get("fallback_reason"),
|
192
195
|
# Timing metrics
|
193
196
|
"transcription_time": kwargs.get("transcription_time"),
|
194
197
|
"total_turnaround_time": kwargs.get("total_turnaround_time"),
|
@@ -205,6 +208,9 @@ class ConversationLogger:
|
|
205
208
|
"provider": kwargs.get("provider"),
|
206
209
|
"provider_url": kwargs.get("provider_url"),
|
207
210
|
"provider_type": kwargs.get("provider_type"),
|
211
|
+
# Fallback information
|
212
|
+
"is_fallback": kwargs.get("is_fallback"),
|
213
|
+
"fallback_reason": kwargs.get("fallback_reason"),
|
208
214
|
"audio_format": kwargs.get("audio_format"),
|
209
215
|
"timing": kwargs.get("timing"),
|
210
216
|
"transport": kwargs.get("transport"),
|
@@ -18,6 +18,7 @@ from typing import Optional
|
|
18
18
|
import numpy as np
|
19
19
|
from pydub import AudioSegment
|
20
20
|
from openai import AsyncOpenAI
|
21
|
+
from .provider_discovery import is_local_provider
|
21
22
|
import httpx
|
22
23
|
|
23
24
|
from .config import SAMPLE_RATE
|
@@ -135,16 +136,22 @@ def get_openai_clients(api_key: str, stt_base_url: Optional[str] = None, tts_bas
|
|
135
136
|
'limits': httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
136
137
|
}
|
137
138
|
|
139
|
+
# Disable retries for local endpoints - they either work or don't
|
140
|
+
stt_max_retries = 0 if is_local_provider(stt_base_url) else 2
|
141
|
+
tts_max_retries = 0 if is_local_provider(tts_base_url) else 2
|
142
|
+
|
138
143
|
return {
|
139
144
|
'stt': AsyncOpenAI(
|
140
145
|
api_key=api_key,
|
141
146
|
base_url=stt_base_url,
|
142
|
-
http_client=httpx.AsyncClient(**http_client_config)
|
147
|
+
http_client=httpx.AsyncClient(**http_client_config),
|
148
|
+
max_retries=stt_max_retries
|
143
149
|
),
|
144
150
|
'tts': AsyncOpenAI(
|
145
151
|
api_key=api_key,
|
146
152
|
base_url=tts_base_url,
|
147
|
-
http_client=httpx.AsyncClient(**http_client_config)
|
153
|
+
http_client=httpx.AsyncClient(**http_client_config),
|
154
|
+
max_retries=tts_max_retries
|
148
155
|
)
|
149
156
|
}
|
150
157
|
|
@@ -26,6 +26,8 @@ logger = logging.getLogger("voice-mode")
|
|
26
26
|
|
27
27
|
def detect_provider_type(base_url: str) -> str:
|
28
28
|
"""Detect provider type from base URL."""
|
29
|
+
if not base_url:
|
30
|
+
return "unknown"
|
29
31
|
if "openai.com" in base_url:
|
30
32
|
return "openai"
|
31
33
|
elif ":8880" in base_url:
|
@@ -47,6 +49,8 @@ def detect_provider_type(base_url: str) -> str:
|
|
47
49
|
|
48
50
|
def is_local_provider(base_url: str) -> bool:
|
49
51
|
"""Check if a provider URL is for a local service."""
|
52
|
+
if not base_url:
|
53
|
+
return False
|
50
54
|
provider_type = detect_provider_type(base_url)
|
51
55
|
return provider_type in ["kokoro", "whisper", "local"] or \
|
52
56
|
"127.0.0.1" in base_url or \
|
@@ -57,13 +61,11 @@ def is_local_provider(base_url: str) -> bool:
|
|
57
61
|
class EndpointInfo:
|
58
62
|
"""Information about a discovered endpoint."""
|
59
63
|
base_url: str
|
60
|
-
healthy: bool
|
61
64
|
models: List[str]
|
62
65
|
voices: List[str] # Only for TTS
|
63
|
-
last_health_check: str # ISO format timestamp
|
64
|
-
response_time_ms: Optional[float] = None
|
65
|
-
error: Optional[str] = None
|
66
66
|
provider_type: Optional[str] = None # e.g., "openai", "kokoro", "whisper"
|
67
|
+
last_check: Optional[str] = None # ISO format timestamp of last attempt
|
68
|
+
last_error: Optional[str] = None # Last error if any
|
67
69
|
|
68
70
|
|
69
71
|
class ProviderRegistry:
|
@@ -78,44 +80,38 @@ class ProviderRegistry:
|
|
78
80
|
self._initialized = False
|
79
81
|
|
80
82
|
async def initialize(self):
|
81
|
-
"""Initialize the registry
|
83
|
+
"""Initialize the registry with configured endpoints."""
|
82
84
|
if self._initialized:
|
83
85
|
return
|
84
|
-
|
86
|
+
|
85
87
|
async with self._discovery_lock:
|
86
88
|
if self._initialized: # Double-check after acquiring lock
|
87
89
|
return
|
88
|
-
|
89
|
-
logger.info("Initializing provider registry
|
90
|
-
|
91
|
-
# Initialize TTS endpoints
|
90
|
+
|
91
|
+
logger.info("Initializing provider registry...")
|
92
|
+
|
93
|
+
# Initialize TTS endpoints
|
92
94
|
for url in TTS_BASE_URLS:
|
93
95
|
provider_type = detect_provider_type(url)
|
94
96
|
self.registry["tts"][url] = EndpointInfo(
|
95
97
|
base_url=url,
|
96
|
-
healthy=True,
|
97
98
|
models=["gpt4o-mini-tts", "tts-1", "tts-1-hd"] if provider_type == "openai" else ["tts-1"],
|
98
99
|
voices=["alloy", "echo", "fable", "nova", "onyx", "shimmer"] if provider_type == "openai" else ["af_alloy", "af_aoede", "af_bella", "af_heart", "af_jadzia", "af_jessica", "af_kore", "af_nicole", "af_nova", "af_river", "af_sarah", "af_sky", "af_v0", "af_v0bella", "af_v0irulan", "af_v0nicole", "af_v0sarah", "af_v0sky", "am_adam", "am_echo", "am_eric", "am_fenrir", "am_liam", "am_michael", "am_onyx", "am_puck", "am_santa", "am_v0adam", "am_v0gurney", "am_v0michael", "bf_alice", "bf_emma", "bf_lily", "bf_v0emma", "bf_v0isabella", "bm_daniel", "bm_fable", "bm_george", "bm_lewis", "bm_v0george", "bm_v0lewis", "ef_dora", "em_alex", "em_santa", "ff_siwis", "hf_alpha", "hf_beta", "hm_omega", "hm_psi", "if_sara", "im_nicola", "jf_alpha", "jf_gongitsune", "jf_nezumi", "jf_tebukuro", "jm_kumo", "pf_dora", "pm_alex", "pm_santa", "zf_xiaobei", "zf_xiaoni", "zf_xiaoxiao", "zf_xiaoyi", "zm_yunjian", "zm_yunxi", "zm_yunxia", "zm_yunyang"],
|
99
|
-
last_health_check=datetime.now(timezone.utc).isoformat(),
|
100
|
-
response_time_ms=None,
|
101
100
|
provider_type=provider_type
|
102
101
|
)
|
103
102
|
|
104
|
-
# Initialize STT endpoints
|
103
|
+
# Initialize STT endpoints
|
105
104
|
for url in STT_BASE_URLS:
|
106
105
|
provider_type = detect_provider_type(url)
|
107
106
|
self.registry["stt"][url] = EndpointInfo(
|
108
107
|
base_url=url,
|
109
|
-
healthy=True,
|
110
108
|
models=["whisper-1"],
|
111
|
-
voices=[],
|
112
|
-
last_health_check=datetime.now(timezone.utc).isoformat(),
|
113
|
-
response_time_ms=None,
|
109
|
+
voices=[], # STT doesn't have voices
|
114
110
|
provider_type=provider_type
|
115
111
|
)
|
116
|
-
|
112
|
+
|
117
113
|
self._initialized = True
|
118
|
-
logger.info(f"Provider registry initialized with {len(self.registry['tts'])} TTS and {len(self.registry['stt'])} STT endpoints
|
114
|
+
logger.info(f"Provider registry initialized with {len(self.registry['tts'])} TTS and {len(self.registry['stt'])} STT endpoints")
|
119
115
|
|
120
116
|
async def _discover_endpoints(self, service_type: str, base_urls: List[str]):
|
121
117
|
"""Discover all endpoints for a service type."""
|
@@ -131,12 +127,11 @@ class ProviderRegistry:
|
|
131
127
|
logger.error(f"Failed to discover {service_type} endpoint {url}: {result}")
|
132
128
|
self.registry[service_type][url] = EndpointInfo(
|
133
129
|
base_url=url,
|
134
|
-
healthy=False,
|
135
130
|
models=[],
|
136
131
|
voices=[],
|
137
|
-
|
138
|
-
|
139
|
-
|
132
|
+
provider_type=detect_provider_type(url),
|
133
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
134
|
+
last_error=str(result)
|
140
135
|
)
|
141
136
|
|
142
137
|
async def _discover_endpoint(self, service_type: str, base_url: str) -> None:
|
@@ -201,12 +196,11 @@ class ProviderRegistry:
|
|
201
196
|
# Store endpoint info
|
202
197
|
self.registry[service_type][base_url] = EndpointInfo(
|
203
198
|
base_url=base_url,
|
204
|
-
healthy=True,
|
205
199
|
models=models,
|
206
200
|
voices=voices,
|
207
|
-
|
208
|
-
|
209
|
-
|
201
|
+
provider_type=detect_provider_type(base_url),
|
202
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
203
|
+
last_error=None
|
210
204
|
)
|
211
205
|
|
212
206
|
logger.info(f"Successfully discovered {service_type} endpoint {base_url} with {len(models)} models and {len(voices)} voices")
|
@@ -215,12 +209,11 @@ class ProviderRegistry:
|
|
215
209
|
logger.warning(f"Endpoint {base_url} discovery failed: {e}")
|
216
210
|
self.registry[service_type][base_url] = EndpointInfo(
|
217
211
|
base_url=base_url,
|
218
|
-
healthy=False,
|
219
212
|
models=[],
|
220
213
|
voices=[],
|
221
|
-
|
222
|
-
|
223
|
-
|
214
|
+
provider_type=detect_provider_type(base_url),
|
215
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
216
|
+
last_error=str(e)
|
224
217
|
)
|
225
218
|
|
226
219
|
async def _discover_voices(self, base_url: str, client: AsyncOpenAI) -> List[str]:
|
@@ -247,41 +240,35 @@ class ProviderRegistry:
|
|
247
240
|
# The system will use configured defaults instead
|
248
241
|
return []
|
249
242
|
|
250
|
-
async def check_health(self, service_type: str, base_url: str) -> bool:
|
251
|
-
"""Check the health of a specific endpoint and update registry."""
|
252
|
-
logger.debug(f"Health check for {service_type} endpoint: {base_url}")
|
253
|
-
|
254
|
-
# Re-discover the endpoint
|
255
|
-
await self._discover_endpoint(service_type, base_url)
|
256
|
-
|
257
|
-
# Return health status
|
258
|
-
endpoint_info = self.registry[service_type].get(base_url)
|
259
|
-
return endpoint_info.healthy if endpoint_info else False
|
260
243
|
|
261
|
-
def
|
262
|
-
"""Get all
|
244
|
+
def get_endpoints(self, service_type: str) -> List[EndpointInfo]:
|
245
|
+
"""Get all endpoints for a service type in priority order."""
|
263
246
|
endpoints = []
|
264
|
-
|
247
|
+
|
265
248
|
# Return endpoints in the order they were configured
|
266
249
|
base_urls = TTS_BASE_URLS if service_type == "tts" else STT_BASE_URLS
|
267
|
-
|
250
|
+
|
268
251
|
for url in base_urls:
|
269
252
|
info = self.registry[service_type].get(url)
|
270
|
-
if info
|
253
|
+
if info:
|
271
254
|
endpoints.append(info)
|
272
|
-
|
255
|
+
|
273
256
|
return endpoints
|
257
|
+
|
258
|
+
def get_healthy_endpoints(self, service_type: str) -> List[EndpointInfo]:
|
259
|
+
"""Deprecated: Use get_endpoints instead. Returns all endpoints."""
|
260
|
+
return self.get_endpoints(service_type)
|
274
261
|
|
275
262
|
def find_endpoint_with_voice(self, voice: str) -> Optional[EndpointInfo]:
|
276
|
-
"""Find the first
|
277
|
-
for endpoint in self.
|
263
|
+
"""Find the first TTS endpoint that supports a specific voice."""
|
264
|
+
for endpoint in self.get_endpoints("tts"):
|
278
265
|
if voice in endpoint.voices:
|
279
266
|
return endpoint
|
280
267
|
return None
|
281
|
-
|
268
|
+
|
282
269
|
def find_endpoint_with_model(self, service_type: str, model: str) -> Optional[EndpointInfo]:
|
283
|
-
"""Find the first
|
284
|
-
for endpoint in self.
|
270
|
+
"""Find the first endpoint that supports a specific model."""
|
271
|
+
for endpoint in self.get_endpoints(service_type):
|
285
272
|
if model in endpoint.models:
|
286
273
|
return endpoint
|
287
274
|
return None
|
@@ -291,47 +278,36 @@ class ProviderRegistry:
|
|
291
278
|
return {
|
292
279
|
"tts": {
|
293
280
|
url: {
|
294
|
-
"healthy": info.healthy,
|
295
281
|
"models": info.models,
|
296
282
|
"voices": info.voices,
|
297
|
-
"
|
298
|
-
"last_check": info.
|
299
|
-
"
|
283
|
+
"provider_type": info.provider_type,
|
284
|
+
"last_check": info.last_check,
|
285
|
+
"last_error": info.last_error
|
300
286
|
}
|
301
287
|
for url, info in self.registry["tts"].items()
|
302
288
|
},
|
303
289
|
"stt": {
|
304
290
|
url: {
|
305
|
-
"healthy": info.healthy,
|
306
291
|
"models": info.models,
|
307
|
-
"
|
308
|
-
"last_check": info.
|
309
|
-
"
|
292
|
+
"provider_type": info.provider_type,
|
293
|
+
"last_check": info.last_check,
|
294
|
+
"last_error": info.last_error
|
310
295
|
}
|
311
296
|
for url, info in self.registry["stt"].items()
|
312
297
|
}
|
313
298
|
}
|
314
299
|
|
315
|
-
async def
|
316
|
-
"""
|
317
|
-
|
318
|
-
|
319
|
-
|
300
|
+
async def mark_failed(self, service_type: str, base_url: str, error: str):
|
301
|
+
"""Record that an endpoint failed.
|
302
|
+
|
303
|
+
This updates the last_error and last_check fields for diagnostics,
|
304
|
+
but doesn't prevent the endpoint from being tried again.
|
320
305
|
"""
|
321
306
|
if base_url in self.registry[service_type]:
|
322
|
-
#
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
# Update error and last check time for diagnostics, but keep healthy=True
|
327
|
-
self.registry[service_type][base_url].error = f"{error} (will retry)"
|
328
|
-
self.registry[service_type][base_url].last_health_check = datetime.now(timezone.utc).isoformat()
|
329
|
-
else:
|
330
|
-
# Normal behavior - mark as unhealthy
|
331
|
-
self.registry[service_type][base_url].healthy = False
|
332
|
-
self.registry[service_type][base_url].error = error
|
333
|
-
self.registry[service_type][base_url].last_health_check = datetime.now(timezone.utc).isoformat()
|
334
|
-
logger.warning(f"Marked {service_type} endpoint {base_url} as unhealthy: {error}")
|
307
|
+
# Update error and last check time for diagnostics
|
308
|
+
self.registry[service_type][base_url].last_error = error
|
309
|
+
self.registry[service_type][base_url].last_check = datetime.now(timezone.utc).isoformat()
|
310
|
+
logger.info(f"{service_type} endpoint {base_url} failed: {error}")
|
335
311
|
|
336
312
|
|
337
313
|
# Global registry instance
|