claude-code-tools 1.19.6__tar.gz → 1.19.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/PKG-INFO +1 -1
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/app.py +5 -1
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/cli.py +25 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/config.py +25 -5
- claude_code_tools-1.19.8/claude_code_tools/voice_type/overlay.py +402 -0
- claude_code_tools-1.19.8/claude_code_tools/voice_type/setup_wizard.py +357 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/pyproject.toml +2 -2
- claude_code_tools-1.19.6/claude_code_tools/voice_type/overlay.py +0 -273
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/.gitignore +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/LICENSE +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/README.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/aichat.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_legacy.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_models.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_reservation.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_retry.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_state.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/export_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/port_claude_noise.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/port_claude_to_codex.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/port_codex_flatten.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/port_codex_to_claude.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/port_service.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session_names.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session_render.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/__init__.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engine_parakeet.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engine_parakeet_mlx.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engines.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/hotkey.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/inject.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/logic.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/lmsh.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/msg-phase-a-plan.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/msg-plan-v2.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/hatch_build.py +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/node_ui/menu.js +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/node_ui/package.json +0 -0
|
@@ -536,7 +536,11 @@ class VoiceTypeApp:
|
|
|
536
536
|
return state.value, state is State.ACTIVE, level
|
|
537
537
|
|
|
538
538
|
overlay.run_overlay(
|
|
539
|
-
sample,
|
|
539
|
+
sample,
|
|
540
|
+
tick,
|
|
541
|
+
self._stop.is_set,
|
|
542
|
+
flex=self.cfg.overlay_flex,
|
|
543
|
+
speed=self.cfg.overlay_speed,
|
|
540
544
|
)
|
|
541
545
|
return result["code"]
|
|
542
546
|
self._status("overlay unavailable; running without it")
|
|
@@ -157,6 +157,12 @@ def _cmd_hotkey() -> int:
|
|
|
157
157
|
return 0
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def _cmd_setup(args: argparse.Namespace) -> int:
|
|
161
|
+
from .setup_wizard import run_setup
|
|
162
|
+
|
|
163
|
+
return run_setup(config_path=args.config, force=args.force)
|
|
164
|
+
|
|
165
|
+
|
|
160
166
|
def _cmd_init(args: argparse.Namespace) -> int:
|
|
161
167
|
try:
|
|
162
168
|
path = write_sample_config(args.config, force=args.force)
|
|
@@ -164,6 +170,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
164
170
|
print(f"voice-type: {e}", file=sys.stderr)
|
|
165
171
|
return 1
|
|
166
172
|
print(f"wrote {path}")
|
|
173
|
+
print("(tip: `voice-type setup` walks you through it interactively)")
|
|
167
174
|
return 0
|
|
168
175
|
|
|
169
176
|
|
|
@@ -179,6 +186,7 @@ examples:
|
|
|
179
186
|
voice-type --engine parakeet-mlx --segmentation hold
|
|
180
187
|
# best accuracy: whole-take dictation
|
|
181
188
|
voice-type --mode wake # hands-free with wake word "claude"
|
|
189
|
+
voice-type setup # interactive walkthrough -> config
|
|
182
190
|
voice-type init # write a commented sample config
|
|
183
191
|
voice-type hotkey # record a chord, print the config line
|
|
184
192
|
|
|
@@ -218,12 +226,29 @@ Accessibility (to type), Input Monitoring (global hotkeys).
|
|
|
218
226
|
"hotkey",
|
|
219
227
|
help="press a key combo and print the matching config line",
|
|
220
228
|
)
|
|
229
|
+
p_setup = sub.add_parser(
|
|
230
|
+
"setup",
|
|
231
|
+
help="interactive walkthrough that writes your config",
|
|
232
|
+
)
|
|
233
|
+
p_setup.add_argument(
|
|
234
|
+
"--config",
|
|
235
|
+
type=Path,
|
|
236
|
+
# SUPPRESS so a --config given BEFORE the subcommand is not
|
|
237
|
+
# clobbered by the subparser's default (see the init parser).
|
|
238
|
+
default=argparse.SUPPRESS,
|
|
239
|
+
help="destination path",
|
|
240
|
+
)
|
|
241
|
+
p_setup.add_argument(
|
|
242
|
+
"--force", action="store_true", help="overwrite existing config"
|
|
243
|
+
)
|
|
221
244
|
|
|
222
245
|
args = parser.parse_args()
|
|
223
246
|
if args.command == "init":
|
|
224
247
|
return _cmd_init(args)
|
|
225
248
|
if args.command == "hotkey":
|
|
226
249
|
return _cmd_hotkey()
|
|
250
|
+
if args.command == "setup":
|
|
251
|
+
return _cmd_setup(args)
|
|
227
252
|
return _cmd_run(args)
|
|
228
253
|
|
|
229
254
|
|
{claude_code_tools-1.19.6 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/config.py
RENAMED
|
@@ -55,9 +55,13 @@ class Config:
|
|
|
55
55
|
fastest for v3-int8 on Apple Silicon; 8 helps v2-fp16).
|
|
56
56
|
strip_fillers: Drop standalone filler words (uh, um, ...) from
|
|
57
57
|
typed text.
|
|
58
|
-
overlay: Show the floating
|
|
59
|
-
(macOS only; ignored elsewhere). The
|
|
58
|
+
overlay: Show the floating ghost indicator while recording
|
|
59
|
+
(macOS only; ignored elsewhere). The ghost is hidden while
|
|
60
60
|
paused/passive — its presence IS the recording indicator.
|
|
61
|
+
overlay_flex: How much the ghost's face flexes/wobbles its
|
|
62
|
+
shape (1.0 = default; higher = more flexible, calmer < 1.0).
|
|
63
|
+
overlay_speed: Overall animation speed multiplier (1.0 =
|
|
64
|
+
default; lower = slower/gentler motion).
|
|
61
65
|
model_arch: Moonshine model architecture name (moonshine engine
|
|
62
66
|
only).
|
|
63
67
|
language: Language tag understood by Moonshine (e.g. "en").
|
|
@@ -98,6 +102,8 @@ class Config:
|
|
|
98
102
|
mlx_model: str = "mlx-community/parakeet-tdt-0.6b-v3"
|
|
99
103
|
strip_fillers: bool = True
|
|
100
104
|
overlay: bool = True
|
|
105
|
+
overlay_flex: float = 1.0
|
|
106
|
+
overlay_speed: float = 1.0
|
|
101
107
|
model_arch: str = "medium-streaming"
|
|
102
108
|
language: str = "en"
|
|
103
109
|
hotkey: str = "<ctrl>+;"
|
|
@@ -181,6 +187,16 @@ class Config:
|
|
|
181
187
|
raise ValueError(
|
|
182
188
|
f"{name} must be a string, got {value!r}"
|
|
183
189
|
)
|
|
190
|
+
for name in ("overlay_flex", "overlay_speed"):
|
|
191
|
+
value = getattr(self, name)
|
|
192
|
+
if isinstance(value, bool) or not isinstance(
|
|
193
|
+
value, (int, float)
|
|
194
|
+
):
|
|
195
|
+
raise ValueError(f"{name} must be a number, got {value!r}")
|
|
196
|
+
if not 0.0 <= value <= 5.0:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"{name} must be between 0 and 5, got {value!r}"
|
|
199
|
+
)
|
|
184
200
|
if self.mode == "wake" and not self.wake_word.strip():
|
|
185
201
|
raise ValueError('mode "wake" requires a non-empty wake_word')
|
|
186
202
|
if isinstance(self.idle_timeout, bool) or not isinstance(
|
|
@@ -304,10 +320,14 @@ parakeet_model = "v3-int8"
|
|
|
304
320
|
# Apple Silicon (~32x realtime); v2-fp16 benefits from 8 (~16x).
|
|
305
321
|
parakeet_threads = 4
|
|
306
322
|
|
|
307
|
-
# Floating
|
|
308
|
-
#
|
|
309
|
-
# steals focus.
|
|
323
|
+
# Floating ghost indicator (macOS): shown ONLY while recording — a
|
|
324
|
+
# little blue ghost whose mouth opens with your voice; hidden when
|
|
325
|
+
# paused/waiting. Click-through; never steals focus.
|
|
310
326
|
overlay = true
|
|
327
|
+
# How much the ghost's face flexes its shape (higher = more flexible).
|
|
328
|
+
overlay_flex = 1.0
|
|
329
|
+
# Overall animation speed (lower = slower, gentler motion).
|
|
330
|
+
overlay_speed = 1.0
|
|
311
331
|
|
|
312
332
|
# Remove standalone filler words (uh, um, ...) from typed text.
|
|
313
333
|
strip_fillers = true
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Floating waveform pill: visible proof that voice-type is listening.
|
|
2
|
+
|
|
3
|
+
A small always-on-top, click-through, non-activating macOS panel at the
|
|
4
|
+
bottom-center of the screen, shown ONLY while recording. It scrolls a
|
|
5
|
+
live waveform of the mic level — red waves as you speak — and hides
|
|
6
|
+
when paused or waiting for the wake word. Because the panel is a
|
|
7
|
+
non-activating borderless NSPanel that ignores mouse events, it can
|
|
8
|
+
never steal keyboard focus from the app being dictated into.
|
|
9
|
+
|
|
10
|
+
Uses pyobjc (already a transitive dependency of pynput on macOS).
|
|
11
|
+
The overlay must run on the process's MAIN thread (AppKit rule); the
|
|
12
|
+
app hands its per-tick housekeeping in as a callback.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import signal
|
|
18
|
+
from typing import Callable
|
|
19
|
+
|
|
20
|
+
# (state_label, is_recording, level 0..1) sampled ~20x per second.
|
|
21
|
+
SampleFn = Callable[[], tuple[str, bool, float]]
|
|
22
|
+
TickFn = Callable[[], None]
|
|
23
|
+
|
|
24
|
+
_WIDTH = 210.0
|
|
25
|
+
_HEIGHT = 240.0 # generous headroom so the halo never clips when moving
|
|
26
|
+
_MARGIN_BOTTOM = 110.0
|
|
27
|
+
_TICK_SECONDS = 0.033 # ~30 fps
|
|
28
|
+
# Master animation speed: small = slow, gentle, gradual motion.
|
|
29
|
+
_PHASE_SPEED = 0.05
|
|
30
|
+
# Idle blink: every ~_BLINK_EVERY frames the eyes close for a few.
|
|
31
|
+
_BLINK_EVERY = 130
|
|
32
|
+
_BLINK_FRAMES = 6
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def overlay_available() -> bool:
|
|
36
|
+
"""True if the AppKit bridge is importable (macOS with pyobjc)."""
|
|
37
|
+
try:
|
|
38
|
+
import AppKit # noqa: F401
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_overlay( # noqa: PLR0913
|
|
46
|
+
sample: SampleFn,
|
|
47
|
+
tick: TickFn,
|
|
48
|
+
stopped: Callable[[], bool],
|
|
49
|
+
flex: float = 1.0,
|
|
50
|
+
speed: float = 1.0,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Show the ghost and block until ``stopped()`` returns True.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
sample: Returns (state_label, is_recording, level) each frame.
|
|
56
|
+
tick: App housekeeping to run each frame (idle timeout, fatal
|
|
57
|
+
checks); exceptions are swallowed so the UI never dies.
|
|
58
|
+
stopped: Polled each frame; True ends the loop and closes the
|
|
59
|
+
panel.
|
|
60
|
+
flex: Face shape-flex multiplier (config overlay_flex).
|
|
61
|
+
speed: Animation speed multiplier (config overlay_speed).
|
|
62
|
+
|
|
63
|
+
SIGINT is redirected to a flag-friendly handler while the loop
|
|
64
|
+
runs (AppKit's run loop would otherwise swallow Ctrl+C), and
|
|
65
|
+
restored afterwards.
|
|
66
|
+
"""
|
|
67
|
+
import AppKit
|
|
68
|
+
import objc
|
|
69
|
+
from AppKit import (
|
|
70
|
+
NSAffineTransform,
|
|
71
|
+
NSApplication,
|
|
72
|
+
NSBackingStoreBuffered,
|
|
73
|
+
NSBezierPath,
|
|
74
|
+
NSColor,
|
|
75
|
+
NSGradient,
|
|
76
|
+
NSGraphicsContext,
|
|
77
|
+
NSMakeRect,
|
|
78
|
+
NSPanel,
|
|
79
|
+
NSScreen,
|
|
80
|
+
NSTimer,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Plain helpers (NOT NSView methods — pyobjc would try to bridge
|
|
84
|
+
# underscored methods as selectors and reject the extra arguments).
|
|
85
|
+
def _mk_oval(cx, cy, w, h): # noqa: ANN001, ANN202
|
|
86
|
+
return NSBezierPath.bezierPathWithOvalInRect_(
|
|
87
|
+
NSMakeRect(cx - w / 2, cy - h / 2, w, h)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _mk_ghost(cx, cy, gw, jelly, ph): # noqa: ANN001, ANN202
|
|
91
|
+
"""Ghost silhouette (domed head, wavy hem) with a soft jelly flex.
|
|
92
|
+
|
|
93
|
+
Built from points so a gentle, slow, spatially-coherent
|
|
94
|
+
displacement can wobble the whole outline — the head is
|
|
95
|
+
non-rigid (flexes shape), but the amplitude is small and smooth
|
|
96
|
+
so it never turns into a lumpy blob. ``jelly`` is the wobble
|
|
97
|
+
amplitude in points; ``ph`` the slow master phase.
|
|
98
|
+
"""
|
|
99
|
+
import math
|
|
100
|
+
|
|
101
|
+
dome_y = cy + gw * 0.55
|
|
102
|
+
hem_y = cy - gw * 0.95
|
|
103
|
+
pts = [(cx + gw, hem_y), (cx + gw, dome_y)]
|
|
104
|
+
nd = 26
|
|
105
|
+
for i in range(1, nd):
|
|
106
|
+
a = math.pi * i / nd
|
|
107
|
+
pts.append((cx + gw * math.cos(a), dome_y + gw * math.sin(a)))
|
|
108
|
+
pts.append((cx - gw, dome_y))
|
|
109
|
+
pts.append((cx - gw, hem_y))
|
|
110
|
+
segs, bumps, ha = 30, 3, gw * 0.16
|
|
111
|
+
for i in range(1, segs + 1):
|
|
112
|
+
u = i / segs
|
|
113
|
+
x = cx - gw + u * 2 * gw
|
|
114
|
+
pts.append((x, hem_y - ha * math.sin(u * math.pi * bumps)))
|
|
115
|
+
|
|
116
|
+
p = NSBezierPath.bezierPath()
|
|
117
|
+
for i, (x, y) in enumerate(pts):
|
|
118
|
+
dx = jelly * math.sin(0.028 * y + ph * 0.8)
|
|
119
|
+
dy = jelly * math.sin(0.028 * x - ph * 0.6)
|
|
120
|
+
pt = (x + dx, y + dy)
|
|
121
|
+
if i == 0:
|
|
122
|
+
p.moveToPoint_(pt)
|
|
123
|
+
else:
|
|
124
|
+
p.lineToPoint_(pt)
|
|
125
|
+
p.closePath()
|
|
126
|
+
return p
|
|
127
|
+
|
|
128
|
+
def _mk_mouth(mx, lip_y, gw, open_, ph): # noqa: ANN001, ANN202
|
|
129
|
+
"""An organic mouth-hole that opens downward (jaw) and morphs.
|
|
130
|
+
|
|
131
|
+
Not a scaling oval: the rim is perturbed by slow low harmonics
|
|
132
|
+
so it wobbles/changes shape, it opens mostly DOWNWARD from a
|
|
133
|
+
fixed upper-lip line (like a dropping jaw), and it stays a thin
|
|
134
|
+
slit at rest. ``open_`` is 0..1; ``ph`` is the slow master phase.
|
|
135
|
+
"""
|
|
136
|
+
import math
|
|
137
|
+
|
|
138
|
+
rx = gw * (0.36 + 0.08 * open_)
|
|
139
|
+
ry = gw * (0.05 + 0.34 * open_)
|
|
140
|
+
cy = lip_y - ry * 0.6 # grows downward from the lip line
|
|
141
|
+
# Constrained flex: two gentle low harmonics at small amplitude
|
|
142
|
+
# so the mouth has organic life without becoming a lumpy blob.
|
|
143
|
+
# (It is also clipped to the face by the caller, so it can never
|
|
144
|
+
# spill past the ghost's silhouette.)
|
|
145
|
+
deform = 0.04 + 0.06 * open_
|
|
146
|
+
n = 44
|
|
147
|
+
p = NSBezierPath.bezierPath()
|
|
148
|
+
for i in range(n + 1):
|
|
149
|
+
a = 2.0 * math.pi * i / n
|
|
150
|
+
wob = 1.0 + deform * (
|
|
151
|
+
math.sin(2 * a + ph * 0.5)
|
|
152
|
+
+ 0.4 * math.sin(3 * a - ph * 0.4)
|
|
153
|
+
)
|
|
154
|
+
x = mx + rx * wob * math.cos(a)
|
|
155
|
+
y = cy + ry * wob * math.sin(a)
|
|
156
|
+
if i == 0:
|
|
157
|
+
p.moveToPoint_((x, y))
|
|
158
|
+
else:
|
|
159
|
+
p.lineToPoint_((x, y))
|
|
160
|
+
p.closePath()
|
|
161
|
+
return p
|
|
162
|
+
|
|
163
|
+
class WaveView(AppKit.NSView): # noqa: D401
|
|
164
|
+
"""A little glowing-blue ghost whose mouth opens with your voice.
|
|
165
|
+
|
|
166
|
+
A friendly translucent ghost floats at the bottom of the screen:
|
|
167
|
+
it bobs and blinks on its own so it feels alive, and its mouth
|
|
168
|
+
opens wider the louder you speak (driven by the live,
|
|
169
|
+
gain-adjusted mic level) -- the Animoji-style talking face that
|
|
170
|
+
makes the tool fun to use.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def initWithFrame_(self, frame): # noqa: ANN001, ANN201, N802
|
|
174
|
+
self = objc.super(WaveView, self).initWithFrame_(frame)
|
|
175
|
+
if self is None:
|
|
176
|
+
return None
|
|
177
|
+
self.amp = 0.0 # smoothed level -> mouth opening
|
|
178
|
+
self.phase = 0.0
|
|
179
|
+
self.frame_no = 0
|
|
180
|
+
self.recording = False
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def push_(self, sample): # noqa: ANN001, ANN201, N802
|
|
184
|
+
_label, recording, level = sample
|
|
185
|
+
level = max(0.0, min(1.0, float(level)))
|
|
186
|
+
# Gentle attack, slow release so the mouth and motion change
|
|
187
|
+
# gradually rather than twitching frame-to-frame.
|
|
188
|
+
k = 0.4 if level > self.amp else 0.10
|
|
189
|
+
self.amp += k * (level - self.amp)
|
|
190
|
+
self.phase += _PHASE_SPEED * speed
|
|
191
|
+
self.frame_no += 1
|
|
192
|
+
self.recording = bool(recording)
|
|
193
|
+
self.setNeedsDisplay_(True)
|
|
194
|
+
|
|
195
|
+
def drawRect_(self, rect): # noqa: ANN001, ANN201, N802
|
|
196
|
+
import math
|
|
197
|
+
|
|
198
|
+
b = self.bounds()
|
|
199
|
+
cx, cy = b.size.width / 2, b.size.height / 2
|
|
200
|
+
gw = min(b.size.width, b.size.height) * 0.22
|
|
201
|
+
amp = self.amp
|
|
202
|
+
ph = self.phase
|
|
203
|
+
|
|
204
|
+
# Slow, gentle motion — a gradual drift, not a vibration.
|
|
205
|
+
# Bob and sway run at DIFFERENT slow frequencies so the
|
|
206
|
+
# ghost describes a lazy figure-eight rather than a rigid
|
|
207
|
+
# vertical bounce; both grow only mildly with volume.
|
|
208
|
+
bob = (3.0 + 3.0 * amp) * math.sin(ph * 0.9)
|
|
209
|
+
sway = (3.0 + 4.5 * amp) * math.sin(ph * 0.6 + 0.8)
|
|
210
|
+
dcx, dcy = cx + sway, cy + bob
|
|
211
|
+
# The whole face gently breathes and EXPANDS with volume
|
|
212
|
+
# (uniform scale — grows/shrinks, no shape distortion), plus
|
|
213
|
+
# a soft left/right lean. That is the head's audio reaction.
|
|
214
|
+
scale = (1.0 + 0.03 * math.sin(ph * 0.7)) * (1.0 + 0.11 * amp)
|
|
215
|
+
sx = sy = scale
|
|
216
|
+
lean = (2.5 + 3.0 * amp) * math.sin(ph * 0.45) # degrees
|
|
217
|
+
|
|
218
|
+
NSGraphicsContext.currentContext().saveGraphicsState()
|
|
219
|
+
t = NSAffineTransform.transform()
|
|
220
|
+
t.translateXBy_yBy_(dcx, dcy)
|
|
221
|
+
t.rotateByDegrees_(lean)
|
|
222
|
+
t.scaleXBy_yBy_(sx, sy)
|
|
223
|
+
t.translateXBy_yBy_(-dcx, -dcy)
|
|
224
|
+
t.concat()
|
|
225
|
+
try:
|
|
226
|
+
# Slow, spatially-coherent jelly flex so the head is
|
|
227
|
+
# visibly non-rigid; more when talking, and scaled by the
|
|
228
|
+
# user's overlay_flex.
|
|
229
|
+
jelly = (2.6 + 5.0 * amp) * flex
|
|
230
|
+
|
|
231
|
+
# soft halo for presence (flexes WITH the body)
|
|
232
|
+
halo = _mk_ghost(dcx, dcy, gw * 1.10, jelly, ph)
|
|
233
|
+
NSColor.colorWithCalibratedRed_green_blue_alpha_(
|
|
234
|
+
0.30, 0.62, 1.0, 0.20
|
|
235
|
+
).setFill()
|
|
236
|
+
halo.fill()
|
|
237
|
+
|
|
238
|
+
# body: bluish radial gradient + bright rim
|
|
239
|
+
body = _mk_ghost(dcx, dcy, gw, jelly, ph)
|
|
240
|
+
core = NSColor.colorWithCalibratedRed_green_blue_alpha_(
|
|
241
|
+
0.62, 0.86, 1.0, 0.88
|
|
242
|
+
)
|
|
243
|
+
edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
|
|
244
|
+
0.16, 0.44, 0.98, 0.74
|
|
245
|
+
)
|
|
246
|
+
grad = (
|
|
247
|
+
NSGradient.alloc().initWithStartingColor_endingColor_(
|
|
248
|
+
core, edge
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
if grad is not None:
|
|
252
|
+
grad.drawInBezierPath_relativeCenterPosition_(
|
|
253
|
+
body, (0.0, 0.35)
|
|
254
|
+
)
|
|
255
|
+
else: # pragma: no cover
|
|
256
|
+
edge.setFill()
|
|
257
|
+
body.fill()
|
|
258
|
+
NSColor.colorWithCalibratedRed_green_blue_alpha_(
|
|
259
|
+
0.80, 0.94, 1.0, 0.9
|
|
260
|
+
).setStroke()
|
|
261
|
+
body.setLineWidth_(2.0)
|
|
262
|
+
body.stroke()
|
|
263
|
+
|
|
264
|
+
# --- face features, CLIPPED to the body so nothing (the
|
|
265
|
+
# mouth in particular) can ever spill past the silhouette
|
|
266
|
+
gc = NSGraphicsContext.currentContext()
|
|
267
|
+
gc.saveGraphicsState()
|
|
268
|
+
body.addClip()
|
|
269
|
+
dark = NSColor.colorWithCalibratedRed_green_blue_alpha_(
|
|
270
|
+
0.05, 0.10, 0.26, 0.92
|
|
271
|
+
)
|
|
272
|
+
dark.setFill()
|
|
273
|
+
eye_y = dcy + gw * 0.45
|
|
274
|
+
eye_dx = gw * 0.42
|
|
275
|
+
blinking = (
|
|
276
|
+
self.frame_no % _BLINK_EVERY
|
|
277
|
+
) < _BLINK_FRAMES
|
|
278
|
+
eye_w = gw * 0.26
|
|
279
|
+
eye_h = gw * 0.06 if blinking else gw * 0.36
|
|
280
|
+
for s in (-1, 1):
|
|
281
|
+
_mk_oval(
|
|
282
|
+
dcx + s * eye_dx, eye_y, eye_w, eye_h
|
|
283
|
+
).fill()
|
|
284
|
+
_mk_oval(dcx, dcy + gw * 0.05, gw * 0.10, gw * 0.12).fill()
|
|
285
|
+
lip_y = dcy - gw * 0.18
|
|
286
|
+
_mk_mouth(dcx, lip_y, gw, amp, ph).fill()
|
|
287
|
+
gc.restoreGraphicsState()
|
|
288
|
+
finally:
|
|
289
|
+
NSGraphicsContext.currentContext().restoreGraphicsState()
|
|
290
|
+
|
|
291
|
+
class Driver(AppKit.NSObject):
|
|
292
|
+
"""NSTimer target: samples audio, ticks the app, ends the loop.
|
|
293
|
+
|
|
294
|
+
The pill is shown ONLY while recording — its mere presence is
|
|
295
|
+
the "you left the mic on" indicator; absence means not
|
|
296
|
+
listening. (A color change alone proved too subtle.)
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def initWithView_panel_(self, view, panel): # noqa: ANN001, ANN201, N802, E501
|
|
300
|
+
self = objc.super(Driver, self).init()
|
|
301
|
+
if self is None:
|
|
302
|
+
return None
|
|
303
|
+
self.view = view
|
|
304
|
+
self.panel = panel
|
|
305
|
+
self.shown = False
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def tick_(self, _timer): # noqa: ANN001, ANN201, N802
|
|
309
|
+
try:
|
|
310
|
+
tick()
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
try:
|
|
314
|
+
data = sample()
|
|
315
|
+
recording = bool(data[1])
|
|
316
|
+
if recording != self.shown:
|
|
317
|
+
if recording:
|
|
318
|
+
self.panel.orderFrontRegardless()
|
|
319
|
+
else:
|
|
320
|
+
self.panel.orderOut_(None)
|
|
321
|
+
self.shown = recording
|
|
322
|
+
if recording:
|
|
323
|
+
self.view.push_(data)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
if stopped():
|
|
327
|
+
NSApplication.sharedApplication().stop_(None)
|
|
328
|
+
# stop_() only takes effect once an event is processed;
|
|
329
|
+
# post a no-op event so the run loop wakes immediately.
|
|
330
|
+
evt = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( # noqa: E501
|
|
331
|
+
AppKit.NSEventTypeApplicationDefined,
|
|
332
|
+
(0, 0), 0, 0.0, 0, None, 0, 0, 0,
|
|
333
|
+
)
|
|
334
|
+
NSApplication.sharedApplication().postEvent_atStart_(
|
|
335
|
+
evt, True
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
app = NSApplication.sharedApplication()
|
|
339
|
+
# Accessory: no Dock icon, no menu bar, but windows still display.
|
|
340
|
+
app.setActivationPolicy_(
|
|
341
|
+
AppKit.NSApplicationActivationPolicyAccessory
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
screen = NSScreen.mainScreen()
|
|
345
|
+
sframe = screen.frame() if screen is not None else NSMakeRect(
|
|
346
|
+
0, 0, 1440, 900
|
|
347
|
+
)
|
|
348
|
+
x = sframe.origin.x + (sframe.size.width - _WIDTH) / 2
|
|
349
|
+
y = sframe.origin.y + _MARGIN_BOTTOM
|
|
350
|
+
panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
351
|
+
NSMakeRect(x, y, _WIDTH, _HEIGHT),
|
|
352
|
+
AppKit.NSWindowStyleMaskBorderless
|
|
353
|
+
| AppKit.NSWindowStyleMaskNonactivatingPanel,
|
|
354
|
+
NSBackingStoreBuffered,
|
|
355
|
+
False,
|
|
356
|
+
)
|
|
357
|
+
panel.setLevel_(AppKit.NSStatusWindowLevel)
|
|
358
|
+
panel.setOpaque_(False)
|
|
359
|
+
panel.setBackgroundColor_(NSColor.clearColor())
|
|
360
|
+
panel.setIgnoresMouseEvents_(True)
|
|
361
|
+
panel.setHasShadow_(True)
|
|
362
|
+
panel.setCollectionBehavior_(
|
|
363
|
+
AppKit.NSWindowCollectionBehaviorCanJoinAllSpaces
|
|
364
|
+
| AppKit.NSWindowCollectionBehaviorStationary
|
|
365
|
+
)
|
|
366
|
+
view = WaveView.alloc().initWithFrame_(
|
|
367
|
+
NSMakeRect(0, 0, _WIDTH, _HEIGHT)
|
|
368
|
+
)
|
|
369
|
+
panel.setContentView_(view)
|
|
370
|
+
# Hidden at startup; the driver shows it only while recording.
|
|
371
|
+
|
|
372
|
+
driver = Driver.alloc().initWithView_panel_(view, panel)
|
|
373
|
+
timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( # noqa: E501
|
|
374
|
+
_TICK_SECONDS, driver, b"tick:", None, True
|
|
375
|
+
)
|
|
376
|
+
AppKit.NSRunLoop.currentRunLoop().addTimer_forMode_(
|
|
377
|
+
timer, AppKit.NSRunLoopCommonModes
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Ctrl+C: AppKit's run loop doesn't deliver KeyboardInterrupt on
|
|
381
|
+
# its own. Record the signal in a flag the timer polls (the timer
|
|
382
|
+
# firing is what lets the Python-level handler run at all), end
|
|
383
|
+
# the loop, and re-raise KeyboardInterrupt for the caller.
|
|
384
|
+
sigint = {"hit": False}
|
|
385
|
+
caller_stopped = stopped
|
|
386
|
+
|
|
387
|
+
def stopped() -> bool: # noqa: ANN202
|
|
388
|
+
return sigint["hit"] or caller_stopped()
|
|
389
|
+
|
|
390
|
+
previous_handler = signal.getsignal(signal.SIGINT)
|
|
391
|
+
signal.signal(
|
|
392
|
+
signal.SIGINT,
|
|
393
|
+
lambda _sig, _frame: sigint.__setitem__("hit", True),
|
|
394
|
+
)
|
|
395
|
+
try:
|
|
396
|
+
app.run()
|
|
397
|
+
finally:
|
|
398
|
+
signal.signal(signal.SIGINT, previous_handler)
|
|
399
|
+
timer.invalidate()
|
|
400
|
+
panel.orderOut_(None)
|
|
401
|
+
if sigint["hit"]:
|
|
402
|
+
raise KeyboardInterrupt
|