python-voiceio 0.9.1__py3-none-any.whl → 0.9.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
- python_voiceio-0.9.1.dist-info/licenses/LICENSE,sha256=Gz61o8jFTAvZUZyB3nWDB3DQQVuipjfPkVu9W8hBHM0,1072
2
- voiceio/__init__.py,sha256=UwJXM8JY2T3tE2id0K2k_lEaVThbRTrGO1mNibyzIz8,22
1
+ python_voiceio-0.9.2.dist-info/licenses/LICENSE,sha256=Gz61o8jFTAvZUZyB3nWDB3DQQVuipjfPkVu9W8hBHM0,1072
2
+ voiceio/__init__.py,sha256=gqT-BGoeEItda9fICQDvLbxEjWRIBhFJxPxxKvmHLUo,22
3
3
  voiceio/__main__.py,sha256=xT5QCGGreYMisHO7Lh_Y-xAQ2TOzG2D7npKknGNcWSY,53
4
4
  voiceio/app.py,sha256=ScrUVDQQ_YbAFKKSMOhaRPX_WTcPuenhnRdSzWMxThE,73785
5
5
  voiceio/audit.py,sha256=p4F2BBX4wEC9eS8MSplzfXL52XC2dyIKbgrRvR-WbTU,15802
@@ -45,8 +45,8 @@ voiceio/hotkeys/chain.py,sha256=8pvPhipmxWwmGG9DRXy8dlEKTPFILxNzYO-adcvw5Ps,2800
45
45
  voiceio/hotkeys/evdev.py,sha256=DHmd2IASBkYnyY5JEGFIoYfZtw3T9Ij6wKclkx-6m94,5109
46
46
  voiceio/hotkeys/pynput_backend.py,sha256=_rLb8p0JakR9T32LJclHVTch49M_mnB3UeUsI_IkfLk,3576
47
47
  voiceio/hotkeys/socket_backend.py,sha256=xLNFa8FvJbL5VLKqMRhKPeaKdhztYwxyVV_VOlG1PkM,3373
48
- voiceio/ibus/__init__.py,sha256=WZgQTPNnQZmmFurw3XPQEfV49nehmKoDI0wTg-WId1E,1120
49
- voiceio/ibus/engine.py,sha256=S_vepGH_6ZNZL1UBbgZZPhWbTkjrF9osVn5_Nf5utqI,9602
48
+ voiceio/ibus/__init__.py,sha256=z_f9-svxKgMpNpnATGl7lkMskAMhW2WzJ1MretmUwgs,2534
49
+ voiceio/ibus/engine.py,sha256=HF2rz3Jp51ljPqR3XP2iB2qpwfSKRV7Rbyj11zOiDrs,10015
50
50
  voiceio/ibus/pending.py,sha256=T6Y_8oimhabahunp9RogtO-OTA5aw9d9G4T5lH1WAL4,1464
51
51
  voiceio/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  voiceio/models/silero_vad.onnx,sha256=GhU6IvRQnikqlOZ9b5uF6N6yW0mIaCt-F0xlJ52HiOM,2327524
@@ -74,8 +74,8 @@ voiceio/typers/pynput_type.py,sha256=DTtkT59M-EKOAlTNkvcZrRvf_Fq52OpI8S5t_8S-QAY
74
74
  voiceio/typers/wtype.py,sha256=d1wG-HZdYDQxXIysV_Xvk9HixDpaJgl3VEoKbP5vhIs,1786
75
75
  voiceio/typers/xdotool.py,sha256=dh1zhPqUT8ihJahJ7ZKm6PtfYY087UAzADx664DvQOM,1356
76
76
  voiceio/typers/ydotool.py,sha256=dt0W9ot__W8LQG2so8vZLGIw9leK2Q6g3i-Voo0LXE4,5327
77
- python_voiceio-0.9.1.dist-info/METADATA,sha256=cl2iSSiUNTBYc_PLsGeyD5uZDE44RVYV81nsJkvfVuc,15877
78
- python_voiceio-0.9.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
79
- python_voiceio-0.9.1.dist-info/entry_points.txt,sha256=U64fA65zxzyLoC8bgbn2ztQVWHLsc0o0H0qCk1J9DMc,218
80
- python_voiceio-0.9.1.dist-info/top_level.txt,sha256=piwtn309lD6uexQyXdZ-efAVBJF9y6Wfr48Z-8zkNhg,8
81
- python_voiceio-0.9.1.dist-info/RECORD,,
77
+ python_voiceio-0.9.2.dist-info/METADATA,sha256=RZm3kWz94AUBzUicA4CWYbm_oLyaFrNVfvU0acqEHEY,15877
78
+ python_voiceio-0.9.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
79
+ python_voiceio-0.9.2.dist-info/entry_points.txt,sha256=U64fA65zxzyLoC8bgbn2ztQVWHLsc0o0H0qCk1J9DMc,218
80
+ python_voiceio-0.9.2.dist-info/top_level.txt,sha256=piwtn309lD6uexQyXdZ-efAVBJF9y6Wfr48Z-8zkNhg,8
81
+ python_voiceio-0.9.2.dist-info/RECORD,,
voiceio/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.1"
1
+ __version__ = "0.9.2"
voiceio/ibus/__init__.py CHANGED
@@ -6,6 +6,37 @@ from pathlib import Path
6
6
 
