voice-mode 4.1.0__py3-none-any.whl → 4.2.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 (74) hide show
  1. voice_mode/__version__.py +1 -1
  2. voice_mode/cli.py +70 -0
  3. voice_mode/cli_commands/claude.py +208 -0
  4. voice_mode/cli_commands/hook.py +197 -0
  5. voice_mode/config.py +25 -0
  6. voice_mode/frontend/.next/BUILD_ID +1 -1
  7. voice_mode/frontend/.next/app-build-manifest.json +5 -5
  8. voice_mode/frontend/.next/build-manifest.json +3 -3
  9. voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
  10. voice_mode/frontend/.next/next-server.js.nft.json +1 -1
  11. voice_mode/frontend/.next/prerender-manifest.json +1 -1
  12. voice_mode/frontend/.next/required-server-files.json +1 -1
  13. voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
  14. voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. voice_mode/frontend/.next/server/app/_not-found.html +1 -1
  16. voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
  17. voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
  18. voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
  19. voice_mode/frontend/.next/server/app/index.html +1 -1
  20. voice_mode/frontend/.next/server/app/index.rsc +2 -2
  21. voice_mode/frontend/.next/server/app/page.js +3 -3
  22. voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
  23. voice_mode/frontend/.next/server/chunks/994.js +2 -2
  24. voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
  25. voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
  26. voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
  27. voice_mode/frontend/.next/server/pages/404.html +1 -1
  28. voice_mode/frontend/.next/server/pages/500.html +1 -1
  29. voice_mode/frontend/.next/server/pages-manifest.json +1 -1
  30. voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
  31. voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
  32. voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
  33. voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
  34. voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
  35. voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
  36. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  37. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
  39. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  40. voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
  41. voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
  42. voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
  43. voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
  44. voice_mode/frontend/.next/standalone/.next/server/app/page.js +3 -3
  45. voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +2 -2
  47. voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  48. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
  49. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
  50. voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
  51. voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
  52. voice_mode/frontend/.next/standalone/.next/server/pages-manifest.json +1 -1
  53. voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  54. voice_mode/frontend/.next/standalone/server.js +1 -1
  55. voice_mode/frontend/.next/static/chunks/app/layout-df4aba5e7309efec.js +1 -0
  56. voice_mode/frontend/.next/static/chunks/app/{page-7c7ec2ad413ace39.js → page-a87d04099ce6aeb2.js} +1 -1
  57. voice_mode/frontend/.next/static/chunks/{main-app-d02bd38ac01adb8a.js → main-app-ca62791c49278d6d.js} +1 -1
  58. voice_mode/frontend/.next/trace +43 -43
  59. voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
  60. voice_mode/frontend/.next/types/app/layout.ts +1 -1
  61. voice_mode/frontend/.next/types/app/page.ts +1 -1
  62. voice_mode/frontend/package-lock.json +3 -3
  63. voice_mode/tools/claude_thinking.py +285 -0
  64. voice_mode/tools/sound_fonts/__init__.py +1 -0
  65. voice_mode/tools/sound_fonts/audio_player.py +87 -0
  66. voice_mode/tools/sound_fonts/hook_handler.py +127 -0
  67. voice_mode/tools/sound_fonts/player.py +180 -0
  68. {voice_mode-4.1.0.dist-info → voice_mode-4.2.0.dist-info}/METADATA +1 -1
  69. {voice_mode-4.1.0.dist-info → voice_mode-4.2.0.dist-info}/RECORD +73 -66
  70. voice_mode/frontend/.next/static/chunks/app/layout-fcb9b9ba5b72c7fc.js +0 -1
  71. /voice_mode/frontend/.next/static/{pbDjheefW1LwCua_8mPoZ → FTYM9NyY_2zq92GYxPDhS}/_buildManifest.js +0 -0
  72. /voice_mode/frontend/.next/static/{pbDjheefW1LwCua_8mPoZ → FTYM9NyY_2zq92GYxPDhS}/_ssgManifest.js +0 -0
  73. {voice_mode-4.1.0.dist-info → voice_mode-4.2.0.dist-info}/WHEEL +0 -0
  74. {voice_mode-4.1.0.dist-info → voice_mode-4.2.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.1.0"
3
+ __version__ = "4.2.0"
voice_mode/cli.py CHANGED
@@ -1361,19 +1361,26 @@ def cli():
1361
1361
  from voice_mode.cli_commands import exchanges as exchanges_cmd
1362
1362
  from voice_mode.cli_commands import transcribe as transcribe_cmd
1363
1363
  from voice_mode.cli_commands import pronounce_commands
1364
+ from voice_mode.cli_commands import claude
1365
+ from voice_mode.cli_commands import hook as hook_cmd
1364
1366
 
1365
1367
  # Add subcommands to legacy CLI
1366
1368
  cli.add_command(exchanges_cmd.exchanges)
1367
1369
  cli.add_command(transcribe_cmd.transcribe)
1368
1370
  cli.add_command(pronounce_commands.pronounce_group)
1371
+ cli.add_command(claude.claude_group)
1369
1372
 
1370
1373
  # Add exchanges to main CLI
1371
1374
  voice_mode_main_cli.add_command(exchanges_cmd.exchanges)
1372
1375
  voice_mode_main_cli.add_command(pronounce_commands.pronounce_group)
1376
+ voice_mode_main_cli.add_command(claude.claude_group)
1373
1377
 
1374
1378
  # Add transcribe to main CLI
1375
1379
  voice_mode_main_cli.add_command(transcribe_cmd.transcribe)
1376
1380
 
1381
+ # Add hook command to main CLI
1382
+ voice_mode_main_cli.add_command(hook_cmd.hook)
1383
+
1377
1384
 
1378
1385
  # Converse command - direct voice conversation from CLI
1379
1386
  @voice_mode_main_cli.command()
@@ -1751,6 +1758,69 @@ def update(force):
1751
1758
  click.echo("Try running: pip install --upgrade voice-mode")
1752
1759
 
1753
1760
 
1761
+ # Sound Fonts command
1762
+ @voice_mode_main_cli.command("play-sound")
1763
+ @click.help_option('-h', '--help')
1764
+ @click.option('-t', '--tool', help='Tool name for direct command-line usage')
1765
+ @click.option('-a', '--action', default='start', type=click.Choice(['start', 'end']), help='Action type')
1766
+ @click.option('-s', '--subagent', help='Subagent type (for Task tool)')
1767
+ def play_sound(tool, action, subagent):
1768
+ """Play sound based on tool events (primarily for Claude Code hooks).
1769
+
1770
+ This command is designed to be called by Claude Code hooks to play sounds
1771
+ when tools are used. It reads hook data from stdin by default, or can be
1772
+ used directly with command-line options.
1773
+
1774
+ Examples:
1775
+ echo '{"tool_name":"Task","tool_input":{"subagent_type":"mama-bear"}}' | voicemode play-sound
1776
+ voicemode play-sound --tool Task --action start --subagent mama-bear
1777
+ """
1778
+ import sys
1779
+ from .tools.sound_fonts.player import AudioPlayer
1780
+ from .tools.sound_fonts.hook_handler import (
1781
+ read_hook_data_from_stdin,
1782
+ parse_claude_code_hook
1783
+ )
1784
+
1785
+ # Try to read hook data from stdin first
1786
+ hook_data = None
1787
+ if not sys.stdin.isatty():
1788
+ hook_data = read_hook_data_from_stdin()
1789
+
1790
+ if hook_data:
1791
+ # Parse Claude Code hook format
1792
+ parsed_data = parse_claude_code_hook(hook_data)
1793
+ if not parsed_data:
1794
+ sys.exit(1)
1795
+
1796
+ tool_name = parsed_data["tool_name"]
1797
+ action_type = parsed_data["action"]
1798
+ subagent_type = parsed_data["subagent_type"]
1799
+ metadata = parsed_data["metadata"]
1800
+ else:
1801
+ # Use command-line arguments
1802
+ if not tool:
1803
+ click.echo("Error: --tool is required when not reading from stdin", err=True)
1804
+ sys.exit(1)
1805
+
1806
+ tool_name = tool
1807
+ action_type = action
1808
+ subagent_type = subagent
1809
+ metadata = {}
1810
+
1811
+ # Play the sound
1812
+ player = AudioPlayer()
1813
+ success = player.play_sound_for_event(
1814
+ tool_name=tool_name,
1815
+ action=action_type,
1816
+ subagent_type=subagent_type,
1817
+ metadata=metadata
1818
+ )
1819
+
1820
+ # Silent exit for hooks - don't clutter Claude Code output
1821
+ sys.exit(0 if success else 1)
1822
+
1823
+
1754
1824
  # Completions command
1755
1825
  @voice_mode_main_cli.command()
1756
1826
  @click.help_option('-h', '--help')
@@ -0,0 +1,208 @@
1
+ """CLI commands for Claude Code message extraction."""
2
+
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
+
19
+
20
+ @click.group(name='claude')
21
+ def claude_group():
22
+ """Extract messages from Claude Code conversation logs."""
23
+ pass
24
+
25
+
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)")
@@ -0,0 +1,197 @@
1
+ """
2
+ Hook commands for Voice Mode - primarily for Claude Code integration.
3
+ """
4
+
5
+ import click
6
+ import sys
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ @click.group()
14
+ @click.help_option('-h', '--help', help='Show this message and exit')
15
+ def hook():
16
+ """Manage Voice Mode hooks and event handlers."""
17
+ pass
18
+
19
+
20
+ @hook.command('stdin-receiver')
21
+ @click.option('--tool-name', help='Override tool name (for testing)')
22
+ @click.option('--action', type=click.Choice(['start', 'end']), help='Override action (for testing)')
23
+ @click.option('--subagent-type', help='Override subagent type (for testing)')
24
+ @click.option('--event', type=click.Choice(['PreToolUse', 'PostToolUse']), help='Override event (for testing)')
25
+ @click.option('--debug', is_flag=True, help='Enable debug output')
26
+ def stdin_receiver(tool_name, action, subagent_type, event, debug):
27
+ """Receive and process hook events from Claude Code via stdin.
28
+
29
+ This command reads JSON from stdin when called by Claude Code hooks,
30
+ or accepts command-line arguments for testing.
31
+
32
+ The filesystem structure defines sound mappings:
33
+ ~/.voicemode/soundfonts/current/PreToolUse/task/subagent/baby-bear.wav
34
+
35
+ Examples:
36
+ # Called by Claude Code (reads JSON from stdin)
37
+ voicemode hook stdin-receiver
38
+
39
+ # Testing with defaults
40
+ voicemode hook stdin-receiver --debug
41
+
42
+ # Testing with specific values
43
+ voicemode hook stdin-receiver --tool-name Task --action start --subagent-type mama-bear
44
+ """
45
+ from voice_mode.tools.sound_fonts.audio_player import Player
46
+
47
+ # Try to read JSON from stdin if available
48
+ hook_data = {}
49
+ if not sys.stdin.isatty():
50
+ try:
51
+ hook_data = json.load(sys.stdin)
52
+ if debug:
53
+ print(f"[DEBUG] Received JSON: {json.dumps(hook_data, indent=2)}", file=sys.stderr)
54
+ except Exception as e:
55
+ if debug:
56
+ print(f"[DEBUG] Failed to parse JSON from stdin: {e}", file=sys.stderr)
57
+ # Silent fail for hooks
58
+ sys.exit(0)
59
+
60
+ # Extract values from JSON or use command-line overrides/defaults
61
+ if not tool_name:
62
+ tool_name = hook_data.get('tool_name', 'Task')
63
+
64
+ if not event:
65
+ event_name = hook_data.get('hook_event_name', 'PreToolUse')
66
+ else:
67
+ event_name = event
68
+
69
+ # Map event to action if not specified
70
+ if not action:
71
+ if event_name == 'PreToolUse':
72
+ action = 'start'
73
+ elif event_name == 'PostToolUse':
74
+ action = 'end'
75
+ else:
76
+ action = 'start' # Default
77
+
78
+ # Get subagent_type from tool_input if not specified
79
+ if not subagent_type and tool_name == 'Task':
80
+ tool_input = hook_data.get('tool_input', {})
81
+ subagent_type = tool_input.get('subagent_type', 'baby-bear')
82
+ elif not subagent_type:
83
+ subagent_type = None
84
+
85
+ if debug:
86
+ print(f"[DEBUG] Processing: event={event_name}, tool={tool_name}, "
87
+ f"action={action}, subagent={subagent_type}", file=sys.stderr)
88
+
89
+ # Find sound file using filesystem conventions
90
+ sound_file = find_sound_file(event_name, tool_name, subagent_type)
91
+
92
+ if sound_file:
93
+ if debug:
94
+ print(f"[DEBUG] Found sound file: {sound_file}", file=sys.stderr)
95
+
96
+ # Play the sound
97
+ player = Player()
98
+ success = player.play(str(sound_file))
99
+
100
+ if debug:
101
+ if success:
102
+ print(f"[DEBUG] Sound played successfully", file=sys.stderr)
103
+ else:
104
+ print(f"[DEBUG] Failed to play sound", file=sys.stderr)
105
+ else:
106
+ if debug:
107
+ print(f"[DEBUG] No sound file found for this event", file=sys.stderr)
108
+
109
+ # Always exit 0 to not disrupt Claude Code
110
+ sys.exit(0)
111
+
112
+
113
+ def find_sound_file(event: str, tool: str, subagent: Optional[str] = None) -> Optional[Path]:
114
+ """
115
+ Find sound file using filesystem conventions.
116
+
117
+ Tries paths in order:
118
+ 1. Most specific: {event}/{tool}/subagent/{subagent}.wav (Task tool only)
119
+ 2. Tool default: {event}/{tool}/default.wav
120
+ 3. Event default: {event}/default.wav
121
+ 4. Global fallback: fallback.wav
122
+
123
+ Args:
124
+ event: Event name (PreToolUse, PostToolUse)
125
+ tool: Tool name (lowercase)
126
+ subagent: Optional subagent type (lowercase)
127
+
128
+ Returns:
129
+ Path to sound file if found, None otherwise
130
+ """
131
+ # Get base path (follow symlink if exists)
132
+ base_path = Path.home() / '.voicemode' / 'soundfonts' / 'current'
133
+
134
+ # Resolve symlink if it exists
135
+ if base_path.is_symlink():
136
+ base_path = base_path.resolve()
137
+
138
+ if not base_path.exists():
139
+ return None
140
+
141
+ # Normalize names to lowercase for filesystem
142
+ event = event.lower() if event else 'pretooluse'
143
+ tool = tool.lower() if tool else 'default'
144
+ subagent = subagent.lower() if subagent else None
145
+
146
+ # Map event names to directory names
147
+ event_map = {
148
+ 'pretooluse': 'PreToolUse',
149
+ 'posttooluse': 'PostToolUse',
150
+ 'start': 'PreToolUse',
151
+ 'end': 'PostToolUse'
152
+ }
153
+ event_dir = event_map.get(event, event)
154
+
155
+ # Build list of paths to try (most specific to least specific)
156
+ paths_to_try = []
157
+
158
+ # 1. Most specific: subagent sound (Task tool only)
159
+ if tool == 'task' and subagent:
160
+ paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.wav')
161
+
162
+ # 2. Tool-specific default
163
+ paths_to_try.append(base_path / event_dir / tool / 'default.wav')
164
+
165
+ # 3. Event-level default
166
+ paths_to_try.append(base_path / event_dir / 'default.wav')
167
+
168
+ # 4. Global fallback
169
+ paths_to_try.append(base_path / 'fallback.wav')
170
+
171
+ # Find first existing file
172
+ for path in paths_to_try:
173
+ if path.exists():
174
+ return path
175
+
176
+ return None
177
+
178
+
179
+ # Keep the old receiver command for backwards compatibility (deprecated)
180
+ @hook.command('receiver', hidden=True)
181
+ @click.argument('tool_name')
182
+ @click.argument('action', type=click.Choice(['start', 'end', 'complete']))
183
+ @click.argument('subagent_type', required=False)
184
+ @click.option('--debug', is_flag=True, help='Enable debug output')
185
+ def receiver_deprecated(tool_name, action, subagent_type, debug):
186
+ """[DEPRECATED] Use stdin-receiver instead."""
187
+ # Map old action to event
188
+ event = 'PreToolUse' if action == 'start' else 'PostToolUse'
189
+
190
+ # Call the new command
191
+ ctx = click.get_current_context()
192
+ ctx.invoke(stdin_receiver,
193
+ tool_name=tool_name,
194
+ action=action if action != 'complete' else 'end',
195
+ subagent_type=subagent_type,
196
+ event=event,
197
+ debug=debug)
voice_mode/config.py CHANGED
@@ -841,3 +841,28 @@ def get_format_export_params(format: str) -> dict:
841
841
  pass
