talktype 0.1.0__tar.gz → 0.2.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.
Files changed (44) hide show
  1. {talktype-0.1.0 → talktype-0.2.0}/.env.example +2 -2
  2. {talktype-0.1.0 → talktype-0.2.0}/.gitignore +3 -0
  3. {talktype-0.1.0 → talktype-0.2.0}/PKG-INFO +8 -7
  4. {talktype-0.1.0 → talktype-0.2.0}/README.md +6 -6
  5. talktype-0.2.0/Talk.spec +98 -0
  6. talktype-0.2.0/assets/Talk.icns +0 -0
  7. talktype-0.2.0/assets/Talk.iconset/icon_128x128.png +0 -0
  8. talktype-0.2.0/assets/Talk.iconset/icon_128x128@2x.png +0 -0
  9. talktype-0.2.0/assets/Talk.iconset/icon_16x16.png +0 -0
  10. talktype-0.2.0/assets/Talk.iconset/icon_16x16@2x.png +0 -0
  11. talktype-0.2.0/assets/Talk.iconset/icon_256x256.png +0 -0
  12. talktype-0.2.0/assets/Talk.iconset/icon_256x256@2x.png +0 -0
  13. talktype-0.2.0/assets/Talk.iconset/icon_32x32.png +0 -0
  14. talktype-0.2.0/assets/Talk.iconset/icon_32x32@2x.png +0 -0
  15. talktype-0.2.0/assets/Talk.iconset/icon_512x512.png +0 -0
  16. talktype-0.2.0/assets/Talk.iconset/icon_512x512@2x.png +0 -0
  17. talktype-0.2.0/assets/Talk.iconset/icon_64x64.png +0 -0
  18. talktype-0.2.0/assets/Talk.iconset/icon_64x64@2x.png +0 -0
  19. talktype-0.2.0/assets/Talk_icon_512.png +0 -0
  20. talktype-0.2.0/assets/mic.png +0 -0
  21. talktype-0.2.0/assets/mic_rec.png +0 -0
  22. talktype-0.2.0/assets/mic_wait.png +0 -0
  23. talktype-0.2.0/entitlements.plist +16 -0
  24. {talktype-0.1.0 → talktype-0.2.0}/pyproject.toml +7 -1
  25. talktype-0.2.0/scripts/build_dmg.sh +109 -0
  26. {talktype-0.1.0 → talktype-0.2.0}/scripts/install_macos.sh +2 -2
  27. {talktype-0.1.0 → talktype-0.2.0}/src/talk/__init__.py +1 -1
  28. {talktype-0.1.0 → talktype-0.2.0}/src/talk/app.py +45 -8
  29. {talktype-0.1.0 → talktype-0.2.0}/src/talk/config.py +2 -2
  30. talktype-0.2.0/src/talk/menubar.py +79 -0
  31. {talktype-0.1.0 → talktype-0.2.0}/uv.lock +109 -1
  32. {talktype-0.1.0 → talktype-0.2.0}/PLAN.md +0 -0
  33. {talktype-0.1.0 → talktype-0.2.0}/RESEARCH.md +0 -0
  34. {talktype-0.1.0 → talktype-0.2.0}/scripts/doctor_macos.sh +0 -0
  35. {talktype-0.1.0 → talktype-0.2.0}/scripts/run_macos.sh +0 -0
  36. {talktype-0.1.0 → talktype-0.2.0}/src/talk/__main__.py +0 -0
  37. {talktype-0.1.0 → talktype-0.2.0}/src/talk/audio.py +0 -0
  38. {talktype-0.1.0 → talktype-0.2.0}/src/talk/backends/__init__.py +0 -0
  39. {talktype-0.1.0 → talktype-0.2.0}/src/talk/backends/base.py +0 -0
  40. {talktype-0.1.0 → talktype-0.2.0}/src/talk/backends/factory.py +0 -0
  41. {talktype-0.1.0 → talktype-0.2.0}/src/talk/backends/openai_backend.py +0 -0
  42. {talktype-0.1.0 → talktype-0.2.0}/src/talk/backends/parakeet_backend.py +0 -0
  43. {talktype-0.1.0 → talktype-0.2.0}/src/talk/doctor.py +0 -0
  44. {talktype-0.1.0 → talktype-0.2.0}/src/talk/paste.py +0 -0
@@ -1,7 +1,7 @@
1
1
  # Core app
2
2
  DICTATE_BACKEND=parakeet
3
- DICTATE_HOTKEY=<cmd>+<shift>+<space>
4
- DICTATE_QUIT_HOTKEY=<cmd>+<shift>+q
3
+ DICTATE_HOTKEY=<ctrl>+<alt>+d
4
+ DICTATE_QUIT_HOTKEY=<ctrl>+<alt>+q
5
5
  DICTATE_AUTOPASTE=true
6
6
  DICTATE_SAMPLE_RATE=16000