7
7
  SOCKET_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio-ibus.sock"
8
8
  READY_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio-ibus.ready"
9
+ # Held for the engine's lifetime so exactly one process ever owns the command
10
+ # socket — see acquire_singleton_lock().
11
+ LOCK_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio-ibus.lock"
12
+
13
+
14
+ def acquire_singleton_lock() -> int | None:
15
+ """Take an exclusive, lifetime-held lock on LOCK_PATH.
16
+
17
+ Returns the open fd on success (the caller must keep it open — the lock is
18
+ released when the fd closes, i.e. when the process dies), or None if another
19
+ engine already holds it.
20
+
21
+ This is the race-free gate against duplicate engines. ibus-daemon
22
+ exec-spawns its own copy of the engine when something activates the voiceio
23
+ source (GNOME's per-window source memory / Super+Space — voiceio sits first
24
+ in mru-sources). A ping-then-bind check is check-then-act: two engines
25
+ starting close together both see "no healthy peer", then both unlink+rebind
26
+ the DGRAM socket in _socket_listener. The loser is left bound to an orphaned
27
+ inode — alive but never receiving pings ("zombie") — which the daemon's
28
+ health watchdog then kills and respawns, churning ~15x/day. flock is atomic:
29
+ the loser never touches the socket, it just exits.
30
+ """
31
+ import fcntl
32
+
33
+ fd = os.open(str(LOCK_PATH), os.O_CREAT | os.O_RDWR, 0o600)
34
+ try:
35
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
36
+ except OSError:
37
+ os.close(fd)
38
+ return None
39
+ return fd
9
40
 
10
41
 
11
42
  def ping_engine(timeout: float = 2.0) -> bool:
voiceio/ibus/engine.py CHANGED
@@ -24,10 +24,18 @@ import gi
24
24
  gi.require_version("IBus", "1.0")
25
25
  from gi.repository import GLib, GObject, IBus
26
26
 
27
- from voiceio.ibus import READY_PATH, SOCKET_PATH, ping_engine
27
+ from voiceio.ibus import (
28
+ READY_PATH,
29
+ SOCKET_PATH,
30
+ acquire_singleton_lock,
31
+ )
28
32
  from voiceio.ibus.pending import PendingBuffer
29
33
 
30
34
  log = logging.getLogger(__name__)
35
+
36
+ # Singleton lock fd, held for the process lifetime (see main()). Module-level so
37
+ # it is never garbage-collected — closing the fd would release the lock.
38
+ _lock_fd: int | None = None
31
39
  ENGINE_NAME = "voiceio"
32
40
  COMPONENT_NAME = "org.voiceio.ibus"
33
41
 
@@ -230,13 +238,17 @@ def main() -> None:
230
238
  ],
231
239
  )
232
240
 
233
- # Refuse to double-run. ibus-daemon can exec-spawn its own copy of this
234
- # engine (component XML) when something activates the voiceio source
235
- # e.g. GNOME's per-window source memory. Binding SOCKET_PATH here would
236
- # steal it from the healthy daemon-spawned engine and silently starve it
237
- # (alive, but never sees another command or ping → zombie recovery).
238
- if ping_engine(timeout=0.5):
239
- log.info("Healthy VoiceIO engine already serving %s exiting duplicate", SOCKET_PATH)
241
+ # Refuse to double-run, race-free. ibus-daemon exec-spawns its own copy of
242
+ # this engine (component XML) when something activates the voiceio source
243
+ # (GNOME per-window source memory / Super+Space voiceio is first in
244
+ # mru-sources). Without an atomic gate, two engines both pass a ping check
245
+ # then both unlink+rebind SOCKET_PATH in _socket_listener; the loser is
246
+ # left on an orphaned inode — a "zombie" the watchdog kills and respawns.
247
+ # The lock fd is held for our whole lifetime (released on process death).
248
+ global _lock_fd
249
+ _lock_fd = acquire_singleton_lock()
250
+ if _lock_fd is None:
251
+ log.info("Another VoiceIO engine holds the lock — exiting duplicate")
240
252
  return
241
253
 
242
254
  IBus.init()