talktype 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # Core app
2
+ DICTATE_BACKEND=parakeet
3
+ DICTATE_HOTKEY=<cmd>+<shift>+<space>
4
+ DICTATE_QUIT_HOTKEY=<cmd>+<shift>+q
5
+ DICTATE_AUTOPASTE=true
6
+ DICTATE_SAMPLE_RATE=16000
7
+ DICTATE_CHANNELS=1
8
+ DICTATE_MIN_SECONDS=0.35
9
+
10
+ # Local backend (Apple Silicon)
11
+ PARAKEET_MODEL=mlx-community/parakeet-tdt-0.6b-v3
12
+
13
+ # Cloud fallback
14
+ OPENAI_API_KEY=
15
+ OPENAI_TRANSCRIBE_MODEL=gpt-4o-transcribe
16
+ OPENAI_TRANSCRIBE_LANGUAGE=
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: talktype
3
+ Version: 0.1.0
4
+ Summary: Local-first dictation for macOS — press a hotkey, talk, text appears at your cursor
5
+ Project-URL: Homepage, https://github.com/strangeloopcanon/talk
6
+ Project-URL: Repository, https://github.com/strangeloopcanon/talk
7
+ Project-URL: Issues, https://github.com/strangeloopcanon/talk/issues
8
+ Author: Rohit Krishnan
9
+ License: MIT
10
+ Keywords: dictation,macos,mlx,parakeet,speech-to-text,transcription
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: numpy>=1.26
22
+ Requires-Dist: openai>=1.108.0
23
+ Requires-Dist: parakeet-mlx>=0.4.2
24
+ Requires-Dist: pynput>=1.8.1
25
+ Requires-Dist: python-dotenv>=1.1.1
26
+ Requires-Dist: sounddevice>=0.5.2
27
+ Description-Content-Type: text/markdown
28
+
29
+ # talk
30
+
31
+ > **macOS only** (Apple Silicon recommended). Requires Python 3.11+.
32
+
33
+ Local-first dictation for macOS.
34
+ Press a hotkey, talk, text appears at your cursor.
35
+
36
+ - Parakeet local transcription by default (zero API cost on Apple Silicon).
37
+ - OpenAI `gpt-4o-transcribe` fallback with one env switch.
38
+ - Automatic paste at cursor (clipboard-safe restore).
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install talk
44
+ ```
45
+
46
+ That's it. On first run, Parakeet model weights (~1.2 GB) download automatically.
47
+
48
+ ### Alternative: from source
49
+
50
+ ```bash
51
+ git clone https://github.com/strangeloopcanon/talk.git
52
+ cd talk
53
+ ./scripts/install_macos.sh
54
+ ./scripts/doctor_macos.sh
55
+ ./scripts/run_macos.sh
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ```bash
61
+ talk run
62
+ ```
63
+
64
+ - Press `Cmd+Shift+Space` to start recording.
65
+ - Press `Cmd+Shift+Space` again to stop, transcribe, and paste.
66
+ - Press `Cmd+Shift+Q` to quit.
67
+
68
+ ### Preflight checks
69
+
70
+ ```bash
71
+ talk doctor
72
+ ```
73
+
74
+ ### File transcription
75
+
76
+ ```bash
77
+ talk transcribe-file /path/to/sample.wav
78
+ ```
79
+
80
+ ## macOS permissions
81
+
82
+ You must grant your terminal app:
83
+
84
+ - **Microphone** -- for audio recording
85
+ - **Accessibility** -- for paste keystroke automation
86
+ - **Input Monitoring** -- for global hotkeys
87
+
88
+ If paste fails but transcription works, Accessibility permission is usually the issue.
89
+
90
+ ## Configuration
91
+
92
+ Works out of the box with zero configuration. To customize, create `~/.config/talk/.env`:
93
+
94
+ ```bash
95
+ mkdir -p ~/.config/talk
96
+ ```
97
+
98
+ Key options (all have sensible defaults):
99
+
100
+ | Variable | Default | Notes |
101
+ |----------|---------|-------|
102
+ | `DICTATE_BACKEND` | `parakeet` | `parakeet` (local) or `openai` (cloud) |
103
+ | `DICTATE_HOTKEY` | `<cmd>+<shift>+<space>` | Toggle recording |
104
+ | `DICTATE_QUIT_HOTKEY` | `<cmd>+<shift>+q` | Quit the app |
105
+ | `DICTATE_AUTOPASTE` | `true` | Paste transcription at cursor |
106
+ | `PARAKEET_MODEL` | `mlx-community/parakeet-tdt-0.6b-v3` | Local model |
107
+ | `OPENAI_API_KEY` | *(empty)* | Required if backend=openai |
108
+
109
+ See `.env.example` for the full list.
110
+
111
+ ## Switching backend
112
+
113
+ ```bash
114
+ # In ~/.config/talk/.env or as env vars:
115
+ DICTATE_BACKEND=openai
116
+ OPENAI_API_KEY=sk-...
117
+ ```
118
+
119
+ ## Cleanup
120
+
121
+ Parakeet model weights are cached in `~/.cache/huggingface/hub/`. To reclaim disk space:
122
+
123
+ ```bash
124
+ rm -rf ~/.cache/huggingface/hub/models--mlx-community--parakeet-tdt-0.6b-v3
125
+ ```
talktype-0.1.0/PLAN.md ADDED
@@ -0,0 +1,35 @@
1
+ # Dictation Build Plan (Executed)
2
+
3
+ ## Human-friendly plan
4
+
5
+ Build a simple, reliable "press hotkey, talk, text appears" loop that feels close to WisprFlow for laptop use.
6
+
7
+ - Prioritize **local transcription first** so per-use cost is effectively zero.
8
+ - Keep a **cloud fallback** for quality and edge cases.
9
+ - Make setup short enough to run in minutes.
10
+ - Avoid heavy UI work until the core dictation loop is proven.
11
+
12
+ ## Task-focused plan
13
+
14
+ 1. Research model/API options and choose architecture.
15
+ 2. Scaffold Python app with environment-based config.
16
+ 3. Implement global hotkey + microphone recorder.
17
+ 4. Implement local Parakeet backend.
18
+ 5. Implement OpenAI backend fallback.
19
+ 6. Implement auto-paste insertion into active app on macOS.
20
+ 7. Add docs and `.env.example`.
21
+ 8. Smoke-test both backends with a sample WAV clip.
22
+ 9. Verify live app starts and reports permission requirements clearly.
23
+
24
+ ## Status
25
+
26
+ All steps above are complete in this repo.
27
+
28
+ ## Onboarding hardening (completed)
29
+
30
+ To make this easy for other laptop users, the repo now includes:
31
+
32
+ 1. `scripts/install_macos.sh` for one-command setup.
33
+ 2. `scripts/doctor_macos.sh` for preflight checks.
34
+ 3. `scripts/run_macos.sh` to launch dictation.
35
+ 4. README guidance that starts with the script-based flow.
@@ -0,0 +1,97 @@
1
+ # talk
2
+
3
+ > **macOS only** (Apple Silicon recommended). Requires Python 3.11+.
4
+
5
+ Local-first dictation for macOS.
6
+ Press a hotkey, talk, text appears at your cursor.
7
+
8
+ - Parakeet local transcription by default (zero API cost on Apple Silicon).
9
+ - OpenAI `gpt-4o-transcribe` fallback with one env switch.
10
+ - Automatic paste at cursor (clipboard-safe restore).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install talk
16
+ ```
17
+
18
+ That's it. On first run, Parakeet model weights (~1.2 GB) download automatically.
19
+
20
+ ### Alternative: from source
21
+
22
+ ```bash
23
+ git clone https://github.com/strangeloopcanon/talk.git
24
+ cd talk
25
+ ./scripts/install_macos.sh
26
+ ./scripts/doctor_macos.sh
27
+ ./scripts/run_macos.sh
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ talk run
34
+ ```
35
+
36
+ - Press `Cmd+Shift+Space` to start recording.
37
+ - Press `Cmd+Shift+Space` again to stop, transcribe, and paste.
38
+ - Press `Cmd+Shift+Q` to quit.
39
+
40
+ ### Preflight checks
41
+
42
+ ```bash
43
+ talk doctor
44
+ ```
45
+
46
+ ### File transcription
47
+
48
+ ```bash
49
+ talk transcribe-file /path/to/sample.wav
50
+ ```
51
+
52
+ ## macOS permissions
53
+
54
+ You must grant your terminal app:
55
+
56
+ - **Microphone** -- for audio recording
57
+ - **Accessibility** -- for paste keystroke automation
58
+ - **Input Monitoring** -- for global hotkeys
59
+
60
+ If paste fails but transcription works, Accessibility permission is usually the issue.
61
+
62
+ ## Configuration
63
+
64
+ Works out of the box with zero configuration. To customize, create `~/.config/talk/.env`:
65
+
66
+ ```bash
67
+ mkdir -p ~/.config/talk
68
+ ```
69
+
70
+ Key options (all have sensible defaults):
71
+
72
+ | Variable | Default | Notes |
73
+ |----------|---------|-------|
74
+ | `DICTATE_BACKEND` | `parakeet` | `parakeet` (local) or `openai` (cloud) |
75
+ | `DICTATE_HOTKEY` | `<cmd>+<shift>+<space>` | Toggle recording |
76
+ | `DICTATE_QUIT_HOTKEY` | `<cmd>+<shift>+q` | Quit the app |
77
+ | `DICTATE_AUTOPASTE` | `true` | Paste transcription at cursor |
78
+ | `PARAKEET_MODEL` | `mlx-community/parakeet-tdt-0.6b-v3` | Local model |
79
+ | `OPENAI_API_KEY` | *(empty)* | Required if backend=openai |
80
+
81
+ See `.env.example` for the full list.
82
+
83
+ ## Switching backend
84
+
85
+ ```bash
86
+ # In ~/.config/talk/.env or as env vars:
87
+ DICTATE_BACKEND=openai
88
+ OPENAI_API_KEY=sk-...
89
+ ```
90
+
91
+ ## Cleanup
92
+
93
+ Parakeet model weights are cached in `~/.cache/huggingface/hub/`. To reclaim disk space:
94
+
95
+ ```bash
96
+ rm -rf ~/.cache/huggingface/hub/models--mlx-community--parakeet-tdt-0.6b-v3
97
+ ```
@@ -0,0 +1,15 @@
1
+ # Research Notes (February 25, 2026)
2
+
3
+ ## Sources used
4
+
5
+ - OpenAI blog: [Introducing next-generation audio models](https://openai.com/index/introducing-our-next-generation-audio-models/)
6
+ - OpenAI Python SDK README (audio transcription examples): [openai/openai-python](https://github.com/openai/openai-python)
7
+ - NVIDIA model card (Parakeet CTC 1.1B): [nvidia/parakeet-ctc-1.1b](https://huggingface.co/nvidia/parakeet-ctc-1.1b)
8
+ - NVIDIA model card (Parakeet TDT 0.6B v2): [nvidia/parakeet-tdt-0.6b-v2](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2)
9
+ - Parakeet MLX implementation docs/repo: [Parakeet-MLX](https://github.com/senstella/parakeet-mlx)
10
+
11
+ ## Key decisions
12
+
13
+ - Use **Parakeet locally by default** for near-zero marginal cost dictation on Apple Silicon.
14
+ - Use **OpenAI `gpt-4o-transcribe`** as a switchable fallback backend.
15
+ - Keep backend swapping to one env var (`DICTATE_BACKEND`) so the same UX works across engines.
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "talktype"
3
+ version = "0.1.0"
4
+ description = "Local-first dictation for macOS — press a hotkey, talk, text appears at your cursor"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = {text = "MIT"}
8
+ authors = [{name = "Rohit Krishnan"}]
9
+ keywords = ["dictation", "speech-to-text", "transcription", "macos", "parakeet", "mlx"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: MacOS X",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: MacOS",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
20
+ ]
21
+ dependencies = [
22
+ "numpy>=1.26",
23
+ "openai>=1.108.0",
24
+ "parakeet-mlx>=0.4.2",
25
+ "pynput>=1.8.1",
26
+ "python-dotenv>=1.1.1",
27
+ "sounddevice>=0.5.2",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/strangeloopcanon/talk"
32
+ Repository = "https://github.com/strangeloopcanon/talk"
33
+ Issues = "https://github.com/strangeloopcanon/talk/issues"
34
+
35
+ [project.scripts]
36
+ talk = "talk.__main__:main"
37
+
38
+ [build-system]
39
+ requires = ["hatchling>=1.24.2"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/talk"]
44
+
45
+ [tool.uv]
46
+ package = true
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ uv run talk doctor
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ if [[ "$(uname -s)" != "Darwin" ]]; then
8
+ echo "[error] This installer currently targets macOS." >&2
9
+ exit 1
10
+ fi
11
+
12
+ if ! command -v uv >/dev/null 2>&1; then
13
+ echo "[error] 'uv' is required but not installed." >&2
14
+ if command -v brew >/dev/null 2>&1; then
15
+ echo "Install it with: brew install uv" >&2
16
+ else
17
+ echo "Install instructions: https://docs.astral.sh/uv/getting-started/installation/" >&2
18
+ fi
19
+ exit 1
20
+ fi
21
+
22
+ python_version="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)"
23
+ if [[ -z "$python_version" ]]; then
24
+ echo "[error] Python 3 not found. Install Python 3.11+ first." >&2
25
+ exit 1
26
+ fi
27
+ python_minor="${python_version#*.}"
28
+ if [[ "$python_minor" -lt 11 ]]; then
29
+ echo "[error] Python >= 3.11 required (found $python_version)." >&2
30
+ echo "Install a newer Python: brew install python@3.13" >&2
31
+ exit 1
32
+ fi
33
+ echo "[setup] Python $python_version detected."
34
+
35
+ if [[ ! -f .env ]]; then
36
+ cp .env.example .env
37
+ echo "[setup] Created .env from .env.example"
38
+ fi
39
+
40
+ ensure_key_default() {
41
+ local key="$1"
42
+ local value="$2"
43
+ if ! grep -q "^${key}=" .env; then
44
+ printf '%s=%s\n' "$key" "$value" >> .env
45
+ echo "[setup] Added default ${key}=${value}"
46
+ fi
47
+ }
48
+
49
+ # Keep Parakeet as the default for fresh installs.
50
+ ensure_key_default "DICTATE_BACKEND" "parakeet"
51
+ ensure_key_default "DICTATE_HOTKEY" "<cmd>+<shift>+<space>"
52
+ ensure_key_default "DICTATE_QUIT_HOTKEY" "<cmd>+<shift>+q"
53
+ ensure_key_default "DICTATE_AUTOPASTE" "true"
54
+ ensure_key_default "PARAKEET_MODEL" "mlx-community/parakeet-tdt-0.6b-v3"
55
+
56
+ uv sync
57
+
58
+ echo ""
59
+ echo "[done] Installation complete."
60
+ echo "Next steps:"
61
+ echo " 1) ./scripts/doctor_macos.sh"
62
+ echo " 2) ./scripts/run_macos.sh"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ uv run talk run
@@ -0,0 +1,4 @@
1
+ """talk — local-first dictation for macOS."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from talk import __version__
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(
11
+ prog="talk",
12
+ description="Local-first laptop dictation with a global hotkey.",
13
+ )
14
+ parser.add_argument(
15
+ "--version", action="version", version=f"%(prog)s {__version__}",
16
+ )
17
+ sub = parser.add_subparsers(dest="command", required=True)
18
+
19
+ sub.add_parser("run", help="Start the live dictation app")
20
+ sub.add_parser("doctor", help="Run setup preflight checks")
21
+
22
+ file_parser = sub.add_parser("transcribe-file", help="Transcribe a WAV file")
23
+ file_parser.add_argument("path", help="Path to a 16-bit PCM WAV file")
24
+ file_parser.add_argument(
25
+ "--paste",
26
+ action="store_true",
27
+ help="Paste transcription at cursor after transcription",
28
+ )
29
+
30
+ return parser
31
+
32
+
33
+ def _cmd_run() -> None:
34
+ from talk.app import DictationApp
35
+ from talk.config import load_settings
36
+
37
+ settings = load_settings()
38
+ app = DictationApp(settings)
39
+ app.run()
40
+
41
+
42
+ def _cmd_doctor() -> None:
43
+ from talk.config import load_settings
44
+ from talk.doctor import run_doctor
45
+
46
+ settings = load_settings()
47
+ code = run_doctor(settings)
48
+ if code:
49
+ sys.exit(code)
50
+
51
+
52
+ def _cmd_transcribe_file(path: str, paste: bool) -> None:
53
+ from talk.audio import load_wav_mono
54
+ from talk.backends.factory import build_backend
55
+ from talk.config import load_settings
56
+ from talk.paste import paste_text
57
+
58
+ settings = load_settings()
59
+ backend = build_backend(settings)
60
+ chunk = load_wav_mono(path)
61
+ text = backend.transcribe(chunk.samples, chunk.sample_rate).strip()
62
+
63
+ if not text:
64
+ print("[warn] No transcription text produced.")
65
+ sys.exit(1)
66
+
67
+ print(text)
68
+ if paste:
69
+ paste_text(text)
70
+
71
+
72
+ def main() -> None:
73
+ parser = build_parser()
74
+ args = parser.parse_args()
75
+
76
+ if args.command == "run":
77
+ _cmd_run()
78
+ elif args.command == "doctor":
79
+ _cmd_doctor()
80
+ elif args.command == "transcribe-file":
81
+ _cmd_transcribe_file(args.path, args.paste)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import signal
4
+ import subprocess
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from pynput import keyboard
10
+
11
+ from talk.audio import AudioChunk, MicRecorder, RecorderError
12
+ from talk.backends.factory import build_backend
13
+ from talk.config import Settings
14
+ from talk.paste import paste_text
15
+
16
+
17
+ def _play_sound(filename: str) -> None:
18
+ sound_path = Path("/System/Library/Sounds") / filename
19
+ if not sound_path.exists():
20
+ return
21
+ subprocess.Popen(
22
+ ["afplay", str(sound_path)],
23
+ stdout=subprocess.DEVNULL,
24
+ stderr=subprocess.DEVNULL,
25
+ )
26
+
27
+
28
+ class DictationApp:
29
+ def __init__(self, settings: Settings) -> None:
30
+ self.settings = settings
31
+ self.backend = build_backend(settings)
32
+ self.recorder = MicRecorder(
33
+ sample_rate=settings.sample_rate,
34
+ channels=settings.channels,
35
+ )
36
+
37
+ self._lock = threading.Lock()
38
+ self._stop_event = threading.Event()
39
+ self._is_recording = False
40
+ self._is_transcribing = False
41
+
42
+ def _toggle_recording(self) -> None:
43
+ chunk: AudioChunk | None = None
44
+
45
+ with self._lock:
46
+ if self._is_transcribing:
47
+ print("[busy] Still transcribing previous clip.")
48
+ return
49
+
50
+ if not self._is_recording:
51
+ try:
52
+ self.recorder.start()
53
+ except Exception as exc: # noqa: BLE001
54
+ print(f"[error] Could not start recording: {exc}")
55
+ return
56
+
57
+ self._is_recording = True
58
+ _play_sound("Glass.aiff")
59
+ print("[rec] Recording... press hotkey again to stop.")
60
+ return
61
+
62
+ try:
63
+ chunk = self.recorder.stop()
64
+ except RecorderError as exc:
65
+ print(f"[error] Could not stop recording cleanly: {exc}")
66
+ self._is_recording = False
67
+ return
68
+
69
+ self._is_recording = False
70
+ self._is_transcribing = True
71
+
72
+ _play_sound("Pop.aiff")
73
+ worker = threading.Thread(
74
+ target=self._transcribe_and_emit,
75
+ args=(chunk,),
76
+ daemon=True,
77
+ )
78
+ worker.start()
79
+
80
+ def _transcribe_and_emit(self, chunk: AudioChunk) -> None:
81
+ try:
82
+ duration_seconds = 0.0
83
+ if chunk.sample_rate > 0:
84
+ duration_seconds = len(chunk.samples) / float(chunk.sample_rate)
85
+
86
+ if duration_seconds < self.settings.min_seconds:
87
+ print("[skip] Clip too short. Try speaking a bit longer.")
88
+ return
89
+
90
+ text = self.backend.transcribe(chunk.samples, chunk.sample_rate).strip()
91
+ if not text:
92
+ print("[skip] No speech detected.")
93
+ return
94
+
95
+ print(f"[text] {text}")
96
+ if self.settings.autopaste:
97
+ paste_text(text)
98
+ print("[paste] Inserted at current cursor.")
99
+ except Exception as exc: # noqa: BLE001
100
+ print(f"[error] Transcription failed: {exc}")
101
+ finally:
102
+ with self._lock:
103
+ self._is_transcribing = False
104
+
105
+ def _request_shutdown(self) -> None:
106
+ with self._lock:
107
+ if self._is_recording:
108
+ try:
109
+ self.recorder.stop()
110
+ except Exception: # noqa: BLE001
111
+ pass
112
+ self._is_recording = False
113
+ print("[exit] Shutting down dictation app.")
114
+ self._stop_event.set()
115
+
116
+ def run(self) -> None:
117
+ print(f"[ready] Backend: {self.backend.name}")
118
+ print(f"[ready] Toggle dictation: {self.settings.hotkey}")
119
+ print(f"[ready] Quit app: {self.settings.quit_hotkey}")
120
+
121
+ keymap = {
122
+ self.settings.hotkey: self._toggle_recording,
123
+ self.settings.quit_hotkey: self._request_shutdown,
124
+ }
125
+
126
+ signal.signal(signal.SIGTERM, lambda *_: self._request_shutdown())
127
+
128
+ listener = keyboard.GlobalHotKeys(keymap)
129
+ listener.start()
130
+ try:
131
+ while not self._stop_event.is_set():
132
+ time.sleep(0.15)
133
+ except KeyboardInterrupt:
134
+ self._request_shutdown()
135
+ finally:
136
+ listener.stop()
137
+