voice-mode 4.3.2__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.
Files changed (80) hide show
  1. voice_mode/__version__.py +1 -1
  2. voice_mode/cli.py +82 -5
  3. voice_mode/cli_commands/claude.py +6 -199
  4. voice_mode/cli_commands/hook.py +13 -16
  5. voice_mode/cli_commands/transcribe.py +7 -6
  6. voice_mode/config.py +1 -1
  7. voice_mode/conversation_logger.py +6 -0
  8. voice_mode/core.py +9 -2
  9. voice_mode/frontend/.next/BUILD_ID +1 -1
  10. voice_mode/frontend/.next/app-build-manifest.json +5 -5
  11. voice_mode/frontend/.next/build-manifest.json +3 -3
  12. voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
  13. voice_mode/frontend/.next/next-server.js.nft.json +1 -1
  14. voice_mode/frontend/.next/prerender-manifest.json +1 -1
  15. voice_mode/frontend/.next/required-server-files.json +1 -1
  16. voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
  17. voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. voice_mode/frontend/.next/server/app/_not-found.html +1 -1
  19. voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
  20. voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
  21. voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
  22. voice_mode/frontend/.next/server/app/index.html +1 -1
  23. voice_mode/frontend/.next/server/app/index.rsc +2 -2
  24. voice_mode/frontend/.next/server/app/page.js +2 -2
  25. voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
  26. voice_mode/frontend/.next/server/chunks/994.js +2 -2
  27. voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
  28. voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
  29. voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
  30. voice_mode/frontend/.next/server/pages/404.html +1 -1
  31. voice_mode/frontend/.next/server/pages/500.html +1 -1
  32. voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
  33. voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
  34. voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
  35. voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
  36. voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
  37. voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
  38. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  39. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  40. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
  41. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  42. voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
  43. voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
  44. voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
  45. voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
  46. voice_mode/frontend/.next/standalone/.next/server/app/page.js +2 -2
  47. voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  48. voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +2 -2
  49. voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  50. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
  51. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
  52. voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
  53. voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
  54. voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  55. voice_mode/frontend/.next/standalone/server.js +1 -1
  56. voice_mode/frontend/.next/static/chunks/app/layout-d3ec7f6f14ea7396.js +1 -0
  57. voice_mode/frontend/.next/static/chunks/app/{page-728ce55710c9bd80.js → page-471796963fb1a4bd.js} +1 -1
  58. voice_mode/frontend/.next/static/chunks/{main-app-991c8c54319817e0.js → main-app-78da5e437b6a2a9f.js} +1 -1
  59. voice_mode/frontend/.next/trace +43 -43
  60. voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
  61. voice_mode/frontend/.next/types/app/layout.ts +1 -1
  62. voice_mode/frontend/.next/types/app/page.ts +1 -1
  63. voice_mode/frontend/package-lock.json +26 -15
  64. voice_mode/provider_discovery.py +55 -79
  65. voice_mode/providers.py +61 -45
  66. voice_mode/simple_failover.py +41 -12
  67. voice_mode/statistics_tracking.py +42 -0
  68. voice_mode/tools/__init__.py +150 -16
  69. voice_mode/tools/converse.py +149 -338
  70. voice_mode/tools/diagnostics.py +2 -1
  71. voice_mode/tools/statistics.py +0 -33
  72. voice_mode/tools/voice_registry.py +24 -28
  73. {voice_mode-4.3.2.dist-info → voice_mode-4.5.0.dist-info}/METADATA +36 -41
  74. {voice_mode-4.3.2.dist-info → voice_mode-4.5.0.dist-info}/RECORD +78 -78
  75. voice_mode/frontend/.next/static/chunks/app/layout-d89915a94cb8f604.js +0 -1
  76. voice_mode/tools/claude_thinking.py +0 -285
  77. /voice_mode/frontend/.next/static/{-9MkXl-xGB2BLmGEAZVmg → Ni4GIqyDdn0QehvmlLBZg}/_buildManifest.js +0 -0
  78. /voice_mode/frontend/.next/static/{-9MkXl-xGB2BLmGEAZVmg → Ni4GIqyDdn0QehvmlLBZg}/_ssgManifest.js +0 -0
  79. {voice_mode-4.3.2.dist-info → voice_mode-4.5.0.dist-info}/WHEEL +0 -0
  80. {voice_mode-4.3.2.dist-info → voice_mode-4.5.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__ = "4.3.2"
3
+ __version__ = "4.5.0"
voice_mode/cli.py CHANGED
@@ -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')
@@ -1394,8 +1470,9 @@ transcribe_audio_cmd = transcribe_cmd.transcribe.commands['audio']
1394
1470
  transcribe_audio_cmd.name = 'transcribe'
1395
1471
  audio.add_command(transcribe_audio_cmd)
1396
1472
 
1397
- # Add hook command under claude group
1398
- claude.claude_group.add_command(hook_cmd.hook)
1473
+ # Add hooks command under claude group
1474
+ from voice_mode.cli_commands.hook import hooks
1475
+ claude.claude_group.add_command(hooks)
1399
1476
 
1400
1477
  # Add pronounce under config group
1401
1478
  config.add_command(pronounce_commands.pronounce_group)
@@ -1,208 +1,15 @@
1
- """CLI commands for Claude Code message extraction."""
1
+ """CLI commands for Claude-related functionality."""
2
2
 
3
3
  import click
4
- import json
5
- import os
6
- import logging
7
- from pathlib import Path
8
- from typing import Optional, List
9
- from datetime import datetime
10
-
11
- from voice_mode.tools.claude_thinking import (
12
- find_claude_log_file,
13
- extract_messages_from_log,
14
- extract_thinking_from_messages
15
- )
16
-
17
- logger = logging.getLogger("voice-mode")
18
4
 
19
5
 
20
6
  @click.group(name='claude')
21
7
  def claude_group():
22
- """Extract messages from Claude Code conversation logs."""
8
+ """Claude-related commands."""
23
9
  pass
24
10
 
25
11
 
26
- @claude_group.command(name='messages')
27
- @click.option('--last-n', '-n', default=2, type=int,
28
- help='Number of recent messages to extract (default: 2)')
29
- @click.option('--type', '-t', 'message_types', multiple=True,
30
- type=click.Choice(['user', 'assistant']),
31
- help='Filter by message type (can specify multiple)')
32
- @click.option('--format', '-f',
33
- type=click.Choice(['full', 'text', 'thinking']),
34
- default='full',
35
- help='Output format: full (complete), text (text only), thinking (thinking only)')
36
- @click.option('--working-dir', '-d', type=click.Path(exists=True),
37
- help='Working directory to find logs for (defaults to CWD)')
38
- @click.option('--output', '-o', type=click.Path(),
39
- help='Save output to file')
40
- @click.option('--json', 'as_json', is_flag=True,
41
- help='Output as JSON (overrides format)')
42
- def messages_command(last_n: int, message_types: tuple, format: str,
43
- working_dir: Optional[str], output: Optional[str],
44
- as_json: bool):
45
- """
46
- Extract recent messages from Claude Code logs.
47
-
48
- Examples:
49
-
50
- voicemode claude messages
51
-
52
- voicemode claude messages -n 5 --format thinking
53
-
54
- voicemode claude messages --type assistant --format text
55
-
56
- voicemode claude messages --json -o messages.json
57
- """
58
- # Find log file
59
- log_file = find_claude_log_file(working_dir)
60
- if not log_file:
61
- click.echo(f"Error: Could not find Claude Code logs for directory: {working_dir or os.getcwd()}", err=True)
62
- return
63
-
64
- # Extract messages
65
- message_type_list = list(message_types) if message_types else None
66
- messages = extract_messages_from_log(log_file, last_n, message_type_list)
67
-
68
- if not messages:
69
- click.echo("No messages found in recent logs", err=True)
70
- return
71
-
72
- # Format output
73
- if as_json:
74
- content = json.dumps(messages, indent=2)
75
- elif format == 'thinking':
76
- thinking_texts = extract_thinking_from_messages(messages)
77
- if not thinking_texts:
78
- click.echo("No thinking content found", err=True)
79
- return
80
- content = "\n\n=== Next Thinking ===\n\n".join(thinking_texts)
81
- elif format == 'text':
82
- result = []
83
- for msg in messages:
84
- content_text = []
85
- content_items = msg.get('content', [])
86
- logger.debug(f"Message has {len(content_items)} content items")
87
- for item in content_items:
88
- if isinstance(item, dict):
89
- item_type = item.get('type')
90
- logger.debug(f"Content item type: {item_type}")
91
- if item_type == 'text':
92
- text = item.get('text', '')
93
- if text:
94
- content_text.append(text)
95
- elif item_type == 'thinking':
96
- text = item.get('text', '')
97
- if text:
98
- content_text.append(f"[Thinking: {text}]")
99
- if content_text:
100
- result.append(f"{msg['type'].title()}: {' '.join(content_text)}")
101
- content = "\n\n".join(result)
102
- else: # full format
103
- # Format as human-readable
104
- result = []
105
- for i, msg in enumerate(messages, 1):
106
- result.append(f"=== Message {i} ===")
107
- result.append(f"Type: {msg['type']}")
108
- result.append(f"Timestamp: {msg.get('timestamp', 'Unknown')}")
109
- if msg.get('model'):
110
- result.append(f"Model: {msg['model']}")
111
-
112
- # Format content
113
- content_items = msg.get('content', [])
114
- if content_items:
115
- result.append("Content:")
116
- for item in content_items:
117
- if isinstance(item, dict):
118
- item_type = item.get('type', 'unknown')
119
- if item_type == 'text':
120
- result.append(f" [Text]: {item.get('text', '')}")
121
- elif item_type == 'thinking':
122
- result.append(f" [Thinking]: {item.get('text', '')}")
123
- elif item_type == 'tool_use':
124
- result.append(f" [Tool Use]: {item.get('name', '')}")
125
- elif item_type == 'tool_result':
126
- result.append(f" [Tool Result]: {item.get('content', '')[:100]}...")
127
- result.append("")
128
- content = "\n".join(result).strip()
129
-
130
- # Output
131
- if output:
132
- Path(output).write_text(content)
133
- click.echo(f"Output saved to {output}")
134
- else:
135
- # Debug: ensure content is printed
136
- logger.debug(f"About to output {len(content)} characters")
137
- click.echo(content)
138
- logger.debug("Output complete")
139
-
140
-
141
- @claude_group.command(name='thinking')
142
- @click.option('--last-n', '-n', default=1, type=int,
143
- help='Number of messages to search for thinking (default: 1)')
144
- @click.option('--working-dir', '-d', type=click.Path(exists=True),
145
- help='Working directory to find logs for (defaults to CWD)')
146
- def thinking_command(last_n: int, working_dir: Optional[str]):
147
- """
148
- Extract only thinking content from Claude Code logs.
149
-
150
- Convenience command equivalent to:
151
- voicemode claude messages --format thinking --type assistant
152
-
153
- Examples:
154
-
155
- voicemode claude thinking
156
-
157
- voicemode claude thinking -n 3
158
- """
159
- # Delegate to messages command with thinking format
160
- ctx = click.get_current_context()
161
- ctx.invoke(messages_command,
162
- last_n=last_n,
163
- message_types=('assistant',),
164
- format='thinking',
165
- working_dir=working_dir,
166
- output=None,
167
- as_json=False)
168
-
169
-
170
- @claude_group.command(name='check')
171
- def check_command():
172
- """
173
- Check if Claude Code context is available.
174
-
175
- Shows information about the Claude Code environment including:
176
- - Whether Claude Code logs are accessible
177
- - Current working directory
178
- - Log file location if found
179
- - Last update time
180
-
181
- Example:
182
- voicemode claude check
183
- """
184
- working_dir = os.getcwd()
185
- log_file = find_claude_log_file(working_dir)
186
-
187
- click.echo(f"Working Directory: {working_dir}")
188
- click.echo(f"Claude Logs Found: {'Yes' if log_file else 'No'}")
189
-
190
- if log_file:
191
- click.echo(f"Log File: {log_file}")
192
- click.echo(f"Log Size: {log_file.stat().st_size:,} bytes")
193
-
194
- mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
195
- now = datetime.now()
196
- age = now - mtime
197
-
198
- if age.total_seconds() < 60:
199
- click.echo(f"Last Updated: {int(age.total_seconds())} seconds ago")
200
- elif age.total_seconds() < 3600:
201
- click.echo(f"Last Updated: {int(age.total_seconds() / 60)} minutes ago")
202
- else:
203
- click.echo(f"Last Updated: {int(age.total_seconds() / 3600)} hours ago")
204
- else:
205
- project_dir = working_dir.replace('/', '-')
206
- expected_path = Path.home() / '.claude' / 'projects' / project_dir
207
- click.echo(f"Expected Log Location: {expected_path}")
208
- click.echo("Note: Logs are only created when using Claude Code (claude.ai/code)")
12
+ @click.group(name='hooks')
13
+ def hooks_group():
14
+ """Manage Voice Mode hooks and event handlers for Claude Code."""
15
+ pass
@@ -12,18 +12,18 @@ from typing import Optional
12
12
 
13
13
  @click.group()
14
14
  @click.help_option('-h', '--help', help='Show this message and exit')
15
- def hook():
15
+ def hooks():
16
16
  """Manage Voice Mode hooks and event handlers."""
17
17
  pass
18
18
 
19
19
 
20
- @hook.command('stdin-receiver')
20
+ @hooks.command('receiver')
21
21
  @click.option('--tool-name', help='Override tool name (for testing)')
22
22
  @click.option('--action', type=click.Choice(['start', 'end']), help='Override action (for testing)')
23
23
  @click.option('--subagent-type', help='Override subagent type (for testing)')
24
24
  @click.option('--event', type=click.Choice(['PreToolUse', 'PostToolUse']), help='Override event (for testing)')
25
25
  @click.option('--debug', is_flag=True, help='Enable debug output')
26
- def stdin_receiver(tool_name, action, subagent_type, event, debug):
26
+ def receiver(tool_name, action, subagent_type, event, debug):
27
27
  """Receive and process hook events from Claude Code via stdin.
28
28
 
29
29
  This command reads JSON from stdin when called by Claude Code hooks,
@@ -34,13 +34,13 @@ def stdin_receiver(tool_name, action, subagent_type, event, debug):
34
34
 
35
35
  Examples:
36
36
  # Called by Claude Code (reads JSON from stdin)
37
- voicemode hook stdin-receiver
37
+ voicemode claude hooks receiver
38
38
 
39
39
  # Testing with defaults
40
- voicemode hook stdin-receiver --debug
40
+ voicemode claude hooks receiver --debug
41
41
 
42
42
  # Testing with specific values
43
- voicemode hook stdin-receiver --tool-name Task --action start --subagent-type mama-bear
43
+ voicemode claude hooks receiver --tool-name Task --action start --subagent-type mama-bear
44
44
  """
45
45
  from voice_mode.tools.sound_fonts.audio_player import Player
46
46
 
@@ -188,22 +188,19 @@ def find_sound_file(event: str, tool: str, subagent: Optional[str] = None) -> Op
188
188
  return None
189
189
 
190
190
 
191
- # Keep the old receiver command for backwards compatibility (deprecated)
192
- @hook.command('receiver', hidden=True)
191
+ # Keep the old stdin-receiver command for backwards compatibility (deprecated)
192
+ @hooks.command('stdin-receiver', hidden=True)
193
193
  @click.argument('tool_name')
194
194
  @click.argument('action', type=click.Choice(['start', 'end', 'complete']))
195
195
  @click.argument('subagent_type', required=False)
196
196
  @click.option('--debug', is_flag=True, help='Enable debug output')
197
- def receiver_deprecated(tool_name, action, subagent_type, debug):
198
- """[DEPRECATED] Use stdin-receiver instead."""
199
- # Map old action to event
200
- event = 'PreToolUse' if action == 'start' else 'PostToolUse'
201
-
202
- # Call the new command
197
+ def stdin_receiver_deprecated(tool_name, action, subagent_type, event, debug):
198
+ """[DEPRECATED] Use receiver instead."""
199
+ # Call the new receiver command
203
200
  ctx = click.get_current_context()
204
- ctx.invoke(stdin_receiver,
201
+ ctx.invoke(receiver,
205
202
  tool_name=tool_name,
206
- action=action if action != 'complete' else 'end',
203
+ action=action,
207
204
  subagent_type=subagent_type,
208
205
  event=event,
209
206
  debug=debug)
@@ -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,
voice_mode/config.py CHANGED
@@ -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
- SIMPLE_FAILOVER = os.getenv("VOICEMODE_SIMPLE_FAILOVER", "true").lower() in ("true", "1", "yes", "on")
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"),
voice_mode/core.py CHANGED
@@ -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
 
@@ -1 +1 @@
1
- -9MkXl-xGB2BLmGEAZVmg
1
+ Ni4GIqyDdn0QehvmlLBZg
@@ -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-991c8c54319817e0.js",
7
+ "static/chunks/main-app-78da5e437b6a2a9f.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-991c8c54319817e0.js",
14
+ "static/chunks/main-app-78da5e437b6a2a9f.js",
15
15
  "static/css/a2f49a47752b5010.css",
16
- "static/chunks/app/layout-d89915a94cb8f604.js"
16
+ "static/chunks/app/layout-d3ec7f6f14ea7396.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-991c8c54319817e0.js",
22
+ "static/chunks/main-app-78da5e437b6a2a9f.js",
23
23
  "static/chunks/144d3bae-2d5f122b82426d88.js",
24
24
  "static/chunks/471-bd4b96a33883dfa2.js",
25
- "static/chunks/app/page-728ce55710c9bd80.js"
25
+ "static/chunks/app/page-471796963fb1a4bd.js"
26
26
  ]
27
27
  }
