python-voiceio 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_voiceio-0.2.0.dist-info/METADATA +260 -0
- python_voiceio-0.2.0.dist-info/RECORD +43 -0
- python_voiceio-0.2.0.dist-info/WHEEL +5 -0
- python_voiceio-0.2.0.dist-info/entry_points.txt +6 -0
- python_voiceio-0.2.0.dist-info/licenses/LICENSE +21 -0
- python_voiceio-0.2.0.dist-info/top_level.txt +1 -0
- voiceio/__init__.py +1 -0
- voiceio/__main__.py +3 -0
- voiceio/app.py +415 -0
- voiceio/backends.py +13 -0
- voiceio/cli.py +475 -0
- voiceio/config.py +136 -0
- voiceio/feedback.py +78 -0
- voiceio/health.py +194 -0
- voiceio/hotkeys/__init__.py +22 -0
- voiceio/hotkeys/base.py +27 -0
- voiceio/hotkeys/chain.py +83 -0
- voiceio/hotkeys/evdev.py +134 -0
- voiceio/hotkeys/pynput_backend.py +80 -0
- voiceio/hotkeys/socket_backend.py +77 -0
- voiceio/ibus/__init__.py +8 -0
- voiceio/ibus/engine.py +268 -0
- voiceio/platform.py +139 -0
- voiceio/recorder.py +208 -0
- voiceio/service.py +234 -0
- voiceio/sounds/__init__.py +0 -0
- voiceio/sounds/commit.wav +0 -0
- voiceio/sounds/start.wav +0 -0
- voiceio/sounds/stop.wav +0 -0
- voiceio/streaming.py +202 -0
- voiceio/transcriber.py +165 -0
- voiceio/tray.py +54 -0
- voiceio/typers/__init__.py +31 -0
- voiceio/typers/base.py +44 -0
- voiceio/typers/chain.py +79 -0
- voiceio/typers/clipboard.py +110 -0
- voiceio/typers/ibus.py +389 -0
- voiceio/typers/pynput_type.py +51 -0
- voiceio/typers/wtype.py +57 -0
- voiceio/typers/xdotool.py +45 -0
- voiceio/typers/ydotool.py +115 -0
- voiceio/wizard.py +882 -0
- voiceio/worker.py +39 -0
voiceio/typers/wtype.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Wtype text injection backend for wlroots compositors (Sway, Hyprland)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from voiceio.backends import ProbeResult
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WtypeTyper:
|
|
14
|
+
"""Type text via wtype (wlroots-based Wayland compositors)."""
|
|
15
|
+
|
|
16
|
+
name = "wtype"
|
|
17
|
+
|
|
18
|
+
def probe(self) -> ProbeResult:
|
|
19
|
+
if not shutil.which("wtype"):
|
|
20
|
+
return ProbeResult(ok=False, reason="wtype not installed",
|
|
21
|
+
fix_hint="sudo apt install wtype")
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
session = os.environ.get("XDG_SESSION_TYPE", "")
|
|
25
|
+
if session != "wayland":
|
|
26
|
+
return ProbeResult(ok=False, reason="wtype requires Wayland")
|
|
27
|
+
|
|
28
|
+
# wtype doesn't work on GNOME/Mutter, only wlroots compositors
|
|
29
|
+
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
|
30
|
+
if "gnome" in desktop:
|
|
31
|
+
return ProbeResult(
|
|
32
|
+
ok=False,
|
|
33
|
+
reason="wtype does not work on GNOME (Mutter doesn't support virtual keyboard protocol)",
|
|
34
|
+
fix_hint="Use ydotool instead.",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return ProbeResult(ok=True)
|
|
38
|
+
|
|
39
|
+
def type_text(self, text: str) -> None:
|
|
40
|
+
if not text:
|
|
41
|
+
return
|
|
42
|
+
subprocess.run(
|
|
43
|
+
["wtype", "--", text],
|
|
44
|
+
check=True, capture_output=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def delete_chars(self, n: int) -> None:
|
|
48
|
+
if n <= 0:
|
|
49
|
+
return
|
|
50
|
+
# Batch all backspaces into one call: wtype -k BackSpace -k BackSpace ...
|
|
51
|
+
args = []
|
|
52
|
+
for _ in range(n):
|
|
53
|
+
args.extend(["-k", "BackSpace"])
|
|
54
|
+
subprocess.run(
|
|
55
|
+
["wtype", *args],
|
|
56
|
+
check=True, capture_output=True,
|
|
57
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Xdotool text injection backend for X11."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from voiceio.backends import ProbeResult
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class XdotoolTyper:
|
|
14
|
+
"""Type text via xdotool (X11)."""
|
|
15
|
+
|
|
16
|
+
name = "xdotool"
|
|
17
|
+
|
|
18
|
+
def probe(self) -> ProbeResult:
|
|
19
|
+
if not shutil.which("xdotool"):
|
|
20
|
+
return ProbeResult(ok=False, reason="xdotool not installed",
|
|
21
|
+
fix_hint="sudo apt install xdotool")
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
session = os.environ.get("XDG_SESSION_TYPE", "")
|
|
25
|
+
if session == "wayland":
|
|
26
|
+
return ProbeResult(ok=False, reason="xdotool does not work on Wayland",
|
|
27
|
+
fix_hint="Use ydotool or wtype instead.")
|
|
28
|
+
|
|
29
|
+
return ProbeResult(ok=True)
|
|
30
|
+
|
|
31
|
+
def type_text(self, text: str) -> None:
|
|
32
|
+
if not text:
|
|
33
|
+
return
|
|
34
|
+
subprocess.run(
|
|
35
|
+
["xdotool", "type", "--clearmodifiers", "--delay", "12", "--", text],
|
|
36
|
+
check=True, capture_output=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def delete_chars(self, n: int) -> None:
|
|
40
|
+
if n <= 0:
|
|
41
|
+
return
|
|
42
|
+
subprocess.run(
|
|
43
|
+
["xdotool", "key", "--clearmodifiers", "--delay", "12"] + ["BackSpace"] * n,
|
|
44
|
+
check=True, capture_output=True,
|
|
45
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Ydotool text injection backend for Wayland via uinput."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from voiceio.backends import ProbeResult
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@functools.lru_cache(maxsize=1)
|
|
16
|
+
def _get_ydotool_version() -> tuple[int, ...]:
|
|
17
|
+
"""Get ydotool major version. Returns (0,) on failure. Cached."""
|
|
18
|
+
try:
|
|
19
|
+
# v1.x prints version, v0.x doesn't support --version
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["ydotool", "--version"], capture_output=True, text=True, timeout=2,
|
|
22
|
+
)
|
|
23
|
+
if result.returncode == 0:
|
|
24
|
+
# e.g. "ydotool 1.0.4"
|
|
25
|
+
parts = result.stdout.strip().split()[-1].split(".")
|
|
26
|
+
return tuple(int(p) for p in parts)
|
|
27
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
|
|
28
|
+
pass
|
|
29
|
+
return (0,)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _needs_daemon() -> bool:
|
|
33
|
+
"""v1.x needs ydotoold, v0.x talks to /dev/uinput directly."""
|
|
34
|
+
return _get_ydotool_version() >= (1,)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ydotoold_running() -> bool:
|
|
38
|
+
"""Check if the ydotoold daemon is running."""
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(["pgrep", "-x", "ydotoold"], capture_output=True)
|
|
41
|
+
return result.returncode == 0
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
return True # can't check, assume ok
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _has_uinput_access() -> bool:
|
|
47
|
+
"""Check if current user can write to /dev/uinput."""
|
|
48
|
+
try:
|
|
49
|
+
return os.access("/dev/uinput", os.W_OK)
|
|
50
|
+
except OSError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class YdotoolTyper:
|
|
55
|
+
"""Type text via ydotool (Wayland, needs uinput access)."""
|
|
56
|
+
|
|
57
|
+
name = "ydotool"
|
|
58
|
+
|
|
59
|
+
def probe(self) -> ProbeResult:
|
|
60
|
+
if not shutil.which("ydotool"):
|
|
61
|
+
return ProbeResult(ok=False, reason="ydotool not installed",
|
|
62
|
+
fix_hint="sudo apt install ydotool")
|
|
63
|
+
|
|
64
|
+
if _needs_daemon():
|
|
65
|
+
# v1.x: needs ydotoold running
|
|
66
|
+
if not _ydotoold_running():
|
|
67
|
+
ydotoold_path = shutil.which("ydotoold") or "ydotoold"
|
|
68
|
+
return ProbeResult(
|
|
69
|
+
ok=False,
|
|
70
|
+
reason="ydotoold daemon not running",
|
|
71
|
+
fix_hint=f"sudo {ydotoold_path} &",
|
|
72
|
+
fix_cmd=["sudo", ydotoold_path],
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
# v0.x: needs /dev/uinput write access
|
|
76
|
+
if not _has_uinput_access():
|
|
77
|
+
return ProbeResult(
|
|
78
|
+
ok=False,
|
|
79
|
+
reason="No write access to /dev/uinput",
|
|
80
|
+
fix_hint="sudo chmod 0666 /dev/uinput (or add udev rule)",
|
|
81
|
+
fix_cmd=["sudo", "chmod", "0666", "/dev/uinput"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return ProbeResult(ok=True)
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._v1 = _get_ydotool_version() >= (1,)
|
|
88
|
+
|
|
89
|
+
def type_text(self, text: str) -> None:
|
|
90
|
+
if not text:
|
|
91
|
+
return
|
|
92
|
+
subprocess.run(
|
|
93
|
+
["ydotool", "type", "--delay", "10", "--key-delay", "2", "--", text],
|
|
94
|
+
check=True, capture_output=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def delete_chars(self, n: int) -> None:
|
|
98
|
+
if n <= 0:
|
|
99
|
+
return
|
|
100
|
+
if self._v1:
|
|
101
|
+
# v1.x: raw keycode:state syntax
|
|
102
|
+
args = []
|
|
103
|
+
for _ in range(n):
|
|
104
|
+
args.extend(["14:1", "14:0"])
|
|
105
|
+
subprocess.run(
|
|
106
|
+
["ydotool", "key", *args],
|
|
107
|
+
check=True, capture_output=True,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# v0.x: key names, batch all backspaces in one call
|
|
111
|
+
keys = ["Backspace"] * n
|
|
112
|
+
subprocess.run(
|
|
113
|
+
["ydotool", "key", "--key-delay", "2", *keys],
|
|
114
|
+
check=True, capture_output=True,
|
|
115
|
+
)
|