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.
Files changed (104) hide show
  1. {python_voiceio-0.3.4/python_voiceio.egg-info → python_voiceio-0.3.5}/PKG-INFO +1 -1
  2. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/pyproject.toml +1 -1
  3. {python_voiceio-0.3.4 → python_voiceio-0.3.5/python_voiceio.egg-info}/PKG-INFO +1 -1
  4. python_voiceio-0.3.5/voiceio/__init__.py +1 -0
  5. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/app.py +86 -1
  6. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/ibus.py +14 -2
  7. python_voiceio-0.3.4/voiceio/__init__.py +0 -1
  8. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/LICENSE +0 -0
  9. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/README.md +0 -0
  10. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/SOURCES.txt +0 -0
  11. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/dependency_links.txt +0 -0
  12. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/entry_points.txt +0 -0
  13. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/requires.txt +0 -0
  14. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/python_voiceio.egg-info/top_level.txt +0 -0
  15. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/setup.cfg +0 -0
  16. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_app_wiring.py +0 -0
  17. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_autocorrect.py +0 -0
  18. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_backend_probes.py +0 -0
  19. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_clipboard_read.py +0 -0
  20. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_commands.py +0 -0
  21. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_config.py +0 -0
  22. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_corrections.py +0 -0
  23. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_fallback.py +0 -0
  24. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_health.py +0 -0
  25. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_hints.py +0 -0
  26. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_history.py +0 -0
  27. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_ibus_typer.py +0 -0
  28. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_llm.py +0 -0
  29. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_llm_api.py +0 -0
  30. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_numbers.py +0 -0
  31. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_platform.py +0 -0
  32. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_postprocess.py +0 -0
  33. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_prebuffer.py +0 -0
  34. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_prompt.py +0 -0
  35. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_recorder_integration.py +0 -0
  36. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_robustness.py +0 -0
  37. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_streaming.py +0 -0
  38. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_transcriber.py +0 -0
  39. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_tts.py +0 -0
  40. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_vad.py +0 -0
  41. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_vocabulary.py +0 -0
  42. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/tests/test_wordfreq.py +0 -0
  43. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/__main__.py +0 -0
  44. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/autocorrect.py +0 -0
  45. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/backends.py +0 -0
  46. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/cli.py +0 -0
  47. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/clipboard_read.py +0 -0
  48. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/commands.py +0 -0
  49. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/config.py +0 -0
  50. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/corrections.py +0 -0
  51. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/demo.py +0 -0
  52. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/feedback.py +0 -0
  53. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/health.py +0 -0
  54. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hints.py +0 -0
  55. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/history.py +0 -0
  56. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/__init__.py +0 -0
  57. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/base.py +0 -0
  58. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/chain.py +0 -0
  59. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/evdev.py +0 -0
  60. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/pynput_backend.py +0 -0
  61. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/hotkeys/socket_backend.py +0 -0
  62. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/ibus/__init__.py +0 -0
  63. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/ibus/engine.py +0 -0
  64. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/llm.py +0 -0
  65. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/llm_api.py +0 -0
  66. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/models/__init__.py +0 -0
  67. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/models/silero_vad.onnx +0 -0
  68. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/numbers.py +0 -0
  69. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/pidlock.py +0 -0
  70. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/platform.py +0 -0
  71. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/postprocess.py +0 -0
  72. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/prompt.py +0 -0
  73. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/recorder.py +0 -0
  74. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/service.py +0 -0
  75. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/__init__.py +0 -0
  76. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/commit.wav +0 -0
  77. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/start.wav +0 -0
  78. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/sounds/stop.wav +0 -0
  79. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/streaming.py +0 -0
  80. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/transcriber.py +0 -0
  81. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/__init__.py +0 -0
  82. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_icons.py +0 -0
  83. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_indicator.py +0 -0
  84. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tray/_pystray.py +0 -0
  85. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/__init__.py +0 -0
  86. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/base.py +0 -0
  87. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/chain.py +0 -0
  88. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/edge_engine.py +0 -0
  89. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/espeak.py +0 -0
  90. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/piper_engine.py +0 -0
  91. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/tts/player.py +0 -0
  92. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/__init__.py +0 -0
  93. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/base.py +0 -0
  94. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/chain.py +0 -0
  95. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/clipboard.py +0 -0
  96. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/pynput_type.py +0 -0
  97. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/wtype.py +0 -0
  98. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/xdotool.py +0 -0
  99. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/typers/ydotool.py +0 -0
  100. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/vad.py +0 -0
  101. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/vocabulary.py +0 -0
  102. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/wizard.py +0 -0
  103. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/wordfreq.py +0 -0
  104. {python_voiceio-0.3.4 → python_voiceio-0.3.5}/voiceio/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "0.3.4"
7
+ version = "0.3.5"
8
8
  description = "Speak → text, locally, instantly."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -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 (ydotool/wtype)")
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