28
28
  }
@@ -5,14 +5,14 @@
5
5
  "devFiles": [],
6
6
  "ampDevFiles": [],
7
7
  "lowPriorityFiles": [
8
- "static/-9MkXl-xGB2BLmGEAZVmg/_buildManifest.js",
9
- "static/-9MkXl-xGB2BLmGEAZVmg/_ssgManifest.js"
8
+ "static/Ni4GIqyDdn0QehvmlLBZg/_buildManifest.js",
9
+ "static/Ni4GIqyDdn0QehvmlLBZg/_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-991c8c54319817e0.js"
15
+ "static/chunks/main-app-78da5e437b6a2a9f.js"
16
16
  ],
17
17
  "pages": {
18
18
  "/_app": [
@@ -1 +1 @@
1
- {"version":1,"files":["../node_modules/styled-jsx/index.js","../node_modules/styled-jsx/package.json","../node_modules/styled-jsx/dist/index/index.js","../node_modules/react/package.json","../node_modules/react/index.js","../node_modules/client-only/package.json","../node_modules/react/cjs/react.production.min.js","../node_modules/client-only/index.js","../node_modules/styled-jsx/style.js","../node_modules/next/dist/compiled/next-server/server.runtime.prod.js","../node_modules/next/package.json","../node_modules/next/dist/lib/constants.js","../node_modules/next/dist/server/body-streams.js","../node_modules/next/dist/lib/picocolors.js","../node_modules/next/dist/shared/lib/constants.js","../node_modules/next/dist/server/web/utils.js","../node_modules/next/dist/client/components/app-router-headers.js","../node_modules/next/dist/server/lib/trace/tracer.js","../node_modules/next/dist/server/lib/trace/constants.js","../node_modules/next/dist/client/components/static-generation-async-storage.external.js","../node_modules/next/dist/shared/lib/error-source.js","../node_modules/next/dist/shared/lib/modern-browserslist-target.js","../node_modules/next/dist/compiled/debug/package.json","../node_modules/next/dist/client/components/static-generation-async-storage-instance.js","../node_modules/next/dist/shared/lib/runtime-config.external.js","../node_modules/next/dist/compiled/debug/index.js","../node_modules/next/dist/compiled/ws/package.json","../node_modules/next/dist/compiled/node-html-parser/package.json","../node_modules/next/dist/compiled/lru-cache/package.json","../node_modules/@swc/helpers/_/_interop_require_default/package.json","../node_modules/next/dist/compiled/ws/index.js","../node_modules/next/dist/compiled/node-html-parser/index.js","../node_modules/next/dist/compiled/lru-cache/index.js","../node_modules/next/dist/client/components/async-local-storage.js","../node_modules/next/dist/compiled/@opentelemetry/api/package.json","../node_modules/@swc/helpers/package.json","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/parseStack.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.js","../node_modules/next/dist/compiled/jsonwebtoken/package.json","../node_modules/next/dist/client/components/react-dev-overlay/server/middleware.js","../node_modules/@swc/helpers/cjs/_interop_require_default.cjs","../node_modules/next/dist/compiled/@opentelemetry/api/index.js","../node_modules/next/dist/compiled/jsonwebtoken/index.js","../node_modules/next/dist/compiled/browserslist/package.json","../node_modules/next/dist/compiled/browserslist/index.js","../node_modules/next/dist/client/components/react-dev-overlay/server/shared.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getRawSourceMap.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/launchEditor.js","../node_modules/next/dist/compiled/babel/code-frame.js","../node_modules/next/dist/compiled/json5/package.json","../node_modules/next/dist/compiled/semver/package.json","../node_modules/next/dist/compiled/babel/package.json","../node_modules/next/dist/lib/semver-noop.js","../node_modules/next/dist/compiled/json5/index.js","../node_modules/next/dist/compiled/semver/index.js","../node_modules/next/dist/compiled/stacktrace-parser/package.json","../node_modules/next/dist/compiled/source-map08/package.json","../node_modules/caniuse-lite/dist/unpacker/agents.js","../node_modules/caniuse-lite/dist/unpacker/feature.js","../node_modules/caniuse-lite/dist/unpacker/region.js","../node_modules/next/dist/compiled/babel/bundle.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getSourceMapUrl.js","../node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js","../node_modules/next/dist/compiled/source-map08/source-map.js","../node_modules/caniuse-lite/package.json","../node_modules/next/dist/compiled/babel/core.js","../node_modules/caniuse-lite/data/agents.js","../node_modules/caniuse-lite/dist/unpacker/browsers.js","../node_modules/caniuse-lite/dist/unpacker/browserVersions.js","../node_modules/caniuse-lite/dist/lib/statuses.js","../node_modules/caniuse-lite/dist/lib/supported.js","../node_modules/next/dist/compiled/data-uri-to-buffer/package.json","../node_modules/next/dist/compiled/shell-quote/package.json","../node_modules/next/dist/compiled/data-uri-to-buffer/index.js","../node_modules/next/dist/compiled/shell-quote/index.js","../node_modules/caniuse-lite/data/browsers.js","../node_modules/caniuse-lite/data/browserVersions.js","../node_modules/next/dist/compiled/babel-packages/package.json","../node_modules/next/dist/compiled/babel-packages/packages-bundle.js","../node_modules/next/dist/compiled/babel/types.js","../node_modules/next/dist/compiled/babel/traverse.js","../node_modules/next/dist/compiled/babel/parser.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/app-page/module.compiled.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/pages/module.compiled.js"]}
1
+ {"version":1,"files":["../node_modules/styled-jsx/index.js","../node_modules/styled-jsx/package.json","../node_modules/styled-jsx/dist/index/index.js","../node_modules/react/package.json","../node_modules/react/index.js","../node_modules/client-only/package.json","../node_modules/react/cjs/react.production.min.js","../node_modules/client-only/index.js","../node_modules/styled-jsx/style.js","../node_modules/next/dist/compiled/next-server/server.runtime.prod.js","../node_modules/next/package.json","../node_modules/next/dist/lib/constants.js","../node_modules/next/dist/server/body-streams.js","../node_modules/next/dist/lib/picocolors.js","../node_modules/next/dist/shared/lib/constants.js","../node_modules/next/dist/server/web/utils.js","../node_modules/next/dist/client/components/app-router-headers.js","../node_modules/next/dist/server/lib/trace/tracer.js","../node_modules/next/dist/server/lib/trace/constants.js","../node_modules/next/dist/client/components/static-generation-async-storage.external.js","../node_modules/next/dist/shared/lib/error-source.js","../node_modules/next/dist/shared/lib/modern-browserslist-target.js","../node_modules/next/dist/compiled/debug/package.json","../node_modules/next/dist/client/components/static-generation-async-storage-instance.js","../node_modules/next/dist/shared/lib/runtime-config.external.js","../node_modules/next/dist/compiled/debug/index.js","../node_modules/next/dist/compiled/ws/package.json","../node_modules/next/dist/compiled/lru-cache/package.json","../node_modules/next/dist/compiled/node-html-parser/package.json","../node_modules/@swc/helpers/_/_interop_require_default/package.json","../node_modules/next/dist/compiled/ws/index.js","../node_modules/next/dist/compiled/lru-cache/index.js","../node_modules/next/dist/compiled/node-html-parser/index.js","../node_modules/next/dist/client/components/async-local-storage.js","../node_modules/next/dist/compiled/@opentelemetry/api/package.json","../node_modules/@swc/helpers/package.json","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/parseStack.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.js","../node_modules/next/dist/compiled/jsonwebtoken/package.json","../node_modules/next/dist/client/components/react-dev-overlay/server/middleware.js","../node_modules/@swc/helpers/cjs/_interop_require_default.cjs","../node_modules/next/dist/compiled/@opentelemetry/api/index.js","../node_modules/next/dist/compiled/jsonwebtoken/index.js","../node_modules/next/dist/compiled/browserslist/package.json","../node_modules/next/dist/compiled/browserslist/index.js","../node_modules/next/dist/client/components/react-dev-overlay/server/shared.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getRawSourceMap.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/launchEditor.js","../node_modules/next/dist/compiled/babel/code-frame.js","../node_modules/next/dist/compiled/json5/package.json","../node_modules/next/dist/compiled/semver/package.json","../node_modules/next/dist/compiled/babel/package.json","../node_modules/next/dist/lib/semver-noop.js","../node_modules/next/dist/compiled/json5/index.js","../node_modules/next/dist/compiled/semver/index.js","../node_modules/next/dist/compiled/stacktrace-parser/package.json","../node_modules/next/dist/compiled/source-map08/package.json","../node_modules/caniuse-lite/dist/unpacker/agents.js","../node_modules/caniuse-lite/dist/unpacker/feature.js","../node_modules/caniuse-lite/dist/unpacker/region.js","../node_modules/next/dist/compiled/babel/bundle.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getSourceMapUrl.js","../node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js","../node_modules/next/dist/compiled/source-map08/source-map.js","../node_modules/caniuse-lite/package.json","../node_modules/next/dist/compiled/babel/core.js","../node_modules/caniuse-lite/data/agents.js","../node_modules/caniuse-lite/dist/lib/statuses.js","../node_modules/caniuse-lite/dist/lib/supported.js","../node_modules/caniuse-lite/dist/unpacker/browsers.js","../node_modules/caniuse-lite/dist/unpacker/browserVersions.js","../node_modules/next/dist/compiled/shell-quote/package.json","../node_modules/next/dist/compiled/data-uri-to-buffer/package.json","../node_modules/next/dist/compiled/shell-quote/index.js","../node_modules/next/dist/compiled/data-uri-to-buffer/index.js","../node_modules/caniuse-lite/data/browsers.js","../node_modules/caniuse-lite/data/browserVersions.js","../node_modules/next/dist/compiled/babel-packages/package.json","../node_modules/next/dist/compiled/babel-packages/packages-bundle.js","../node_modules/next/dist/compiled/babel/parser.js","../node_modules/next/dist/compiled/babel/types.js","../node_modules/next/dist/compiled/babel/traverse.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/app-page/module.compiled.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/pages/module.compiled.js"]}