voice-mode 4.4.0__py3-none-any.whl → 4.5.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 +79 -3
- voice_mode/cli_commands/transcribe.py +7 -6
- voice_mode/config.py +1 -1
- voice_mode/conversation_logger.py +6 -0
- voice_mode/core.py +9 -2
- voice_mode/frontend/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/app-build-manifest.json +5 -5
- 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 +3 -3
- voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +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/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 +3 -3
- voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +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-d3ec7f6f14ea7396.js +1 -0
- voice_mode/frontend/.next/static/chunks/app/{page-ae0d14863ed895ea.js → page-471796963fb1a4bd.js} +1 -1
- voice_mode/frontend/.next/static/chunks/{main-app-836e76fc70b52220.js → main-app-78da5e437b6a2a9f.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 +26 -15
- voice_mode/provider_discovery.py +55 -79
- voice_mode/providers.py +61 -45
- voice_mode/simple_failover.py +41 -12
- voice_mode/tools/__init__.py +138 -30
- voice_mode/tools/converse.py +148 -337
- voice_mode/tools/diagnostics.py +2 -1
- voice_mode/tools/voice_registry.py +24 -28
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/METADATA +5 -2
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/RECORD +74 -74
- voice_mode/frontend/.next/static/chunks/app/layout-917e8410913fe899.js +0 -1
- /voice_mode/frontend/.next/static/{WhZriRkBKVNPSmCnOFRav → Ni4GIqyDdn0QehvmlLBZg}/_buildManifest.js +0 -0
- /voice_mode/frontend/.next/static/{WhZriRkBKVNPSmCnOFRav → Ni4GIqyDdn0QehvmlLBZg}/_ssgManifest.js +0 -0
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/WHEEL +0 -0
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/entry_points.txt +0 -0
voice_mode/simple_failover.py
CHANGED
@@ -8,11 +8,12 @@ Connection refused errors are instant, so there's no performance penalty.
|
|
8
8
|
import logging
|
9
9
|
from typing import Optional, Tuple, Dict, Any
|
10
10
|
from openai import AsyncOpenAI
|
11
|
+
from .provider_discovery import is_local_provider
|
11
12
|
|
12
13
|
from .config import TTS_BASE_URLS, STT_BASE_URLS, OPENAI_API_KEY
|
13
14
|
from .provider_discovery import detect_provider_type
|
14
15
|
|
15
|
-
logger = logging.getLogger("
|
16
|
+
logger = logging.getLogger("voicemode")
|
16
17
|
|
17
18
|
|
18
19
|
async def simple_tts_failover(
|
@@ -71,10 +72,13 @@ async def simple_tts_failover(
|
|
71
72
|
else:
|
72
73
|
selected_voice = voice # Use original voice for Kokoro
|
73
74
|
|
75
|
+
# Disable retries for local endpoints - they either work or don't
|
76
|
+
max_retries = 0 if is_local_provider(base_url) else 2
|
74
77
|
client = AsyncOpenAI(
|
75
78
|
api_key=api_key,
|
76
79
|
base_url=base_url,
|
77
|
-
timeout=30.0 # Reasonable timeout
|
80
|
+
timeout=30.0, # Reasonable timeout
|
81
|
+
max_retries=max_retries
|
78
82
|
)
|
79
83
|
|
80
84
|
# Create clients dict for text_to_speech
|
@@ -132,19 +136,31 @@ async def simple_stt_failover(
|
|
132
136
|
"""
|
133
137
|
last_error = None
|
134
138
|
|
139
|
+
# Log STT request details
|
140
|
+
logger.info("STT: Starting speech-to-text conversion")
|
141
|
+
logger.info(f" Available endpoints: {STT_BASE_URLS}")
|
142
|
+
|
135
143
|
# Try each STT endpoint in order
|
136
|
-
for base_url in STT_BASE_URLS:
|
144
|
+
for i, base_url in enumerate(STT_BASE_URLS):
|
137
145
|
try:
|
138
|
-
|
139
|
-
|
140
|
-
# Create client for this endpoint
|
146
|
+
# Detect provider type for logging
|
141
147
|
provider_type = detect_provider_type(base_url)
|
148
|
+
|
149
|
+
if i == 0:
|
150
|
+
logger.info(f"STT: Attempting primary endpoint: {base_url} ({provider_type})")
|
151
|
+
else:
|
152
|
+
logger.warning(f"STT: Primary failed, attempting fallback #{i}: {base_url} ({provider_type})")
|
153
|
+
|
154
|
+
# Create client for this endpoint
|
142
155
|
api_key = OPENAI_API_KEY if provider_type == "openai" else (OPENAI_API_KEY or "dummy-key-for-local")
|
143
156
|
|
157
|
+
# Disable retries for local endpoints - they either work or don't
|
158
|
+
max_retries = 0 if is_local_provider(base_url) else 2
|
144
159
|
client = AsyncOpenAI(
|
145
160
|
api_key=api_key,
|
146
161
|
base_url=base_url,
|
147
|
-
timeout=30.0
|
162
|
+
timeout=30.0,
|
163
|
+
max_retries=max_retries
|
148
164
|
)
|
149
165
|
|
150
166
|
# Try STT with this endpoint
|
@@ -155,17 +171,30 @@ async def simple_stt_failover(
|
|
155
171
|
)
|
156
172
|
|
157
173
|
text = transcription.strip() if isinstance(transcription, str) else transcription.text.strip()
|
158
|
-
|
174
|
+
|
159
175
|
if text:
|
160
|
-
logger.info(f"STT succeeded with {base_url}")
|
161
|
-
|
176
|
+
logger.info(f"✓ STT succeeded with {provider_type} at {base_url}")
|
177
|
+
logger.info(f" Transcribed: {text[:100]}{'...' if len(text) > 100 else ''}")
|
178
|
+
# Return both text and provider info for display
|
179
|
+
return {"text": text, "provider": provider_type, "endpoint": base_url}
|
180
|
+
else:
|
181
|
+
logger.warning(f"STT returned empty result from {base_url} ({provider_type})")
|
162
182
|
|
163
183
|
except Exception as e:
|
164
184
|
last_error = str(e)
|
165
|
-
|
185
|
+
provider_type = detect_provider_type(base_url)
|
186
|
+
|
187
|
+
# Log failure with appropriate level based on whether we have fallbacks
|
188
|
+
if i < len(STT_BASE_URLS) - 1:
|
189
|
+
logger.warning(f"STT failed for {base_url} ({provider_type}): {e}")
|
190
|
+
logger.info(" Will try next endpoint...")
|
191
|
+
else:
|
192
|
+
logger.error(f"STT failed for final endpoint {base_url} ({provider_type}): {e}")
|
193
|
+
|
166
194
|
# Continue to next endpoint
|
167
195
|
continue
|
168
196
|
|
169
197
|
# All endpoints failed
|
170
|
-
logger.error(f"All STT endpoints failed
|
198
|
+
logger.error(f"✗ All STT endpoints failed after {len(STT_BASE_URLS)} attempts")
|
199
|
+
logger.error(f" Last error: {last_error}")
|
171
200
|
return None
|
voice_mode/tools/__init__.py
CHANGED
@@ -9,42 +9,150 @@ logger = logging.getLogger("voice-mode")
|
|
9
9
|
# Get the directory containing this file
|
10
10
|
tools_dir = Path(__file__).parent
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
for tool_name in tool_list:
|
23
|
-
tool_file = tools_dir / f"{tool_name}.py"
|
24
|
-
if tool_file.exists():
|
25
|
-
logger.debug(f"Loading tool: {tool_name}")
|
26
|
-
importlib.import_module(f".{tool_name}", package=__name__)
|
27
|
-
else:
|
28
|
-
logger.warning(f"Tool module not found: {tool_name}.py")
|
29
|
-
else:
|
30
|
-
# Default behavior: load all tools
|
31
|
-
logger.info("Loading all available tools (set VOICEMODE_TOOLS to limit)")
|
32
|
-
|
33
|
-
# Import all Python files in this directory (except __init__.py)
|
12
|
+
def get_all_available_tools() -> set[str]:
|
13
|
+
"""
|
14
|
+
Get all available tool names from the filesystem.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
Set of tool module names (without .py extension)
|
18
|
+
"""
|
19
|
+
available_tools = set()
|
20
|
+
|
21
|
+
# Get tools from main directory
|
34
22
|
for file in tools_dir.glob("*.py"):
|
35
23
|
if file.name != "__init__.py" and not file.name.startswith("_"):
|
36
|
-
|
37
|
-
logger.debug(f"Loading tool: {module_name}")
|
38
|
-
importlib.import_module(f".{module_name}", package=__name__)
|
24
|
+
available_tools.add(file.stem)
|
39
25
|
|
40
|
-
#
|
26
|
+
# Get tools from services subdirectories
|
41
27
|
services_dir = tools_dir / "services"
|
42
28
|
if services_dir.exists():
|
43
29
|
for service_dir in services_dir.iterdir():
|
44
30
|
if service_dir.is_dir() and not service_dir.name.startswith("_"):
|
45
|
-
# Import all Python files in each service directory
|
46
31
|
for file in service_dir.glob("*.py"):
|
47
32
|
if file.name != "__init__.py" and not file.name.startswith("_") and file.name != "helpers.py":
|
48
|
-
|
49
|
-
|
50
|
-
|
33
|
+
# Use flattened naming: service_toolname
|
34
|
+
tool_name = f"{service_dir.name}_{file.stem}"
|
35
|
+
available_tools.add(tool_name)
|
36
|
+
|
37
|
+
return available_tools
|
38
|
+
|
39
|
+
def parse_tool_list(tool_string: str) -> set[str]:
|
40
|
+
"""
|
41
|
+
Parse comma-separated tool list into a set of tool names.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
tool_string: Comma-separated string of tool names
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Set of trimmed tool names
|
48
|
+
"""
|
49
|
+
if not tool_string:
|
50
|
+
return set()
|
51
|
+
return {t.strip() for t in tool_string.split(",") if t.strip()}
|
52
|
+
|
53
|
+
def determine_tools_to_load() -> tuple[set[str], str]:
|
54
|
+
"""
|
55
|
+
Determine which tools should be loaded based on environment variables.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Tuple of (tools_to_load, mode_description)
|
59
|
+
"""
|
60
|
+
# Check for new environment variables
|
61
|
+
enabled_tools = os.environ.get("VOICEMODE_TOOLS_ENABLED", "").strip()
|
62
|
+
disabled_tools = os.environ.get("VOICEMODE_TOOLS_DISABLED", "").strip()
|
63
|
+
|
64
|
+
# Check for legacy variable
|
65
|
+
legacy_tools = os.environ.get("VOICEMODE_TOOLS", "").strip()
|
66
|
+
|
67
|
+
# Get all available tools
|
68
|
+
all_tools = get_all_available_tools()
|
69
|
+
|
70
|
+
# Determine which tools to load
|
71
|
+
if enabled_tools:
|
72
|
+
# Whitelist mode - only load specified tools
|
73
|
+
requested = parse_tool_list(enabled_tools)
|
74
|
+
tools_to_load = requested & all_tools # Only load tools that exist
|
75
|
+
invalid = requested - all_tools
|
76
|
+
|
77
|
+
if invalid:
|
78
|
+
logger.warning(f"Requested tools not found: {', '.join(sorted(invalid))}")
|
79
|
+
|
80
|
+
return tools_to_load, f"whitelist mode ({len(tools_to_load)} tools)"
|
81
|
+
|
82
|
+
elif disabled_tools:
|
83
|
+
# Blacklist mode - load all except specified
|
84
|
+
excluded = parse_tool_list(disabled_tools)
|
85
|
+
tools_to_load = all_tools - excluded
|
86
|
+
|
87
|
+
# Log if any excluded tools don't exist (informational)
|
88
|
+
nonexistent = excluded - all_tools
|
89
|
+
if nonexistent:
|
90
|
+
logger.debug(f"Excluded tools not found (ignoring): {', '.join(sorted(nonexistent))}")
|
91
|
+
|
92
|
+
return tools_to_load, f"blacklist mode (excluding {len(excluded & all_tools)} tools)"
|
93
|
+
|
94
|
+
elif legacy_tools:
|
95
|
+
# Legacy support with deprecation warning
|
96
|
+
logger.warning(
|
97
|
+
"VOICEMODE_TOOLS is deprecated and will be removed in v5.0. "
|
98
|
+
"Please use VOICEMODE_TOOLS_ENABLED or VOICEMODE_TOOLS_DISABLED instead."
|
99
|
+
)
|
100
|
+
requested = parse_tool_list(legacy_tools)
|
101
|
+
tools_to_load = requested & all_tools
|
102
|
+
invalid = requested - all_tools
|
103
|
+
|
104
|
+
if invalid:
|
105
|
+
logger.warning(f"Requested tools not found: {', '.join(sorted(invalid))}")
|
106
|
+
|
107
|
+
return tools_to_load, f"legacy mode ({len(tools_to_load)} tools)"
|
108
|
+
|
109
|
+
else:
|
110
|
+
# Default - load everything
|
111
|
+
return all_tools, "default mode (all tools)"
|
112
|
+
|
113
|
+
def load_tool(tool_name: str) -> bool:
|
114
|
+
"""
|
115
|
+
Load a single tool by name.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
tool_name: Name of the tool to load
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
True if successfully loaded, False otherwise
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
# Check if it's a service tool (contains underscore)
|
125
|
+
if "_" in tool_name:
|
126
|
+
parts = tool_name.split("_", 1)
|
127
|
+
if len(parts) == 2:
|
128
|
+
service_name, tool_file = parts
|
129
|
+
module_path = f".services.{service_name}.{tool_file}"
|
130
|
+
logger.debug(f"Loading service tool: {tool_name}")
|
131
|
+
importlib.import_module(module_path, package=__name__)
|
132
|
+
return True
|
133
|
+
|
134
|
+
# Try as a regular tool
|
135
|
+
tool_file = tools_dir / f"{tool_name}.py"
|
136
|
+
if tool_file.exists():
|
137
|
+
logger.debug(f"Loading tool: {tool_name}")
|
138
|
+
importlib.import_module(f".{tool_name}", package=__name__)
|
139
|
+
return True
|
140
|
+
|
141
|
+
logger.warning(f"Tool not found: {tool_name}")
|
142
|
+
return False
|
143
|
+
|
144
|
+
except ImportError as e:
|
145
|
+
logger.error(f"Failed to import tool {tool_name}: {e}")
|
146
|
+
return False
|
147
|
+
|
148
|
+
# Main loading logic
|
149
|
+
tools_to_load, mode = determine_tools_to_load()
|
150
|
+
|
151
|
+
if tools_to_load:
|
152
|
+
logger.info(f"Tool loading: {mode} - loading {len(tools_to_load)} tools")
|
153
|
+
|
154
|
+
# Sort for consistent loading order
|
155
|
+
for tool_name in sorted(tools_to_load):
|
156
|
+
load_tool(tool_name)
|
157
|
+
else:
|
158
|
+
logger.warning("No tools to load based on current configuration")
|