842
842
 
843
843
  return params
844
+
845
+ # ==================== THINK OUT LOUD CONFIGURATION ====================
846
+
847
+ # Enable Think Out Loud mode
848
+ THINK_OUT_LOUD_ENABLED = env_bool("VOICEMODE_THINK_OUT_LOUD", False)
849
+
850
+ # Voice persona mappings for thinking roles (role:voice pairs)
851
+ # Default: analytical:am_adam,creative:af_sarah,critical:af_bella,synthesis:af_nova
852
+ THINKING_VOICES_STR = os.getenv(
853
+ "VOICEMODE_THINKING_VOICES",
854
+ "analytical:am_adam,creative:af_sarah,critical:af_bella,synthesis:af_nova"
855
+ )
856
+
857
+ # Parse thinking voices into a dictionary
858
+ THINKING_VOICES = {}
859
+ for pair in THINKING_VOICES_STR.split(","):
860
+ if ":" in pair:
861
+ role, voice = pair.strip().split(":", 1)
862
+ THINKING_VOICES[role.strip()] = voice.strip()
863
+
864
+ # Thinking presentation style: sequential, debate, or chorus
865
+ THINKING_STYLE = os.getenv("VOICEMODE_THINKING_STYLE", "sequential")
866
+
867
+ # Whether to announce which voice is speaking
868
+ THINKING_ANNOUNCE_VOICE = env_bool("VOICEMODE_THINKING_ANNOUNCE_VOICE", True)
@@ -1 +1 @@
1
- pbDjheefW1LwCua_8mPoZ
1
+ FTYM9NyY_2zq92GYxPDhS
@@ -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-d02bd38ac01adb8a.js",
7
+ "static/chunks/main-app-ca62791c49278d6d.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-d02bd38ac01adb8a.js",
14
+ "static/chunks/main-app-ca62791c49278d6d.js",
15
15
  "static/css/a2f49a47752b5010.css",
16
- "static/chunks/app/layout-fcb9b9ba5b72c7fc.js"
16
+ "static/chunks/app/layout-df4aba5e7309efec.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-d02bd38ac01adb8a.js",
22
+ "static/chunks/main-app-ca62791c49278d6d.js",
23
23
  "static/chunks/144d3bae-2d5f122b82426d88.js",
24
24
  "static/chunks/471-bd4b96a33883dfa2.js",
25
- "static/chunks/app/page-7c7ec2ad413ace39.js"
25
+ "static/chunks/app/page-a87d04099ce6aeb2.js"
26
26
  ]
27
27
  }
28
28
  }
@@ -5,14 +5,14 @@
5
5
  "devFiles": [],
6
6
  "ampDevFiles": [],
7
7
  "lowPriorityFiles": [
8
- "static/pbDjheefW1LwCua_8mPoZ/_buildManifest.js",
9
- "static/pbDjheefW1LwCua_8mPoZ/_ssgManifest.js"
8
+ "static/FTYM9NyY_2zq92GYxPDhS/_buildManifest.js",
9
+ "static/FTYM9NyY_2zq92GYxPDhS/_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-d02bd38ac01adb8a.js"
15
+ "static/chunks/main-app-ca62791c49278d6d.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/feature.js","../node_modules/caniuse-lite/dist/unpacker/agents.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/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/parser.js","../node_modules/next/dist/compiled/babel/traverse.js","../node_modules/next/dist/compiled/babel/types.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/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/browserVersions.js","../node_modules/caniuse-lite/data/browsers.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/traverse.js","../node_modules/next/dist/compiled/babel/parser.js","../node_modules/next/dist/compiled/babel/types.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"]}