python-voiceio 0.3.4__tar.gz → 0.3.5__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.
- {python_voiceio-0.3.4/python_voiceio.egg-info → python_voiceio-0.3.5}/PKG-INFO +1 -1
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/pyproject.toml +1 -1
- {python_voiceio-0.3.4 → python_voiceio-0.3.5/python_voiceio.egg-info}/PKG-INFO +1 -1
- python_voiceio-0.3.5/voiceio/__init__.py +1 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/app.py +86 -1
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/ibus.py +14 -2
- python_voiceio-0.3.4/voiceio/__init__.py +0 -1
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/LICENSE +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/README.md +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/setup.cfg +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_app_wiring.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_autocorrect.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_commands.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_config.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_corrections.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_fallback.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_health.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_hints.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_history.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_llm.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_llm_api.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_numbers.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_platform.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_postprocess.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_prebuffer.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_prompt.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_robustness.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_streaming.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_transcriber.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_tts.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_vad.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_vocabulary.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_wordfreq.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/__main__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/autocorrect.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/backends.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/cli.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/commands.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/config.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/corrections.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/demo.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/feedback.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/health.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hints.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/history.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/llm.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/llm_api.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/models/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/numbers.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/pidlock.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/platform.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/postprocess.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/prompt.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/recorder.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/service.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/streaming.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/transcriber.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/base.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/chain.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/player.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/vad.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/vocabulary.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/wizard.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/wordfreq.py +0 -0
- {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/worker.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.5"
|
|
@@ -473,6 +473,34 @@ class VoiceIO:
|
|
|
473
473
|
self._set_gnome_input_source_index(0)
|
|
474
474
|
log.info("VoiceIO IBus engine ready (dormant until recording)")
|
|
475
475
|
|
|
476
|
+
def _reactivate_ibus_if_stale(self) -> None:
|
|
477
|
+
"""Re-activate IBus engine if it lost registration (e.g. after hibernate).
|
|
478
|
+
|
|
479
|
+
After suspend/hibernate, the IBus daemon may forget about our engine.
|
|
480
|
+
Check by querying the current active engine; if it's not 'voiceio',
|
|
481
|
+
re-run ``ibus engine voiceio`` to re-register.
|
|
482
|
+
"""
|
|
483
|
+
from voiceio.typers.ibus import _ibus_env
|
|
484
|
+
try:
|
|
485
|
+
result = subprocess.run(
|
|
486
|
+
["ibus", "engine"], capture_output=True, text=True,
|
|
487
|
+
timeout=3, env=_ibus_env(),
|
|
488
|
+
)
|
|
489
|
+
current = result.stdout.strip() if result.returncode == 0 else ""
|
|
490
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
491
|
+
return
|
|
492
|
+
if current == "voiceio":
|
|
493
|
+
return # still registered, nothing to do
|
|
494
|
+
log.warning("IBus engine stale (current=%r), re-activating", current)
|
|
495
|
+
try:
|
|
496
|
+
subprocess.run(
|
|
497
|
+
["ibus", "engine", "voiceio"],
|
|
498
|
+
capture_output=True, timeout=5, env=_ibus_env(),
|
|
499
|
+
)
|
|
500
|
+
log.info("IBus engine re-activated")
|
|
501
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
502
|
+
log.warning("IBus re-activation failed: %s", e)
|
|
503
|
+
|
|
476
504
|
def _wait_for_ibus(self, chain: list[str]) -> TyperBackend | None:
|
|
477
505
|
"""Wait for IBus daemon to become available and switch to IBus typer.
|
|
478
506
|
|
|
@@ -584,17 +612,70 @@ class VoiceIO:
|
|
|
584
612
|
|
|
585
613
|
# ── Health watchdog ─────────────────────────────────────────────
|
|
586
614
|
|
|
615
|
+
# If the gap between health checks exceeds this, we probably resumed
|
|
616
|
+
# from suspend/hibernate and should re-probe everything.
|
|
617
|
+
_RESUME_THRESHOLD = 30 # seconds
|
|
618
|
+
|
|
587
619
|
def _health_loop(self) -> None:
|
|
588
620
|
"""Periodic health check: transcriber worker, IBus engine, audio stream."""
|
|
621
|
+
last_check = time.monotonic()
|
|
589
622
|
while not self._shutdown.is_set():
|
|
590
623
|
self._shutdown.wait(_HEALTH_CHECK_INTERVAL)
|
|
591
624
|
if self._shutdown.is_set():
|
|
592
625
|
break
|
|
626
|
+
|
|
627
|
+
now = time.monotonic()
|
|
628
|
+
gap = now - last_check
|
|
629
|
+
last_check = now
|
|
630
|
+
|
|
593
631
|
try:
|
|
632
|
+
if gap > self._RESUME_THRESHOLD:
|
|
633
|
+
log.info("System resume detected (%.0fs gap), re-probing all backends", gap)
|
|
634
|
+
self._on_resume()
|
|
594
635
|
self._check_health()
|
|
595
636
|
except Exception:
|
|
596
637
|
log.debug("Health check error", exc_info=True)
|
|
597
638
|
|
|
639
|
+
def _on_resume(self) -> None:
|
|
640
|
+
"""Re-probe all backends after system suspend/hibernate.
|
|
641
|
+
|
|
642
|
+
Sleep/hibernate breaks connections to system services (IBus, audio,
|
|
643
|
+
tray D-Bus, ydotoold) across all platforms. Instead of catching each
|
|
644
|
+
failure individually, do a single sweep to restore everything.
|
|
645
|
+
"""
|
|
646
|
+
# Audio: reopen stream (device may have changed or died)
|
|
647
|
+
try:
|
|
648
|
+
self.recorder.reopen_stream()
|
|
649
|
+
log.info("Resume: audio stream reopened")
|
|
650
|
+
except Exception:
|
|
651
|
+
log.warning("Resume: audio stream reopen failed", exc_info=True)
|
|
652
|
+
|
|
653
|
+
# IBus: re-activate engine registration
|
|
654
|
+
if self._typer.name == "ibus" and self._engine_proc is not None:
|
|
655
|
+
if self._engine_proc.poll() is not None:
|
|
656
|
+
# Engine process died during sleep
|
|
657
|
+
self._engine_proc = None
|
|
658
|
+
try:
|
|
659
|
+
self._ensure_ibus_engine()
|
|
660
|
+
log.info("Resume: IBus engine restarted")
|
|
661
|
+
except Exception:
|
|
662
|
+
log.exception("Resume: IBus engine restart failed")
|
|
663
|
+
else:
|
|
664
|
+
self._reactivate_ibus_if_stale()
|
|
665
|
+
|
|
666
|
+
# Tray: restart if subprocess died
|
|
667
|
+
if self.cfg.tray.enabled and not tray.is_alive():
|
|
668
|
+
log.info("Resume: restarting tray")
|
|
669
|
+
tray.restart(self.on_hotkey)
|
|
670
|
+
|
|
671
|
+
# Transcriber: ensure worker is alive
|
|
672
|
+
if not self.transcriber.is_worker_alive():
|
|
673
|
+
try:
|
|
674
|
+
self.transcriber._ensure_worker()
|
|
675
|
+
log.info("Resume: transcriber worker restarted")
|
|
676
|
+
except RuntimeError:
|
|
677
|
+
log.error("Resume: transcriber worker failed")
|
|
678
|
+
|
|
598
679
|
def _check_health(self) -> None:
|
|
599
680
|
"""Run one health check cycle."""
|
|
600
681
|
# Check transcriber worker
|
|
@@ -644,7 +725,7 @@ class VoiceIO:
|
|
|
644
725
|
log.warning("Tray subprocess died, restarting")
|
|
645
726
|
tray.restart(self.on_hotkey)
|
|
646
727
|
|
|
647
|
-
# Check IBus engine (restart if died)
|
|
728
|
+
# Check IBus engine (restart if died, re-activate if stale after resume)
|
|
648
729
|
if self._typer.name == "ibus" and self._engine_proc is not None:
|
|
649
730
|
if self._engine_proc.poll() is not None:
|
|
650
731
|
log.warning("IBus engine process died (rc=%d), restarting",
|
|
@@ -655,6 +736,10 @@ class VoiceIO:
|
|
|
655
736
|
log.info("IBus engine recovered")
|
|
656
737
|
except Exception:
|
|
657
738
|
log.exception("IBus engine recovery failed")
|
|
739
|
+
elif self._state == _State.IDLE:
|
|
740
|
+
# Engine alive but may have lost IBus registration after
|
|
741
|
+
# hibernate/suspend. Re-activate so preedit works next time.
|
|
742
|
+
self._reactivate_ibus_if_stale()
|
|
658
743
|
|
|
659
744
|
# ── Main loop ───────────────────────────────────────────────────────
|
|
660
745
|
|
|
@@ -384,10 +384,22 @@ class IBusTyper:
|
|
|
384
384
|
except (OSError, socket.timeout):
|
|
385
385
|
return True # assume focused on error (safe default: no extra paste)
|
|
386
386
|
|
|
387
|
+
@staticmethod
|
|
388
|
+
def _ydotoold_running() -> bool:
|
|
389
|
+
"""Check if ydotoold daemon is running (required for reliable ydotool)."""
|
|
390
|
+
try:
|
|
391
|
+
result = subprocess.run(
|
|
392
|
+
["pgrep", "-x", "ydotoold"],
|
|
393
|
+
capture_output=True, timeout=2,
|
|
394
|
+
)
|
|
395
|
+
return result.returncode == 0
|
|
396
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
397
|
+
return False
|
|
398
|
+
|
|
387
399
|
def _simulate_paste(self) -> None:
|
|
388
400
|
"""Simulate Ctrl+V to paste clipboard into non-IBus apps (terminals)."""
|
|
389
401
|
try:
|
|
390
|
-
if self._ydotool:
|
|
402
|
+
if self._ydotool and self._ydotoold_running():
|
|
391
403
|
# ydotool key scancodes: 29=LCtrl, 47=V
|
|
392
404
|
subprocess.run(
|
|
393
405
|
[self._ydotool, "key", "29:1", "47:1", "47:0", "29:0"],
|
|
@@ -399,7 +411,7 @@ class IBusTyper:
|
|
|
399
411
|
capture_output=True, timeout=3,
|
|
400
412
|
)
|
|
401
413
|
else:
|
|
402
|
-
log.debug("No paste tool available (
|
|
414
|
+
log.debug("No paste tool available (ydotoold not running, wtype not found)")
|
|
403
415
|
except (subprocess.TimeoutExpired, OSError) as e:
|
|
404
416
|
log.debug("Paste simulation failed: %s", e)
|
|
405
417
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|