7
7
  DICTATE_CHANNELS=1
@@ -2,3 +2,6 @@
2
2
  __pycache__/
3
3
  *.pyc
4
4
  .env
5
+ build/
6
+ dist/
7
+ *.egg-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talktype
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Local-first dictation for macOS — press a hotkey, talk, text appears at your cursor
5
5
  Project-URL: Homepage, https://github.com/strangeloopcanon/talk
6
6
  Project-URL: Repository, https://github.com/strangeloopcanon/talk
@@ -23,6 +23,7 @@ Requires-Dist: openai>=1.108.0
23
23
  Requires-Dist: parakeet-mlx>=0.4.2
24
24
  Requires-Dist: pynput>=1.8.1
25
25
  Requires-Dist: python-dotenv>=1.1.1
26
+ Requires-Dist: rumps>=0.4.0
26
27
  Requires-Dist: sounddevice>=0.5.2
27
28
  Description-Content-Type: text/markdown
28
29
 
@@ -40,7 +41,7 @@ Press a hotkey, talk, text appears at your cursor.
40
41
  ## Install
41
42
 
42
43
  ```bash
43
- pip install talk
44
+ pip install talktype
44
45
  ```
45
46
 
46
47
  That's it. On first run, Parakeet model weights (~1.2 GB) download automatically.
@@ -61,9 +62,9 @@ cd talk
61
62
  talk run
62
63
  ```
63
64
 
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.
65
+ - Press `Ctrl+Option+D` to start recording.
66
+ - Press `Ctrl+Option+D` again to stop, transcribe, and paste.
67
+ - Press `Ctrl+Option+Q` to quit.
67
68
 
68
69
  ### Preflight checks
69
70
 
@@ -100,8 +101,8 @@ Key options (all have sensible defaults):
100
101
  | Variable | Default | Notes |
101
102
  |----------|---------|-------|
102
103
  | `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 |
104
+ | `DICTATE_HOTKEY` | `<ctrl>+<alt>+d` | Toggle recording |
105
+ | `DICTATE_QUIT_HOTKEY` | `<ctrl>+<alt>+q` | Quit the app |
105
106
  | `DICTATE_AUTOPASTE` | `true` | Paste transcription at cursor |
106
107
  | `PARAKEET_MODEL` | `mlx-community/parakeet-tdt-0.6b-v3` | Local model |
107
108
  | `OPENAI_API_KEY` | *(empty)* | Required if backend=openai |
@@ -12,7 +12,7 @@ Press a hotkey, talk, text appears at your cursor.
12
12
  ## Install
13
13
 
14
14
  ```bash
15
- pip install talk
15
+ pip install talktype
16
16
  ```
17
17
 
18
18
  That's it. On first run, Parakeet model weights (~1.2 GB) download automatically.
@@ -33,9 +33,9 @@ cd talk
33
33
  talk run
