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.
- talktype-0.1.0/.env.example +16 -0
- talktype-0.1.0/.gitignore +4 -0
- talktype-0.1.0/PKG-INFO +125 -0
- talktype-0.1.0/PLAN.md +35 -0
- talktype-0.1.0/README.md +97 -0
- talktype-0.1.0/RESEARCH.md +15 -0
- talktype-0.1.0/pyproject.toml +46 -0
- talktype-0.1.0/scripts/doctor_macos.sh +7 -0
- talktype-0.1.0/scripts/install_macos.sh +62 -0
- talktype-0.1.0/scripts/run_macos.sh +7 -0
- talktype-0.1.0/src/talk/__init__.py +4 -0
- talktype-0.1.0/src/talk/__main__.py +85 -0
- talktype-0.1.0/src/talk/app.py +137 -0
- talktype-0.1.0/src/talk/audio.py +107 -0
- talktype-0.1.0/src/talk/backends/__init__.py +1 -0
- talktype-0.1.0/src/talk/backends/base.py +12 -0
- talktype-0.1.0/src/talk/backends/factory.py +24 -0
- talktype-0.1.0/src/talk/backends/openai_backend.py +31 -0
- talktype-0.1.0/src/talk/backends/parakeet_backend.py +29 -0
- talktype-0.1.0/src/talk/config.py +94 -0
- talktype-0.1.0/src/talk/doctor.py +226 -0
- talktype-0.1.0/src/talk/paste.py +28 -0
- talktype-0.1.0/uv.lock +1557 -0
|
@@ -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=
|
talktype-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
talktype-0.1.0/README.md
ADDED
|
@@ -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,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,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
|
+
|