python-voiceio 1.15.2__tar.gz → 1.16.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.
- {python_voiceio-1.15.2/python_voiceio.egg-info → python_voiceio-1.16.0}/PKG-INFO +1 -1
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/pyproject.toml +1 -1
- {python_voiceio-1.15.2 → python_voiceio-1.16.0/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
- python_voiceio-1.16.0/tests/test_gpu_fallback.py +80 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_health.py +13 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_report.py +33 -4
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_service.py +26 -0
- python_voiceio-1.16.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/doctor.py +8 -5
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/__init__.py +5 -1
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/worker.py +35 -9
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/health.py +58 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/report.py +38 -7
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/service.py +28 -29
- python_voiceio-1.15.2/voiceio/__init__.py +0 -1
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/LICENSE +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/README.md +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/setup.cfg +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_announce.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_config.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_history.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_server.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_sherpa.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_spool.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tray.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/announce.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/app.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/learn.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/config.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/display.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/history.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/power.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/spool.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/wordfreq.py +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""device = "auto" with a GPU whose CUDA libraries are missing: CPU, not a crash loop."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
worker = pytest.importorskip("voiceio.core.decoders.faster_whisper.worker")
|
|
11
|
+
|
|
12
|
+
ARGS = {"model": "small", "compute_type": "int8", "language": "en"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _loader(fail_on: set[str]):
|
|
16
|
+
devices = []
|
|
17
|
+
|
|
18
|
+
def load(args, device):
|
|
19
|
+
devices.append(device)
|
|
20
|
+
if device in fail_on:
|
|
21
|
+
raise RuntimeError("Library libcublas.so.12 is not found or cannot be loaded")
|
|
22
|
+
return SimpleNamespace(device=device)
|
|
23
|
+
return load, devices
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_auto_falls_back_to_cpu_when_cuda_fails_at_first_encode(capsys):
|
|
27
|
+
load, devices = _loader({"auto"})
|
|
28
|
+
with patch.object(worker, "_load_and_warm", load):
|
|
29
|
+
model, why = worker.load_ready({**ARGS, "device": "auto"})
|
|
30
|
+
assert model.device == "cpu" and devices == ["auto", "cpu"]
|
|
31
|
+
assert "libcublas.so.12" in why
|
|
32
|
+
assert "decoding on CPU" in capsys.readouterr().err
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_explicit_cuda_still_fails_loudly():
|
|
36
|
+
load, _ = _loader({"cuda"})
|
|
37
|
+
with patch.object(worker, "_load_and_warm", load), pytest.raises(RuntimeError):
|
|
38
|
+
worker.load_ready({**ARGS, "device": "cuda"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_working_device_reports_no_fallback():
|
|
42
|
+
load, devices = _loader(set())
|
|
43
|
+
with patch.object(worker, "_load_and_warm", load):
|
|
44
|
+
assert worker.load_ready({**ARGS, "device": "auto"})[1] == ""
|
|
45
|
+
assert devices == ["auto"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_doctor_names_missing_cuda_libs(monkeypatch):
|
|
49
|
+
from voiceio import health
|
|
50
|
+
from voiceio.config import Config
|
|
51
|
+
cfg = Config()
|
|
52
|
+
cfg.model.name, cfg.model.device = "small", "auto"
|
|
53
|
+
monkeypatch.setattr("voiceio.config.load", lambda: cfg)
|
|
54
|
+
monkeypatch.setitem(sys.modules, "ctranslate2",
|
|
55
|
+
SimpleNamespace(get_cuda_device_count=lambda: 1))
|
|
56
|
+
real = __import__("ctypes").CDLL
|
|
57
|
+
|
|
58
|
+
def cdll(name, *a, **k):
|
|
59
|
+
if name == "libcublas.so.12":
|
|
60
|
+
raise OSError(name)
|
|
61
|
+
return real("libc.so.6")
|
|
62
|
+
monkeypatch.setattr("ctypes.CDLL", cdll)
|
|
63
|
+
msg = health._gpu_libs_warning()
|
|
64
|
+
assert "libcublas.so.12" in msg and "libcudnn" not in msg and 'model.device = "cpu"' in msg
|
|
65
|
+
cfg.model.device = "cpu"
|
|
66
|
+
assert health._gpu_libs_warning() == ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_daemon_accepts_the_fallback_handshake_and_logs_it(caplog):
|
|
70
|
+
from unittest.mock import MagicMock
|
|
71
|
+
|
|
72
|
+
from voiceio.config import ModelConfig
|
|
73
|
+
from voiceio.core.decoders.faster_whisper import Transcriber
|
|
74
|
+
proc = MagicMock()
|
|
75
|
+
proc.stdout.readline.return_value = "READY cpu-fallback: libcublas.so.12 missing\n"
|
|
76
|
+
proc.poll.return_value = None
|
|
77
|
+
with patch("voiceio.core.decoders.faster_whisper.subprocess.Popen", return_value=proc), \
|
|
78
|
+
caplog.at_level("WARNING"):
|
|
79
|
+
Transcriber(ModelConfig())
|
|
80
|
+
assert "decoding on CPU instead: libcublas.so.12 missing" in caplog.text
|
|
@@ -149,3 +149,16 @@ def test_check_ibus_asks_about_voiceio_only_when_the_engine_is_silent():
|
|
|
149
149
|
alive.reset_mock()
|
|
150
150
|
assert health._check_ibus().voiceio_running is True
|
|
151
151
|
alive.assert_not_called()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_kde_wayland_without_ibus_input_method_warns(tmp_path):
|
|
155
|
+
from voiceio.health import _kde_input_method_warning
|
|
156
|
+
kde = Platform(os="linux", display_server="wayland", desktop="kde")
|
|
157
|
+
rc = tmp_path / "kwinrc"
|
|
158
|
+
rc.write_text("[Wayland]\nInputMethod[$e]=/usr/share/applications/com.github.maliit.keyboard.desktop\n")
|
|
159
|
+
assert "Virtual Keyboard" in _kde_input_method_warning(kde, rc)
|
|
160
|
+
rc.write_text("[Wayland]\nInputMethod[$e]=/usr/share/applications/org.freedesktop.IBus.Panel.Wayland.Gtk3.desktop\n")
|
|
161
|
+
assert _kde_input_method_warning(kde, rc) == ""
|
|
162
|
+
assert "Virtual Keyboard" in _kde_input_method_warning(kde, tmp_path / "missing")
|
|
163
|
+
gnome = Platform(os="linux", display_server="wayland", desktop="gnome")
|
|
164
|
+
assert _kde_input_method_warning(gnome, tmp_path / "missing") == ""
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
6
|
import sys
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
@@ -96,12 +97,12 @@ def test_report_contains_only_backend_and_model_names(identity):
|
|
|
96
97
|
def test_report_lists_failures_and_warnings():
|
|
97
98
|
health = _health(typer_backends=[BackendStatus("ibus", False, "daemon down")],
|
|
98
99
|
cli_in_path=False, tray_ok=False, tray_reason="no AppIndicator")
|
|
99
|
-
text = report.build_report(health, missing_deps=["portaudio"], perm_issues=
|
|
100
|
+
text = report.build_report(health, missing_deps=["portaudio"], perm_issues=[Path(FAKE_HOME) / ".config/voiceio/config.toml"], cfg=Config())
|
|
100
101
|
assert "typer none" in text
|
|
101
102
|
assert "typer ibus: daemon down" in text
|
|
102
103
|
assert "missing system deps" in text
|
|
103
104
|
assert "cli not in PATH" in text and "tray: no AppIndicator" in text
|
|
104
|
-
assert "insecure permissions on
|
|
105
|
+
assert "insecure permissions on ~/.config/voiceio/config.toml" in text
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
def test_silent_ibus_engine_is_expected_while_voiceio_is_stopped():
|
|
@@ -109,9 +110,9 @@ def test_silent_ibus_engine_is_expected_while_voiceio_is_stopped():
|
|
|
109
110
|
ib = IBusStatus(component_installed=True, daemon_running=True, env_persisted=True)
|
|
110
111
|
stopped = report.build_report(_health(ibus_checks=IBusStatus(**{**vars(ib), "voiceio_running": False})),
|
|
111
112
|
cfg=Config())
|
|
112
|
-
assert "engine ping" not in stopped and "voiceio not running" in stopped
|
|
113
|
+
assert "engine ping" not in stopped and "voiceio was not running when this report was made" in stopped
|
|
113
114
|
running = report.build_report(_health(ibus_checks=ib), cfg=Config())
|
|
114
|
-
assert "ibus engine ping" in running and "
|
|
115
|
+
assert "ibus engine ping" in running and "not running" not in running
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
def test_comment_is_one_bounded_line():
|
|
@@ -121,6 +122,34 @@ def test_comment_is_one_bounded_line():
|
|
|
121
122
|
assert len(line) <= len("comment: ") + report.MAX_COMMENT
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def test_long_comment_ends_with_an_ellipsis_at_a_word():
|
|
126
|
+
text = report.build_report(_health(), comment="word " * 400, cfg=Config())
|
|
127
|
+
line = next(ln for ln in text.splitlines() if ln.startswith("comment:"))
|
|
128
|
+
assert line.endswith("word…") and len(line) <= len("comment: ") + report.MAX_COMMENT
|
|
129
|
+
short = report.build_report(_health(), comment="fine " * 100, cfg=Config())
|
|
130
|
+
assert "…" not in short
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_works_line_only_for_a_known_answer():
|
|
134
|
+
assert "works: no" in report.build_report(_health(), works="no", cfg=Config()).splitlines()
|
|
135
|
+
assert "works:" not in report.build_report(_health(), works="maybe", cfg=Config())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_many_insecure_paths_are_counted(identity):
|
|
139
|
+
paths = [Path(FAKE_HOME) / f"f{i}" for i in range(7)]
|
|
140
|
+
text = report.build_report(_health(), perm_issues=paths, cfg=Config())
|
|
141
|
+
assert "~/f0, ~/f1, ~/f2, ~/f3, ~/f4 and 2 more" in text
|
|
142
|
+
_assert_private(text)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_ask_details_maps_answers(monkeypatch):
|
|
146
|
+
answers = iter([" it typed twice ", "Y"])
|
|
147
|
+
monkeypatch.setattr(report, "_ask", lambda prompt: next(answers))
|
|
148
|
+
assert report.ask_details() == ("it typed twice", "yes")
|
|
149
|
+
answers = iter(["", "whatever"])
|
|
150
|
+
assert report.ask_details() == ("", "")
|
|
151
|
+
|
|
152
|
+
|
|
124
153
|
def test_comment_drops_terminal_escapes():
|
|
125
154
|
# A real 1.15.0 report arrived with "comment: \x1bv" (Alt+V at the prompt).
|
|
126
155
|
text = report.build_report(_health(), comment="\x1bv", cfg=Config())
|
|
@@ -168,3 +168,29 @@ def test_reinstalling_the_service_refreshes_learning_units_that_are_on(monkeypat
|
|
|
168
168
|
monkeypatch.setattr(service, "refresh_learn_timer", lambda: calls.append("refresh"))
|
|
169
169
|
monkeypatch.setattr(service.subprocess, "run", lambda *a, **k: None)
|
|
170
170
|
assert service.install_service() and calls == ["refresh"]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_slow_daemon_reload_still_enables_the_service(tmp_path, monkeypatch):
|
|
174
|
+
# Mint report: daemon-reload hit its 5s timeout and setup never ran enable.
|
|
175
|
+
import subprocess
|
|
176
|
+
from voiceio import service
|
|
177
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
178
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
|
|
179
|
+
monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
|
|
180
|
+
monkeypatch.setattr(service, "SERVICE_PATH", tmp_path / service.SERVICE_NAME)
|
|
181
|
+
for name in ("install_symlinks", "uninstall_correct_timer", "refresh_learn_timer"):
|
|
182
|
+
monkeypatch.setattr(service, name, lambda: None)
|
|
183
|
+
ran = []
|
|
184
|
+
|
|
185
|
+
def run(cmd, **kw):
|
|
186
|
+
ran.append(cmd[2])
|
|
187
|
+
assert kw["timeout"] == service.SYSTEMCTL_TIMEOUT
|
|
188
|
+
if cmd[2] == "daemon-reload":
|
|
189
|
+
raise subprocess.TimeoutExpired(cmd, kw["timeout"])
|
|
190
|
+
return subprocess.CompletedProcess(cmd, 1 if cmd[2] == enable_fails else 0)
|
|
191
|
+
monkeypatch.setattr(service.subprocess, "run", run)
|
|
192
|
+
enable_fails = None
|
|
193
|
+
assert service.install_service()
|
|
194
|
+
assert ran == ["daemon-reload", "disable", "enable"]
|
|
195
|
+
enable_fails = "enable"
|
|
196
|
+
assert not service.install_service()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.16.0"
|
|
@@ -87,7 +87,7 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
87
87
|
from voiceio.hints import hint
|
|
88
88
|
hint("vocab_suggest", "Run 'voiceio vocab suggest' to find names it keeps mishearing")
|
|
89
89
|
if args.report:
|
|
90
|
-
_share_report(args, report, missing_deps,
|
|
90
|
+
_share_report(args, report, missing_deps, [p for p, _, _ in perm_issues])
|
|
91
91
|
sys.exit(0 if (report.all_ok and not perm_issues and not missing_deps) else 1)
|
|
92
92
|
|
|
93
93
|
# Auto-fix mode
|
|
@@ -199,16 +199,19 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
199
199
|
|
|
200
200
|
if args.report:
|
|
201
201
|
_share_report(args, report, plat.check_system_deps(),
|
|
202
|
-
|
|
202
|
+
[p for p, _, _ in _config.check_permissions()])
|
|
203
203
|
sys.exit(0 if report.all_ok else 1)
|
|
204
204
|
|
|
205
205
|
|
|
206
206
|
def _share_report(args: argparse.Namespace, report, missing_deps: list[str],
|
|
207
|
-
perm_issues:
|
|
208
|
-
from voiceio.report import build_report, share
|
|
207
|
+
perm_issues: list) -> None:
|
|
208
|
+
from voiceio.report import _interactive, ask_details, build_report, share
|
|
209
|
+
comment, works = args.comment or "", ""
|
|
210
|
+
if not comment and not args.yes and _interactive():
|
|
211
|
+
comment, works = ask_details()
|
|
209
212
|
try:
|
|
210
213
|
text = build_report(report, missing_deps=missing_deps, perm_issues=perm_issues,
|
|
211
|
-
comment=
|
|
214
|
+
comment=comment, works=works)
|
|
212
215
|
except Exception as e: # noqa: BLE001 — the report must not take doctor down
|
|
213
216
|
print(f"\nCould not build the report ({type(e).__name__}); nothing was posted.")
|
|
214
217
|
return
|
{python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/__init__.py
RENAMED
|
@@ -134,12 +134,16 @@ class Transcriber:
|
|
|
134
134
|
|
|
135
135
|
t0 = time.monotonic()
|
|
136
136
|
ready = self._read_with_timeout(WORKER_START_TIMEOUT)
|
|
137
|
-
if ready is None or ready.strip() != "READY":
|
|
137
|
+
if ready is None or ready.split(" ", 1)[0].strip() != "READY":
|
|
138
138
|
self._kill_worker()
|
|
139
139
|
detail = "timed out" if ready is None else f"said {ready.strip()!r}"
|
|
140
140
|
raise DecodeError(
|
|
141
141
|
f"worker failed to start ({detail} after {time.monotonic() - t0:.0f}s)"
|
|
142
142
|
)
|
|
143
|
+
if fallback := ready.strip().partition("cpu-fallback: ")[2]:
|
|
144
|
+
log.warning("GPU unusable, decoding on CPU instead: %s "
|
|
145
|
+
"(voiceio doctor shows the fix; model.device = \"cpu\" silences this)",
|
|
146
|
+
fallback)
|
|
143
147
|
elapsed = time.monotonic() - t0
|
|
144
148
|
log.info("Model ready (%.1fs)", elapsed)
|
|
145
149
|
|
{python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/worker.py
RENAMED
|
@@ -135,6 +135,39 @@ def decode(model: WhisperModel, audio: np.ndarray, kwargs: dict) -> list:
|
|
|
135
135
|
kwargs.get("initial_prompt"))})
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _load_and_warm(args: dict, device: str) -> WhisperModel:
|
|
139
|
+
model = load_model(
|
|
140
|
+
args["model"], device=device, compute_type=args["compute_type"],
|
|
141
|
+
cpu_threads=int(args.get("cpu_threads") or 0),
|
|
142
|
+
)
|
|
143
|
+
# Warmup: the first transcription is always slow. It also runs the first
|
|
144
|
+
# encode, where a CUDA build missing libcublas/libcudnn actually fails.
|
|
145
|
+
segs, _ = model.transcribe(np.zeros(16000, dtype=np.float32),
|
|
146
|
+
language=args.get("language"), beam_size=1)
|
|
147
|
+
list(segs)
|
|
148
|
+
return model
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_ready(args: dict) -> tuple[WhisperModel, str]:
|
|
152
|
+
"""The warmed-up model, and why it fell back to CPU ("" if it didn't).
|
|
153
|
+
|
|
154
|
+
device "auto" picks CUDA whenever a GPU is visible, even when the CUDA
|
|
155
|
+
libraries CTranslate2 needs are missing (a torch for another CUDA major
|
|
156
|
+
pulled in by the train extra): that failed every start and crash-looped
|
|
157
|
+
the service. On "auto" a GPU that can't run is a CPU decode instead;
|
|
158
|
+
an explicit "cuda" still fails loudly.
|
|
159
|
+
"""
|
|
160
|
+
device = args["device"]
|
|
161
|
+
try:
|
|
162
|
+
return _load_and_warm(args, device), ""
|
|
163
|
+
except Exception as e: # noqa: BLE001
|
|
164
|
+
if device != "auto":
|
|
165
|
+
raise
|
|
166
|
+
reason = " ".join(str(e).split())[:200] or type(e).__name__
|
|
167
|
+
print(f"voiceio: GPU unusable ({reason}); decoding on CPU", file=sys.stderr, flush=True)
|
|
168
|
+
return _load_and_warm(args, "cpu"), reason
|
|
169
|
+
|
|
170
|
+
|
|
138
171
|
def main() -> None:
|
|
139
172
|
# A hung decode is killed by the parent after a timeout; SIGUSR1 first
|
|
140
173
|
# dumps every thread's stack to stderr (worker.log) so hangs are diagnosable.
|
|
@@ -142,15 +175,8 @@ def main() -> None:
|
|
|
142
175
|
faulthandler.register(signal.SIGUSR1, all_threads=True)
|
|
143
176
|
|
|
144
177
|
args = json.loads(sys.argv[1])
|
|
145
|
-
model =
|
|
146
|
-
|
|
147
|
-
cpu_threads=int(args.get("cpu_threads") or 0),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Warmup: first transcription is always slow
|
|
151
|
-
segs, _ = model.transcribe(np.zeros(16000, dtype=np.float32), language=args.get("language"), beam_size=1)
|
|
152
|
-
list(segs)
|
|
153
|
-
print("READY", flush=True)
|
|
178
|
+
model, fallback = load_ready(args)
|
|
179
|
+
print(f"READY cpu-fallback: {fallback}" if fallback else "READY", flush=True)
|
|
154
180
|
|
|
155
181
|
for line in sys.stdin:
|
|
156
182
|
if line.strip() == "QUIT":
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from voiceio import platform as plat
|
|
8
9
|
from voiceio.hotkeys import chain as hotkey_chain
|
|
@@ -125,9 +126,66 @@ def check_health(p: plat.Platform | None = None) -> HealthReport:
|
|
|
125
126
|
"Grant it in System Settings → Privacy & Security → Accessibility."
|
|
126
127
|
)
|
|
127
128
|
|
|
129
|
+
if gpu := _gpu_libs_warning():
|
|
130
|
+
report.warnings.append(gpu)
|
|
131
|
+
if kde := _kde_input_method_warning(report.platform):
|
|
132
|
+
report.warnings.append(kde)
|
|
133
|
+
|
|
128
134
|
return report
|
|
129
135
|
|
|
130
136
|
|
|
137
|
+
def _kde_input_method_warning(p: plat.Platform, kwinrc: Path | None = None) -> str:
|
|
138
|
+
"""KWin on Wayland only hands text to the input method picked under
|
|
139
|
+
Virtual Keyboard; without IBus there, voiceio has no live preview."""
|
|
140
|
+
if not (p.desktop == "kde" and p.display_server == "wayland"):
|
|
141
|
+
return ""
|
|
142
|
+
import configparser
|
|
143
|
+
rc = configparser.ConfigParser(interpolation=None, strict=False)
|
|
144
|
+
try:
|
|
145
|
+
rc.read(kwinrc or Path.home() / ".config" / "kwinrc")
|
|
146
|
+
except configparser.Error:
|
|
147
|
+
return ""
|
|
148
|
+
# KWin writes it as "InputMethod[$e]" (a KConfig flag), so match the prefix.
|
|
149
|
+
section = rc["Wayland"] if rc.has_section("Wayland") else {}
|
|
150
|
+
if any(k.startswith("inputmethod") and "ibus" in v.lower() for k, v in section.items()):
|
|
151
|
+
return ""
|
|
152
|
+
return ("KDE isn't using IBus as its Wayland input method, so there's no live preview. "
|
|
153
|
+
"System Settings → Keyboard → Virtual Keyboard → IBus Wayland, then log out and in.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# What CTranslate2 4.x's CUDA build loads at the first encode.
|
|
157
|
+
_CUDA_LIBS = ("libcublas.so.12", "libcudnn.so.9")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _gpu_libs_warning() -> str:
|
|
161
|
+
"""Why Whisper can't use the GPU it sees, when that's a missing CUDA
|
|
162
|
+
library (a torch for another CUDA major, pulled in by the train extra,
|
|
163
|
+
is the usual way). The daemon then decodes on CPU instead."""
|
|
164
|
+
try:
|
|
165
|
+
from voiceio.config import load
|
|
166
|
+
cfg = load().model
|
|
167
|
+
from voiceio.core.decoders.catalog import is_whisper
|
|
168
|
+
if cfg.device not in ("auto", "cuda") or not is_whisper(cfg.name):
|
|
169
|
+
return ""
|
|
170
|
+
import ctranslate2
|
|
171
|
+
if ctranslate2.get_cuda_device_count() == 0:
|
|
172
|
+
return ""
|
|
173
|
+
except Exception: # noqa: BLE001 — no faster-whisper, or no CUDA build
|
|
174
|
+
return ""
|
|
175
|
+
import ctypes
|
|
176
|
+
missing = []
|
|
177
|
+
for lib in _CUDA_LIBS:
|
|
178
|
+
try:
|
|
179
|
+
ctypes.CDLL(lib)
|
|
180
|
+
except OSError:
|
|
181
|
+
missing.append(lib)
|
|
182
|
+
if not missing:
|
|
183
|
+
return ""
|
|
184
|
+
return (f"A GPU is present but {', '.join(missing)} can't be loaded, so Whisper "
|
|
185
|
+
"decodes on CPU. Set model.device = \"cpu\" in config.toml to stay on CPU, "
|
|
186
|
+
"or install the CUDA 12 runtime (cuBLAS 12 and cuDNN 9) to use the GPU.")
|
|
187
|
+
|
|
188
|
+
|
|
131
189
|
def _check_features() -> FeatureGroups:
|
|
132
190
|
"""Check which features are active based on config and files."""
|
|
133
191
|
groups = FeatureGroups()
|
|
@@ -19,6 +19,7 @@ import urllib.parse
|
|
|
19
19
|
import urllib.request
|
|
20
20
|
import uuid
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from collections.abc import Sequence
|
|
22
23
|
from typing import TYPE_CHECKING
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
@@ -27,7 +28,10 @@ if TYPE_CHECKING:
|
|
|
27
28
|
BOARD_URL = "https://voiceio.dev/#feedback"
|
|
28
29
|
POST_URL = "https://swarmmemo.com/w/voiceio/feedback"
|
|
29
30
|
TIMEOUT_SECS = 8.0
|
|
30
|
-
MAX_COMMENT =
|
|
31
|
+
MAX_COMMENT = 1000
|
|
32
|
+
# Insecure paths named in a report; more are counted.
|
|
33
|
+
MAX_PERM_PATHS = 5
|
|
34
|
+
WORKS = ("yes", "no", "not tried")
|
|
31
35
|
MAX_DETAIL = 90
|
|
32
36
|
|
|
33
37
|
# A terminal escape: CSI sequences (arrow keys) or ESC plus one key (Alt+key).
|
|
@@ -126,8 +130,9 @@ def build_report(
|
|
|
126
130
|
health: HealthReport,
|
|
127
131
|
*,
|
|
128
132
|
missing_deps: list[str] | None = None,
|
|
129
|
-
perm_issues:
|
|
133
|
+
perm_issues: Sequence[Path] = (),
|
|
130
134
|
comment: str = "",
|
|
135
|
+
works: str = "",
|
|
131
136
|
cfg=None,
|
|
132
137
|
) -> str:
|
|
133
138
|
"""The exact public text: short, plain, and scrubbed."""
|
|
@@ -168,7 +173,9 @@ def build_report(
|
|
|
168
173
|
if not health.tray_ok:
|
|
169
174
|
warned.append(f"tray: {_short(health.tray_reason)}" if health.tray_reason else "tray")
|
|
170
175
|
if perm_issues:
|
|
171
|
-
|
|
176
|
+
named = ", ".join(str(p) for p in perm_issues[:MAX_PERM_PATHS])
|
|
177
|
+
more = len(perm_issues) - MAX_PERM_PATHS
|
|
178
|
+
warned.append(f"insecure permissions on {named}" + (f" and {more} more" if more > 0 else ""))
|
|
172
179
|
unavailable = [b.name for chosen, chain in ((hotkey, health.hotkey_backends),
|
|
173
180
|
(typer, health.typer_backends))
|
|
174
181
|
if chosen is not None for b in chain if not b.ok]
|
|
@@ -181,8 +188,8 @@ def build_report(
|
|
|
181
188
|
f"backends: hotkey {hotkey.name if hotkey else 'none'} · "
|
|
182
189
|
f"typer {typer.name if typer else 'none'} · audio {audio} "
|
|
183
190
|
f"(vad {cfg.audio.vad_backend})"
|
|
184
|
-
+ (" · voiceio not running
|
|
185
|
-
and not health.ibus_checks.voiceio_running else ""),
|
|
191
|
+
+ (" · voiceio was not running when this report was made"
|
|
192
|
+
if health.ibus_checks is not None and not health.ibus_checks.voiceio_running else ""),
|
|
186
193
|
f"model: {_model_label(cfg.model.name)}",
|
|
187
194
|
]
|
|
188
195
|
if unavailable:
|
|
@@ -191,7 +198,9 @@ def build_report(
|
|
|
191
198
|
if warned:
|
|
192
199
|
lines.append("warnings: " + "; ".join(warned))
|
|
193
200
|
comment = "".join(c if c.isprintable() else " " for c in _ESCAPE.sub("", comment))
|
|
194
|
-
|
|
201
|
+
if works in WORKS:
|
|
202
|
+
lines.append(f"works: {works}")
|
|
203
|
+
comment = _clip(" ".join(comment.split()), MAX_COMMENT)
|
|
195
204
|
if comment:
|
|
196
205
|
lines.append(f"comment: {comment}")
|
|
197
206
|
return scrub("\n".join(lines))
|
|
@@ -233,6 +242,28 @@ def _urlopen(url: str, *, timeout: float):
|
|
|
233
242
|
return urllib.request.urlopen(req, timeout=timeout)
|
|
234
243
|
|
|
235
244
|
|
|
245
|
+
def _clip(text: str, limit: int) -> str:
|
|
246
|
+
"""At most `limit` characters, cut at a word with an ellipsis so a cut
|
|
247
|
+
comment doesn't read as the user's own last word."""
|
|
248
|
+
if len(text) <= limit:
|
|
249
|
+
return text
|
|
250
|
+
cut = text[:limit - 1]
|
|
251
|
+
word = cut.rsplit(" ", 1)[0]
|
|
252
|
+
return (word if len(word) >= len(cut) - 40 else cut) + "…"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def ask_details() -> tuple[str, str]:
|
|
256
|
+
"""Two questions for a report filed from a terminal: what happened, and
|
|
257
|
+
whether dictation types into apps. Enter skips either."""
|
|
258
|
+
comment = _ask("\n What happened? One or two sentences for the public board "
|
|
259
|
+
"(Enter to skip): ").strip()
|
|
260
|
+
answer = _ask(" Does dictation type text into your apps? [y/n/not tried, Enter to skip]: ")
|
|
261
|
+
answer = answer.strip().lower()
|
|
262
|
+
works = {"y": "yes", "yes": "yes", "n": "no", "no": "no",
|
|
263
|
+
"not tried": "not tried", "t": "not tried", "nt": "not tried"}.get(answer, "")
|
|
264
|
+
return comment, works
|
|
265
|
+
|
|
266
|
+
|
|
236
267
|
def _interactive() -> bool:
|
|
237
268
|
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
238
269
|
|
|
@@ -279,7 +310,7 @@ def gather(comment: str = "") -> tuple[HealthReport, str]:
|
|
|
279
310
|
return health, build_report(
|
|
280
311
|
health,
|
|
281
312
|
missing_deps=plat.check_system_deps(),
|
|
282
|
-
perm_issues=
|
|
313
|
+
perm_issues=[p for p, _, _ in config.check_permissions()],
|
|
283
314
|
comment=comment,
|
|
284
315
|
)
|
|
285
316
|
|
|
@@ -149,10 +149,16 @@ WantedBy=timers.target
|
|
|
149
149
|
return service, timer
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
# systemd --user can take well over 5s to answer on a busy or freshly booted
|
|
153
|
+
# session (a Mint report: daemon-reload timed out and setup never enabled the
|
|
154
|
+
# service).
|
|
155
|
+
SYSTEMCTL_TIMEOUT = 30
|
|
156
|
+
|
|
157
|
+
|
|
152
158
|
def _systemctl(*args: str) -> bool:
|
|
153
159
|
try:
|
|
154
160
|
return subprocess.run(["systemctl", "--user", *args], capture_output=True,
|
|
155
|
-
timeout=
|
|
161
|
+
timeout=SYSTEMCTL_TIMEOUT).returncode == 0
|
|
156
162
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
157
163
|
return False
|
|
158
164
|
|
|
@@ -233,7 +239,7 @@ def uninstall_correct_timer() -> None:
|
|
|
233
239
|
return
|
|
234
240
|
try:
|
|
235
241
|
subprocess.run(["systemctl", "--user", "disable", "--now", CORRECT_TIMER_NAME],
|
|
236
|
-
capture_output=True, timeout=
|
|
242
|
+
capture_output=True, timeout=SYSTEMCTL_TIMEOUT)
|
|
237
243
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
238
244
|
pass
|
|
239
245
|
for p in (CORRECT_TIMER_PATH, CORRECT_SERVICE_PATH):
|
|
@@ -291,30 +297,23 @@ def install_service() -> bool:
|
|
|
291
297
|
SERVICE_PATH.write_text(_service_unit(bin_path))
|
|
292
298
|
log.info("Installed systemd service to %s", SERVICE_PATH)
|
|
293
299
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
)
|
|
306
|
-
subprocess.run(
|
|
307
|
-
["systemctl", "--user", "enable", SERVICE_NAME],
|
|
308
|
-
capture_output=True, timeout=5,
|
|
309
|
-
)
|
|
310
|
-
log.info("Enabled %s", SERVICE_NAME)
|
|
311
|
-
uninstall_correct_timer()
|
|
312
|
-
refresh_learn_timer()
|
|
313
|
-
install_symlinks() # the CLI must run the same install as the service
|
|
314
|
-
return True
|
|
315
|
-
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
316
|
-
log.warning("Could not enable service: %s", e)
|
|
300
|
+
# Each step on its own: a slow daemon-reload is worth a warning, not a
|
|
301
|
+
# reason to skip enabling (enable reloads the unit it links anyway).
|
|
302
|
+
if not _systemctl("daemon-reload"):
|
|
303
|
+
log.warning("systemctl --user daemon-reload failed or timed out; enabling anyway")
|
|
304
|
+
# disable before enable so a prior [Install] target (older versions
|
|
305
|
+
# wired WantedBy=default.target, which never restarts the service on a
|
|
306
|
+
# GNOME logout/login) is unlinked instead of left as a stale want.
|
|
307
|
+
_systemctl("disable", SERVICE_NAME)
|
|
308
|
+
if not _systemctl("enable", SERVICE_NAME):
|
|
309
|
+
log.warning("Could not enable %s: run `systemctl --user enable %s`",
|
|
310
|
+
SERVICE_NAME, SERVICE_NAME)
|
|
317
311
|
return False
|
|
312
|
+
log.info("Enabled %s", SERVICE_NAME)
|
|
313
|
+
uninstall_correct_timer()
|
|
314
|
+
refresh_learn_timer()
|
|
315
|
+
install_symlinks() # the CLI must run the same install as the service
|
|
316
|
+
return True
|
|
318
317
|
|
|
319
318
|
|
|
320
319
|
def uninstall_service() -> bool:
|
|
@@ -325,11 +324,11 @@ def uninstall_service() -> bool:
|
|
|
325
324
|
try:
|
|
326
325
|
subprocess.run(
|
|
327
326
|
["systemctl", "--user", "disable", SERVICE_NAME],
|
|
328
|
-
capture_output=True, timeout=
|
|
327
|
+
capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
|
|
329
328
|
)
|
|
330
329
|
subprocess.run(
|
|
331
330
|
["systemctl", "--user", "stop", SERVICE_NAME],
|
|
332
|
-
capture_output=True, timeout=
|
|
331
|
+
capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
|
|
333
332
|
)
|
|
334
333
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
335
334
|
pass
|
|
@@ -343,7 +342,7 @@ def uninstall_service() -> bool:
|
|
|
343
342
|
try:
|
|
344
343
|
subprocess.run(
|
|
345
344
|
["systemctl", "--user", "daemon-reload"],
|
|
346
|
-
capture_output=True, timeout=
|
|
345
|
+
capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
|
|
347
346
|
)
|
|
348
347
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
349
348
|
pass
|
|
@@ -388,7 +387,7 @@ def start_service() -> bool:
|
|
|
388
387
|
try:
|
|
389
388
|
result = subprocess.run(
|
|
390
389
|
["systemctl", "--user", "start", SERVICE_NAME],
|
|
391
|
-
capture_output=True, timeout=
|
|
390
|
+
capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
|
|
392
391
|
)
|
|
393
392
|
return result.returncode == 0
|
|
394
393
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.15.2"
|
|
File without changes
|
|
File without changes
|
{python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|