34
34
  ```
35
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.
36
+ - Press `Ctrl+Option+D` to start recording.
37
+ - Press `Ctrl+Option+D` again to stop, transcribe, and paste.
38
+ - Press `Ctrl+Option+Q` to quit.
39
39
 
40
40
  ### Preflight checks
41
41
 
@@ -72,8 +72,8 @@ Key options (all have sensible defaults):
72
72
  | Variable | Default | Notes |
73
73
  |----------|---------|-------|
74
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 |
75
+ | `DICTATE_HOTKEY` | `<ctrl>+<alt>+d` | Toggle recording |
76
+ | `DICTATE_QUIT_HOTKEY` | `<ctrl>+<alt>+q` | Quit the app |
77
77
  | `DICTATE_AUTOPASTE` | `true` | Paste transcription at cursor |
78
78
  | `PARAKEET_MODEL` | `mlx-community/parakeet-tdt-0.6b-v3` | Local model |
79
79
  | `OPENAI_API_KEY` | *(empty)* | Required if backend=openai |
@@ -0,0 +1,98 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+ """PyInstaller spec for Talk.app — macOS menu bar dictation."""
3
+
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from PyInstaller.utils.hooks import collect_all, collect_submodules
9
+
10
+ block_cipher = None
11
+
12
+ ROOT = os.path.dirname(os.path.abspath(SPEC)) # noqa: F821
13
+
14
+ # Heavy native packages that need special collection
15
+ mlx_datas, mlx_binaries, mlx_hiddenimports = collect_all("mlx")
16
+ parakeet_datas, parakeet_binaries, parakeet_hiddenimports = collect_all("parakeet_mlx")
17
+ numpy_datas, numpy_binaries, numpy_hiddenimports = collect_all("numpy")
18
+
19
+ extra_hiddenimports = (
20
+ collect_submodules("talk")
21
+ + collect_submodules("rumps")
22
+ + collect_submodules("pynput")
23
+ + collect_submodules("sounddevice")
24
+ + collect_submodules("dotenv")
25
+ + collect_submodules("openai")
26
+ + collect_submodules("huggingface_hub")
27
+ + mlx_hiddenimports
28
+ + parakeet_hiddenimports
29
+ + numpy_hiddenimports
30
+ + [
31
+ "pyobjc",
32
+ "AppKit",
33
+ "Foundation",
34
+ "objc",
35
+ "PyObjCTools",
36
+ ]
37
+ )
38
+
39
+ a = Analysis(
40
+ [os.path.join(ROOT, "src", "talk", "menubar.py")],
41
+ pathex=[os.path.join(ROOT, "src")],
42
+ binaries=mlx_binaries + parakeet_binaries + numpy_binaries,
43
+ datas=(
44
+ [(os.path.join(ROOT, "assets", f), ".") for f in ("mic.png", "mic_rec.png", "mic_wait.png")]
45
+ + mlx_datas
46
+ + parakeet_datas
47
+ + numpy_datas
48
+ ),
49
+ hiddenimports=extra_hiddenimports,
50
+ hookspath=[],
51
+ hooksconfig={},
52
+ runtime_hooks=[],
53
+ excludes=[],
54
+ win_no_prefer_redirects=False,
55
+ win_private_assemblies=False,
56
+ cipher=block_cipher,
57
+ noarchive=False,
58
+ )
59
+
60
+ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # noqa: F821
61
+
62
+ exe = EXE( # noqa: F821
63
+ pyz,
64
+ a.scripts,
65
+ [],
66
+ exclude_binaries=True,
67
+ name="Talk",
68
+ debug=False,
69
+ bootloader_ignore_signals=False,
70
+ strip=False,
71
+ upx=False,
72
+ console=False,
73
+ target_arch="arm64",
74
+ )
75
+
76
+ coll = COLLECT( # noqa: F821
77
+ exe,
78
+ a.binaries,
79
+ a.zipfiles,
80
+ a.datas,
81
+ strip=False,
82
+ upx=False,
83
+ name="Talk",
84
+ )
85
+
86
+ app = BUNDLE( # noqa: F821
87
+ coll,
88
+ name="Talk.app",
89
+ icon=os.path.join(ROOT, "assets", "Talk.icns"),
90
+ bundle_identifier="com.powrofyou.talk",
91
+ info_plist={
92
+ "LSUIElement": True,
93
+ "NSMicrophoneUsageDescription": "Talk needs microphone access for voice dictation.",
94
+ "CFBundleShortVersionString": "0.2.0",
95
+ "CFBundleName": "Talk",
96
+ "NSHighResolutionCapable": True,
97
+ },
98
+ )
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ <key>com.apple.security.cs.disable-library-validation</key>
10
+ <true/>
11
+ <key>com.apple.security.device.audio-input</key>
12
+ <true/>
13
+ <key>com.apple.security.automation.apple-events</key>
14
+ <true/>
15
+ </dict>
16
+ </plist>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "talktype"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Local-first dictation for macOS — press a hotkey, talk, text appears at your cursor"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "parakeet-mlx>=0.4.2",
25
25
  "pynput>=1.8.1",
26
26
  "python-dotenv>=1.1.1",
27
+ "rumps>=0.4.0",
27
28
  "sounddevice>=0.5.2",
28
29
  ]
29
30
 
@@ -44,3 +45,8 @@ packages = ["src/talk"]
44
45
 
45
46
  [tool.uv]
46
47
  package = true
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "pyinstaller>=6.19.0",
52
+ ]
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # Build Talk.app, code-sign it, package as DMG, notarize, and staple.
3
+ # Prerequisites:
4
+ # uv add --dev pyinstaller
5
+ # xcrun notarytool store-credentials "talk-notarize" --apple-id <email> --team-id 7NV8TSX4VN
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10
+
11
+ IDENTITY="Developer ID Application: Powr of You Ltd. (7NV8TSX4VN)"
12
+ KEYCHAIN_PROFILE="talk-notarize"
13
+ APP_NAME="Talk"
14
+ BUNDLE_ID="com.powrofyou.talk"
15
+
16
+ DIST="$ROOT/dist"
17
+ APP="$DIST/$APP_NAME.app"
18
+ DMG="$DIST/$APP_NAME.dmg"
19
+ ENTITLEMENTS="$ROOT/entitlements.plist"
20
+ SPEC="$ROOT/Talk.spec"
21
+
22
+ # ---- helpers ----------------------------------------------------------------
23
+ info() { printf '\033[1;34m→ %s\033[0m\n' "$*"; }
24
+ ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
25
+ die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
26
+
27
+ # ---- pre-flight checks -----------------------------------------------------
28
+ command -v pyinstaller >/dev/null 2>&1 || command -v uv >/dev/null 2>&1 || die "pyinstaller not found"
29
+ command -v hdiutil >/dev/null 2>&1 || die "hdiutil not found (expected on macOS)"
30
+ command -v codesign >/dev/null 2>&1 || die "codesign not found"
31
+ [[ -f "$SPEC" ]] || die "Talk.spec not found at $SPEC"
32
+ [[ -f "$ENTITLEMENTS" ]] || die "entitlements.plist not found at $ENTITLEMENTS"
33
+
34
+ # ---- 1. build .app ---------------------------------------------------------
35
+ info "Building $APP_NAME.app with PyInstaller"
36
+ cd "$ROOT"
37
+ uv run pyinstaller "$SPEC" --clean --noconfirm
38
+ [[ -d "$APP" ]] || die "PyInstaller build did not produce $APP"
39
+ ok "Build complete ($(du -sh "$APP" | cut -f1))"
40
+
41
+ # ---- 2. code-sign (inside-out) ----------------------------------------------
42
+ info "Signing binaries inside the bundle"
43
+
44
+ # Sign all .dylib and .so files first
45
+ find "$APP" -type f \( -name "*.dylib" -o -name "*.so" \) -print0 | \
46
+ while IFS= read -r -d '' lib; do
47
+ codesign --sign "$IDENTITY" --timestamp --force --options runtime \
48
+ --entitlements "$ENTITLEMENTS" "$lib" 2>/dev/null || true
49
+ done
50
+
51
+ # Sign all embedded frameworks
52
+ find "$APP/Contents/Frameworks" -type f -perm +111 -print0 2>/dev/null | \
53
+ while IFS= read -r -d '' bin; do
54
+ codesign --sign "$IDENTITY" --timestamp --force --options runtime \
55
+ --entitlements "$ENTITLEMENTS" "$bin" 2>/dev/null || true
56
+ done
57
+
58
+ # Sign the main executable
59
+ codesign --sign "$IDENTITY" --timestamp --force --options runtime \
60
+ --entitlements "$ENTITLEMENTS" "$APP/Contents/MacOS/$APP_NAME"
61
+
62
+ # Sign the whole .app bundle
63
+ codesign --sign "$IDENTITY" --deep --force --options runtime \
64
+ --timestamp --entitlements "$ENTITLEMENTS" "$APP"
65
+
66
+ ok "Code signing complete"
67
+
68
+ # Verify
69
+ codesign --verify --deep --strict "$APP" || die "Signature verification failed"
70
+ ok "Signature verified"
71
+
72
+ # ---- 3. create DMG ---------------------------------------------------------
73
+ info "Creating DMG"
74
+ rm -f "$DMG"
75
+
76
+ STAGING="$(mktemp -d)"
77
+ trap 'rm -rf "$STAGING"' EXIT
78
+ cp -R "$APP" "$STAGING/"
79
+ ln -s /Applications "$STAGING/Applications"
80
+
81
+ hdiutil create -volname "$APP_NAME" -srcfolder "$STAGING" -ov -format UDZO "$DMG"
82
+ rm -rf "$STAGING"
83
+ trap - EXIT
84
+
85
+ ok "DMG created at $DMG ($(du -sh "$DMG" | cut -f1))"
86
+
87
+ # ---- 4. sign DMG -----------------------------------------------------------
88
+ info "Signing DMG"
89
+ codesign --sign "$IDENTITY" --timestamp "$DMG"
90
+ ok "DMG signed"
91
+
92
+ # ---- 5. notarize ------------------------------------------------------------
93
+ info "Submitting for notarization (this may take a few minutes)"
94
+ xcrun notarytool submit "$DMG" \
95
+ --keychain-profile "$KEYCHAIN_PROFILE" \
96
+ --wait
97
+
98
+ ok "Notarization complete"
99
+
100
+ # ---- 6. staple --------------------------------------------------------------
101
+ info "Stapling notarization ticket"
102
+ xcrun stapler staple "$DMG"
103
+ ok "Stapled"
104
+
105
+ # ---- done -------------------------------------------------------------------
106
+ echo ""
107
+ ok "Done! Distributable DMG: $DMG"
108
+ echo " Size: $(du -sh "$DMG" | cut -f1)"
109
+ echo " Verify: spctl --assess --type open --context context:primary-signature \"$DMG\""
@@ -48,8 +48,8 @@ ensure_key_default() {
48
48
 
49
49
  # Keep Parakeet as the default for fresh installs.
50
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"
51
+ ensure_key_default "DICTATE_HOTKEY" "<ctrl>+<alt>+d"
52
+ ensure_key_default "DICTATE_QUIT_HOTKEY" "<ctrl>+<alt>+q"
53
53
  ensure_key_default "DICTATE_AUTOPASTE" "true"
54
54
  ensure_key_default "PARAKEET_MODEL" "mlx-community/parakeet-tdt-0.6b-v3"
55
55
 
@@ -1,4 +1,4 @@
1
1
  """talk — local-first dictation for macOS."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.1.0"
4
+ __version__ = "0.2.0"
@@ -5,6 +5,7 @@ import subprocess
5
5
  import threading
6
6
  import time
7
7
  from pathlib import Path
8
+ from typing import Callable
8
9
 
9
10
  from pynput import keyboard
10
11
 
@@ -13,6 +14,8 @@ from talk.backends.factory import build_backend
13
14
  from talk.config import Settings
14
15
  from talk.paste import paste_text
15
16
 
17
+ StateCallback = Callable[[str], None]
18
+
16
19
 
17
20
  def _play_sound(filename: str) -> None:
18
21
  sound_path = Path("/System/Library/Sounds") / filename
@@ -26,7 +29,11 @@ def _play_sound(filename: str) -> None:
26
29
 
27
30
 
28
31
  class DictationApp:
29
- def __init__(self, settings: Settings) -> None:
32
+ def __init__(
33
+ self,
34
+ settings: Settings,
35
+ on_state_change: StateCallback | None = None,
36
+ ) -> None:
30
37
  self.settings = settings
31
38
  self.backend = build_backend(settings)
32
39
  self.recorder = MicRecorder(
@@ -34,11 +41,20 @@ class DictationApp:
34
41
  channels=settings.channels,
35
42
  )
36
43
 
44
+ self._on_state_change = on_state_change
37
45
  self._lock = threading.Lock()
38
46
  self._stop_event = threading.Event()
47
+ self._listener: keyboard.GlobalHotKeys | None = None
39
48
  self._is_recording = False
40
49
  self._is_transcribing = False
41
50
 
51
+ def _emit_state(self, state: str) -> None:
52
+ if self._on_state_change is not None:
53
+ try:
54
+ self._on_state_change(state)
55
+ except Exception: # noqa: BLE001
56
+ pass
57
+
42
58
  def _toggle_recording(self) -> None:
43
59
  chunk: AudioChunk | None = None
44
60
 
@@ -55,6 +71,7 @@ class DictationApp:
55
71
  return
56
72
 
57
73
  self._is_recording = True
74
+ self._emit_state("recording")
58
75
  _play_sound("Glass.aiff")
59
76
  print("[rec] Recording... press hotkey again to stop.")
60
77
  return
@@ -64,10 +81,12 @@ class DictationApp:
64
81
  except RecorderError as exc:
65
82
  print(f"[error] Could not stop recording cleanly: {exc}")
66
83
  self._is_recording = False
84
+ self._emit_state("idle")
67
85
  return
68
86
 
69
87
  self._is_recording = False
70
88
  self._is_transcribing = True
89
+ self._emit_state("transcribing")
71
90
 
72
91
  _play_sound("Pop.aiff")
73
92
  worker = threading.Thread(
@@ -101,6 +120,7 @@ class DictationApp:
101
120
  finally:
102
121
  with self._lock:
103
122
  self._is_transcribing = False
123
+ self._emit_state("idle")
104
124
 
105
125
  def _request_shutdown(self) -> None:
106
126
  with self._lock:
@@ -113,25 +133,42 @@ class DictationApp:
113
133
  print("[exit] Shutting down dictation app.")
114
134
  self._stop_event.set()
115
135
 
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}")
136
+ def start_hotkey_listener(self) -> None:
137
+ """Start the global hotkey listener without blocking.
120
138
 
139
+ Use this when another event loop (e.g. rumps) owns the main thread.
140
+ Call stop() to tear down.
141
+ """
121
142
  keymap = {
122
143
  self.settings.hotkey: self._toggle_recording,
123
144
  self.settings.quit_hotkey: self._request_shutdown,
124
145
  }
146
+ self._listener = keyboard.GlobalHotKeys(keymap)
147
+ self._listener.start()
148
+
149
+ def stop(self) -> None:
150
+ """Stop the hotkey listener and clean up."""
151
+ self._request_shutdown()
152
+ if self._listener is not None:
153
+ self._listener.stop()
154
+ self._listener = None
155
+
156
+ def run(self) -> None:
157
+ """Blocking run loop for CLI mode."""
158
+ print(f"[ready] Backend: {self.backend.name}")
159
+ print(f"[ready] Toggle dictation: {self.settings.hotkey}")
160
+ print(f"[ready] Quit app: {self.settings.quit_hotkey}")
125
161
 
126
162
  signal.signal(signal.SIGTERM, lambda *_: self._request_shutdown())
127
163
 
128
- listener = keyboard.GlobalHotKeys(keymap)
129
- listener.start()
164
+ self.start_hotkey_listener()
130
165
  try:
131
166
  while not self._stop_event.is_set():
132
167
  time.sleep(0.15)
133
168
  except KeyboardInterrupt:
134
169
  self._request_shutdown()
135
170
  finally:
136
- listener.stop()
171
+ if self._listener is not None:
172
+ self._listener.stop()
173
+ self._listener = None
137
174
 
@@ -76,8 +76,8 @@ def load_settings() -> Settings:
76
76
  load_dotenv(CONFIG_DIR / ".env")
77
77
 
78
78
  backend = os.getenv("DICTATE_BACKEND", "parakeet").strip().lower()
79
- hotkey = normalize_hotkey(os.getenv("DICTATE_HOTKEY", "<cmd>+<shift>+<space>"))
80
- quit_hotkey = normalize_hotkey(os.getenv("DICTATE_QUIT_HOTKEY", "<cmd>+<shift>+q"))
79
+ hotkey = normalize_hotkey(os.getenv("DICTATE_HOTKEY", "<ctrl>+<alt>+d"))
80
+ quit_hotkey = normalize_hotkey(os.getenv("DICTATE_QUIT_HOTKEY", "<ctrl>+<alt>+q"))
81
81
 
82
82
  return Settings(
83
83
  backend=backend,
@@ -0,0 +1,79 @@
1
+ """Menu bar app entry point for Talk.app (macOS)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def _resource_path(filename: str) -> str:
11
+ """Resolve asset paths for both dev and PyInstaller bundles."""
12
+ if getattr(sys, "_MEIPASS", None):
13
+ return str(Path(sys._MEIPASS) / filename) # type: ignore[attr-defined]
14
+ return str(Path(__file__).resolve().parent.parent.parent / "assets" / filename)
15
+
16
+
17
+ def main() -> None:
18
+ import rumps
19
+
20
+ from talk.app import DictationApp
21
+ from talk.config import load_settings
22
+
23
+ ICON_IDLE = _resource_path("mic.png")
24
+ ICON_RECORDING = _resource_path("mic_rec.png")
25
+ ICON_TRANSCRIBING = _resource_path("mic_wait.png")
26
+
27
+ class TalkApp(rumps.App):
28
+ def __init__(self) -> None:
29
+ super().__init__(
30
+ "Talk",
31
+ icon=ICON_IDLE if os.path.exists(ICON_IDLE) else None,
32
+ quit_button=None,
33
+ )
34
+ self._status_item = rumps.MenuItem("Starting...")
35
+ self.menu = [
36
+ self._status_item,
37
+ None,
38
+ rumps.MenuItem("Quit Talk", callback=self._quit),
39
+ ]
40
+ self._dictation: DictationApp | None = None
41
+
42
+ @rumps.timer(1)
43
+ def _deferred_init(self, timer: rumps.Timer) -> None:
44
+ """Load model after the menu bar icon is visible."""
45
+ timer.stop()
46
+ settings = load_settings()
47
+ self._dictation = DictationApp(
48
+ settings, on_state_change=self._on_state,
49
+ )
50
+ self._dictation.start_hotkey_listener()
51
+ self._status_item.title = f"Ready \u2014 {settings.hotkey} to dictate"
52
+
53
+ def _on_state(self, state: str) -> None:
54
+ icons = {
55
+ "idle": ICON_IDLE,
56
+ "recording": ICON_RECORDING,
57
+ "transcribing": ICON_TRANSCRIBING,
58
+ }
59
+ icon_path = icons.get(state, ICON_IDLE)
60
+ if os.path.exists(icon_path):
61
+ self.icon = icon_path
62
+
63
+ labels = {
64
+ "idle": "Ready",
65
+ "recording": "Recording...",
66
+ "transcribing": "Transcribing...",
67
+ }
68
+ self._status_item.title = labels.get(state, "Ready")
69
+
70
+ def _quit(self, _sender: rumps.MenuItem) -> None:
71
+ if self._dictation is not None:
72
+ self._dictation.stop()
73
+ rumps.quit_application()
74
+
75
+ TalkApp().run()
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
@@ -6,6 +6,15 @@ resolution-markers = [
6
6
  "python_full_version < '3.13'",
7
7
  ]
8
8
 
9
+ [[package]]
10
+ name = "altgraph"
11
+ version = "0.17.5"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
16
+ ]
17
+
9
18
  [[package]]
10
19
  name = "annotated-doc"
11
20
  version = "0.0.4"
@@ -585,6 +594,18 @@ wheels = [
585
594
  { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" },
586
595
  ]
587
596
 
597
+ [[package]]
598
+ name = "macholib"
599
+ version = "1.16.4"
600
+ source = { registry = "https://pypi.org/simple" }
601
+ dependencies = [
602
+ { name = "altgraph" },
603
+ ]
604
+ sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
605
+ wheels = [
606
+ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
607
+ ]
608
+
588
609
  [[package]]
589
610
  name = "markdown-it-py"
590
611
  version = "4.0.0"
@@ -851,6 +872,15 @@ wheels = [
851
872
  { url = "https://files.pythonhosted.org/packages/76/38/2ea2d9dfc35295b600e86583baf6cbb9f3242a52cbea98625ad788fb9afb/parakeet_mlx-0.5.1-py3-none-any.whl", hash = "sha256:869cfe8d7a16b00f4a49d2f42a76210aca638681bd972911f4ab4d7bf9ca36b6", size = 37205, upload-time = "2026-02-21T04:18:14.899Z" },
852
873
  ]
853
874
 
875
+ [[package]]
876
+ name = "pefile"
877
+ version = "2024.8.26"
878
+ source = { registry = "https://pypi.org/simple" }
879
+ sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
880
+ wheels = [
881
+ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
882
+ ]
883
+
854
884
  [[package]]
855
885
  name = "platformdirs"
856
886
  version = "4.9.2"
@@ -1004,6 +1034,47 @@ wheels = [
1004
1034
  { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
1005
1035
  ]
1006
1036
 
1037
+ [[package]]
1038
+ name = "pyinstaller"
1039
+ version = "6.19.0"
1040
+ source = { registry = "https://pypi.org/simple" }
1041
+ dependencies = [
1042
+ { name = "altgraph" },
1043
+ { name = "macholib", marker = "sys_platform == 'darwin'" },
1044
+ { name = "packaging" },
1045
+ { name = "pefile", marker = "sys_platform == 'win32'" },
1046
+ { name = "pyinstaller-hooks-contrib" },
1047
+ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
1048
+ { name = "setuptools" },
1049
+ ]
1050
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/63/fd62472b6371d89dc138d40c36d87a50dc2de18a035803bbdc376b4ffac4/pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865", size = 4036072, upload-time = "2026-02-14T18:06:28.718Z" }
1051
+ wheels = [
1052
+ { url = "https://files.pythonhosted.org/packages/e3/eb/23374721fecfa72677e79800921cb6aceefa6ba48574dc404f3f6c6c3be7/pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134", size = 1040563, upload-time = "2026-02-14T18:05:22.436Z" },
1053
+ { url = "https://files.pythonhosted.org/packages/cd/7e/dfd724b0b533f5aaec0ee5df406fe2319987ed6964480a706f85478b12ea/pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11", size = 735477, upload-time = "2026-02-14T18:05:27.143Z" },
1054
+ { url = "https://files.pythonhosted.org/packages/88/c9/ee3a4101c31f26344e66896c73c1fd6ed8282bf871473365b7f8674af406/pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf", size = 747143, upload-time = "2026-02-14T18:05:31.488Z" },
1055
+ { url = "https://files.pythonhosted.org/packages/da/0a/fc77e9f861be8cf300ac37155f59cc92aff99b29f2ddd78546f563a5b5a6/pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122", size = 744849, upload-time = "2026-02-14T18:05:35.492Z" },
1056
+ { url = "https://files.pythonhosted.org/packages/6d/e3/6872e020ee758afe0b821663858492c10745608b07150e5e2c824a5b3e1c/pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63", size = 741590, upload-time = "2026-02-14T18:05:39.59Z" },
1057
+ { url = "https://files.pythonhosted.org/packages/53/60/b8db5f1a4b0fb228175f2ea0aa33f949adcc097fbe981cc524f9faf85777/pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe", size = 741448, upload-time = "2026-02-14T18:05:45.636Z" },
1058
+ { url = "https://files.pythonhosted.org/packages/6f/4d/63b0600f2694e9141b83129fbc1c488ec84d5a0770b1448ec154dcd0fee9/pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83", size = 740613, upload-time = "2026-02-14T18:05:49.726Z" },
1059
+ { url = "https://files.pythonhosted.org/packages/01/d4/e812ad36178093a0e9fd4b8127577748dd85b0cb71de912229dca21fd741/pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6", size = 740350, upload-time = "2026-02-14T18:05:54.093Z" },
1060
+ { url = "https://files.pythonhosted.org/packages/52/03/b2c2ee41fb8e10fd2a45d21f5ec2ef25852cfb978dbf762972eed59e3d63/pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2", size = 1324317, upload-time = "2026-02-14T18:06:00.085Z" },
1061
+ { url = "https://files.pythonhosted.org/packages/9c/d3/6d5e62b8270e2b53a6065e281b3a7785079b00e9019c8019952828dd1669/pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33", size = 1384894, upload-time = "2026-02-14T18:06:06.425Z" },
1062
+ { url = "https://files.pythonhosted.org/packages/81/65/458cd523308a101a22fd2742893405030cc24994cc74b1b767cecf137160/pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea", size = 1325374, upload-time = "2026-02-14T18:06:12.804Z" },
1063
+ ]
1064
+
1065
+ [[package]]
1066
+ name = "pyinstaller-hooks-contrib"
1067
+ version = "2026.1"
1068
+ source = { registry = "https://pypi.org/simple" }
1069
+ dependencies = [
1070
+ { name = "packaging" },
1071
+ { name = "setuptools" },
1072
+ ]
1073
+ sdist = { url = "https://files.pythonhosted.org/packages/95/eb/e1dd9a5348e4cf348471c0e5fd617d948779bc3199cf4edb134d8fceca91/pyinstaller_hooks_contrib-2026.1.tar.gz", hash = "sha256:a5f0891a1e81e92406ab917d9e76adfd7a2b68415ee2e35c950a7b3910bc361b", size = 171504, upload-time = "2026-02-18T13:01:15.711Z" }
1074
+ wheels = [
1075
+ { url = "https://files.pythonhosted.org/packages/7f/69/12bafee3cc485d977f596e0d803d7c6fb147430fc35dfe505730aa3a28dd/pyinstaller_hooks_contrib-2026.1-py3-none-any.whl", hash = "sha256:66ad4888ba67de6f3cfd7ef554f9dd1a4389e2eb19f84d7129a5a6818e3f2180", size = 452841, upload-time = "2026-02-18T13:01:14.471Z" },
1076
+ ]
1077
+
1007
1078
  [[package]]
1008
1079
  name = "pynput"
1009
1080
  version = "1.8.1"
@@ -1129,6 +1200,15 @@ wheels = [
1129
1200
  { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" },
1130
1201
  ]
1131
1202
 
1203
+ [[package]]
1204
+ name = "pywin32-ctypes"
1205
+ version = "0.2.3"
1206
+ source = { registry = "https://pypi.org/simple" }
1207
+ sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
1208
+ wheels = [
1209
+ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
1210
+ ]
1211
+
1132
1212
  [[package]]
1133
1213
  name = "pyyaml"
1134
1214
  version = "6.0.3"
@@ -1212,6 +1292,15 @@ wheels = [
1212
1292
  { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
1213
1293
  ]
1214
1294
 
1295
+ [[package]]
1296
+ name = "rumps"
1297
+ version = "0.4.0"
1298
+ source = { registry = "https://pypi.org/simple" }
1299
+ dependencies = [
1300
+ { name = "pyobjc-framework-cocoa" },
1301
+ ]
1302
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/e2/2e6a47951290bd1a2831dcc50aec4b25d104c0cf00e8b7868cbd29cf3bfe/rumps-0.4.0.tar.gz", hash = "sha256:17fb33c21b54b1e25db0d71d1d793dc19dc3c0b7d8c79dc6d833d0cffc8b1596", size = 39257, upload-time = "2022-10-15T05:15:10.386Z" }
1303
+
1215
1304
  [[package]]
1216
1305
  name = "scikit-learn"
1217
1306
  version = "1.8.0"
@@ -1333,6 +1422,15 @@ wheels = [
1333
1422
  { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
1334
1423
  ]
1335
1424
 
1425
+ [[package]]
1426
+ name = "setuptools"
1427
+ version = "82.0.0"
1428
+ source = { registry = "https://pypi.org/simple" }
1429
+ sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" }
1430
+ wheels = [
1431
+ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" },
1432
+ ]
1433
+
1336
1434
  [[package]]
1337
1435
  name = "shellingham"
1338
1436
  version = "1.5.4"
@@ -1456,7 +1554,7 @@ wheels = [
1456
1554
  ]
1457
1555
 
1458
1556
  [[package]]
1459
- name = "talk"
1557
+ name = "talktype"
1460
1558
  version = "0.1.0"
1461
1559
  source = { editable = "." }
1462
1560
  dependencies = [
@@ -1465,9 +1563,15 @@ dependencies = [
1465
1563
  { name = "parakeet-mlx" },
1466
1564
  { name = "pynput" },
1467
1565
  { name = "python-dotenv" },
1566
+ { name = "rumps" },
1468
1567
  { name = "sounddevice" },
1469
1568
  ]
1470
1569
 
1570
+ [package.dev-dependencies]
1571
+ dev = [
1572
+ { name = "pyinstaller" },
1573
+ ]
1574
+
1471
1575
  [package.metadata]
1472
1576
  requires-dist = [
1473
1577
  { name = "numpy", specifier = ">=1.26" },
@@ -1475,9 +1579,13 @@ requires-dist = [
1475
1579
  { name = "parakeet-mlx", specifier = ">=0.4.2" },
1476
1580
  { name = "pynput", specifier = ">=1.8.1" },
1477
1581
  { name = "python-dotenv", specifier = ">=1.1.1" },
1582
+ { name = "rumps", specifier = ">=0.4.0" },
1478
1583
  { name = "sounddevice", specifier = ">=0.5.2" },
1479
1584
  ]
1480
1585
 
1586
+ [package.metadata.requires-dev]
1587
+ dev = [{ name = "pyinstaller", specifier = ">=6.19.0" }]
1588
+
1481
1589
  [[package]]
1482
1590
  name = "threadpoolctl"
1483
1591
  version = "3.6.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes