python-voiceio 1.16.1__tar.gz → 1.17.1__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.16.1/python_voiceio.egg-info → python_voiceio-1.17.1}/PKG-INFO +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/README.md +1 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/pyproject.toml +1 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1/python_voiceio.egg-info}/PKG-INFO +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/SOURCES.txt +2 -0
- python_voiceio-1.17.1/tests/test_errorlog.py +136 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_security_hardening.py +88 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_setup.py +105 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tts.py +51 -0
- python_voiceio-1.17.1/voiceio/__init__.py +1 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/app.py +6 -3
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/__init__.py +8 -5
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/doctor.py +66 -16
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/learn.py +2 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/uninstall.py +6 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/config.py +40 -3
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/__init__.py +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/corrections.py +3 -2
- python_voiceio-1.17.1/voiceio/errorlog.py +178 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/health.py +25 -11
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hints.py +2 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/history.py +1 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/install.py +2 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/augment.py +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/evaluate.py +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/importer.py +1 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/store.py +3 -7
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/llm.py +6 -4
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/platform.py +65 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/report.py +5 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/retention.py +2 -4
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/configfile.py +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/noninteractive.py +6 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/steps.py +15 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/system.py +7 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/__init__.py +1 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_indicator.py +4 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_pystray.py +1 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/chain.py +3 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vocab_stats.py +2 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vocabulary.py +5 -1
- python_voiceio-1.16.1/voiceio/__init__.py +0 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/LICENSE +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/setup.cfg +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_announce.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_cancel.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_cli.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_commands.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_config.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_corrections.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_decoders.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_fallback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_focus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_gpu_fallback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_health.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_hints.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_history.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_llm.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_numbers.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_platform.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_presence.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_prompt.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_report.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_retention.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_robustness.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_server.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_service.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_sherpa.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_spool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_streaming.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_suggest.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tokens.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tray.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_vad.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/__main__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/announce.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/backends.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/commands.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/consent.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/demo.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/display.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/feedback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/focus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/numbers.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/power.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/presence.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/privacy.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/prompt.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/recorder.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/app.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/config.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/service.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/spool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/streaming.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/suggest.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tokens.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ui.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vad.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/wordfreq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.1
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
96
96
|
voiceio setup
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
99
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
100
100
|
|
|
101
101
|
## Make it yours
|
|
102
102
|
|
|
@@ -43,7 +43,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
43
43
|
voiceio setup
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
46
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
47
47
|
|
|
48
48
|
## Make it yours
|
|
49
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.1
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
96
96
|
voiceio setup
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
99
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
100
100
|
|
|
101
101
|
## Make it yours
|
|
102
102
|
|
|
@@ -24,6 +24,7 @@ tests/test_corrections.py
|
|
|
24
24
|
tests/test_dead_mic.py
|
|
25
25
|
tests/test_decoders.py
|
|
26
26
|
tests/test_engine.py
|
|
27
|
+
tests/test_errorlog.py
|
|
27
28
|
tests/test_evdev_uaccess.py
|
|
28
29
|
tests/test_fallback.py
|
|
29
30
|
tests/test_fcitx_bridge.py
|
|
@@ -101,6 +102,7 @@ voiceio/context_terms.py
|
|
|
101
102
|
voiceio/corrections.py
|
|
102
103
|
voiceio/demo.py
|
|
103
104
|
voiceio/display.py
|
|
105
|
+
voiceio/errorlog.py
|
|
104
106
|
voiceio/feedback.py
|
|
105
107
|
voiceio/focus.py
|
|
106
108
|
voiceio/health.py
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""The last error in a report, and the service's repeated-error nudge."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from voiceio import errorlog
|
|
8
|
+
|
|
9
|
+
NOW = dt.datetime(2026, 11, 5, 15, 0, 0)
|
|
10
|
+
SECRET = "please wire the money to my sister on Friday"
|
|
11
|
+
PKG = errorlog._PKG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _log(tmp_path, text):
|
|
15
|
+
path = tmp_path / "voiceio.log"
|
|
16
|
+
path.write_text(text)
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SERVICE_LOG = f"""\
|
|
21
|
+
2026-11-05 12:58:10,961 voiceio INFO Recording started
|
|
22
|
+
2026-11-05 12:58:11,000 voiceio.streaming ERROR Streaming failed on: {SECRET}
|
|
23
|
+
Traceback (most recent call last):
|
|
24
|
+
File "{PKG}/core/decoders/sherpa.py", line 280, in decode
|
|
25
|
+
stream = self._rec.create_stream(hotwords)
|
|
26
|
+
AttributeError: 'NoneType' object has no attribute 'create_stream'
|
|
27
|
+
|
|
28
|
+
The above exception was the direct cause of the following exception:
|
|
29
|
+
|
|
30
|
+
Traceback (most recent call last):
|
|
31
|
+
File "{PKG}/streaming.py", line 488, in _transcribe_and_apply
|
|
32
|
+
result = self._decoder.decode(tail)
|
|
33
|
+
File "/usr/lib/python3/dist-packages/numpy/core.py", line 9, in f
|
|
34
|
+
pass
|
|
35
|
+
voiceio.core.decoders.base.DecodeError: decode failed: {SECRET}
|
|
36
|
+
Word
|
|
37
|
+
{SECRET}
|
|
38
|
+
2026-11-05 12:58:12,000 voiceio INFO voiceio stopped
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_last_error_names_only_type_frame_and_age(tmp_path):
|
|
43
|
+
got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
|
|
44
|
+
assert got == "DecodeError at voiceio/streaming.py:488 (2 h ago)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_dictated_text_in_a_log_never_reaches_the_report(tmp_path):
|
|
48
|
+
from voiceio.health import BackendStatus, HealthReport
|
|
49
|
+
from voiceio.platform import Platform
|
|
50
|
+
from voiceio.report import build_report
|
|
51
|
+
from voiceio.config import Config
|
|
52
|
+
|
|
53
|
+
err = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
|
|
54
|
+
health = HealthReport(
|
|
55
|
+
platform=Platform(os="linux", display_server="wayland", desktop="gnome"),
|
|
56
|
+
hotkey_backends=[BackendStatus("evdev", True)],
|
|
57
|
+
typer_backends=[BackendStatus("ibus", True)], audio_ok=True)
|
|
58
|
+
text = build_report(health, last_error=err, cfg=Config())
|
|
59
|
+
assert "last error: DecodeError at voiceio/streaming.py:488" in text
|
|
60
|
+
for word in ("money", "sister", "Friday", "Word", "NoneType", "create_stream"):
|
|
61
|
+
assert word not in text
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_an_error_without_traceback_names_its_logger(tmp_path):
|
|
65
|
+
log = f"2026-11-05 14:30:00,000 voiceio.recorder ERROR mic gone: {SECRET}\n"
|
|
66
|
+
got = errorlog.last_error(_log(tmp_path, log), tmp_path / "none", now=NOW)
|
|
67
|
+
assert got == "logged by voiceio.recorder (30 min ago)"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_errors_older_than_a_week_are_left_out(tmp_path):
|
|
71
|
+
old = SERVICE_LOG.replace("2026-11-05 12:58:11", "2026-10-20 12:58:11")
|
|
72
|
+
assert errorlog.last_error(_log(tmp_path, old), tmp_path / "none", now=NOW) == ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_crash_log_keeps_the_frame_not_the_argv(tmp_path):
|
|
76
|
+
crash = tmp_path / "crash.log"
|
|
77
|
+
crash.write_text(f"""
|
|
78
|
+
===== 2026-11-04T14:00:00.123456 =====
|
|
79
|
+
argv: ['voiceio', 'say', '{SECRET}']
|
|
80
|
+
Traceback (most recent call last):
|
|
81
|
+
File "/home/someone/src/voiceio/voiceio/cli/__init__.py", line 205, in _entry_point
|
|
82
|
+
main()
|
|
83
|
+
File "/home/someone/.venv/lib/python3.13/site-packages/numpy/__init__.py", line 1, in <module>
|
|
84
|
+
x
|
|
85
|
+
ImportError: cannot import {SECRET}
|
|
86
|
+
""")
|
|
87
|
+
got = errorlog.last_error(tmp_path / "none", crash, now=NOW)
|
|
88
|
+
assert got == "ImportError at voiceio/cli/__init__.py:205 (1 d ago)"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_the_newest_of_service_and_crash_errors_wins(tmp_path):
|
|
92
|
+
crash = tmp_path / "crash.log"
|
|
93
|
+
crash.write_text("===== 2026-11-05T14:59:00 =====\nargv: []\n"
|
|
94
|
+
"Traceback (most recent call last):\n"
|
|
95
|
+
f' File "{PKG}/app.py", line 7, in run\n x\nKeyError: 1\n')
|
|
96
|
+
got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), crash, now=NOW)
|
|
97
|
+
assert got == "KeyError at voiceio/app.py:7 (1 min ago)"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _record(msg, *args):
|
|
101
|
+
return logging.LogRecord("voiceio.streaming", logging.ERROR, __file__, 1, msg, args, None)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_the_same_error_three_times_notifies_once_a_day(tmp_path):
|
|
105
|
+
sent = []
|
|
106
|
+
h = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
|
|
107
|
+
notify=lambda title, body: sent.append(body))
|
|
108
|
+
for n in range(2):
|
|
109
|
+
h.emit(_record("decode failed: %s", f"words {n}"))
|
|
110
|
+
h.emit(_record("something else"))
|
|
111
|
+
assert sent == []
|
|
112
|
+
h.emit(_record("decode failed: %s", "more words"))
|
|
113
|
+
assert sent == ["Run 'voiceio doctor --report' to tell us."]
|
|
114
|
+
# A restarted daemon reads the same state: no second nudge today.
|
|
115
|
+
h2 = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
|
|
116
|
+
notify=lambda title, body: sent.append(body))
|
|
117
|
+
for _ in range(3):
|
|
118
|
+
h2.emit(_record("decode failed: %s", "x"))
|
|
119
|
+
assert len(sent) == 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_doctor_offers_a_report_only_after_a_failure_in_a_terminal(monkeypatch):
|
|
123
|
+
import argparse
|
|
124
|
+
|
|
125
|
+
from voiceio import report
|
|
126
|
+
from voiceio.cli.doctor import _offer_report
|
|
127
|
+
|
|
128
|
+
asked = []
|
|
129
|
+
monkeypatch.setattr(report, "_ask", lambda prompt: asked.append(prompt) or "y")
|
|
130
|
+
monkeypatch.setattr(report, "_interactive", lambda: True)
|
|
131
|
+
args = argparse.Namespace(yes=False)
|
|
132
|
+
assert not _offer_report(args, healthy=True)
|
|
133
|
+
assert not _offer_report(argparse.Namespace(yes=True), healthy=False)
|
|
134
|
+
assert _offer_report(args, healthy=False) and "Something isn't working" in asked[0]
|
|
135
|
+
monkeypatch.setattr(report, "_interactive", lambda: False)
|
|
136
|
+
assert not _offer_report(args, healthy=False)
|
|
@@ -85,6 +85,94 @@ def test_check_permissions_flags_loose_and_clears_after_harden(tmp_path, monkeyp
|
|
|
85
85
|
assert config.check_permissions() == []
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
@pytest.fixture
|
|
89
|
+
def umask_002():
|
|
90
|
+
"""The user-private-group umask of Debian/Fedora desktops: plain mkdir
|
|
91
|
+
gives 0775, which is how the reported ~/.local/state/voiceio got there."""
|
|
92
|
+
import os
|
|
93
|
+
old = os.umask(0o002)
|
|
94
|
+
try:
|
|
95
|
+
yield
|
|
96
|
+
finally:
|
|
97
|
+
os.umask(old)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@posix_only
|
|
101
|
+
def test_private_dir_creates_0700_with_missing_parents(tmp_path, umask_002):
|
|
102
|
+
(tmp_path / "home").mkdir(mode=0o755)
|
|
103
|
+
target = tmp_path / "home" / ".local" / "state" / "voiceio"
|
|
104
|
+
assert config.private_dir(target) == target
|
|
105
|
+
assert _mode(target) == 0o700
|
|
106
|
+
assert _mode(target.parent) == 0o700 # XDG: created dirs are 0700
|
|
107
|
+
assert _mode(tmp_path / "home") == 0o755 # existing parents untouched
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@posix_only
|
|
111
|
+
def test_private_dir_tightens_existing_lax_dir(tmp_path):
|
|
112
|
+
d = tmp_path / "voiceio"
|
|
113
|
+
d.mkdir()
|
|
114
|
+
d.chmod(0o775)
|
|
115
|
+
config.private_dir(d)
|
|
116
|
+
assert _mode(d) == 0o700
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@posix_only
|
|
120
|
+
def test_crash_log_dir_is_private(tmp_path, monkeypatch, umask_002, capsys):
|
|
121
|
+
from voiceio import cli
|
|
122
|
+
log_dir = tmp_path / "state" / "voiceio"
|
|
123
|
+
monkeypatch.setattr(config, "LOG_DIR", log_dir)
|
|
124
|
+
|
|
125
|
+
def boom():
|
|
126
|
+
raise RuntimeError("crash before logging")
|
|
127
|
+
|
|
128
|
+
monkeypatch.setattr(cli, "main", boom)
|
|
129
|
+
with pytest.raises(SystemExit):
|
|
130
|
+
cli._entry_point()
|
|
131
|
+
assert (log_dir / "crash.log").exists()
|
|
132
|
+
assert _mode(log_dir) == 0o700
|
|
133
|
+
assert _mode(log_dir.parent) == 0o700
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@posix_only
|
|
137
|
+
def test_error_notifier_state_dir_is_private(tmp_path, umask_002):
|
|
138
|
+
import logging
|
|
139
|
+
from voiceio.errorlog import RepeatedErrorNotifier
|
|
140
|
+
state = tmp_path / "state" / "voiceio" / "error-notify.json"
|
|
141
|
+
handler = RepeatedErrorNotifier(state_path=state, notify=lambda *a, **k: None)
|
|
142
|
+
handler.emit(logging.LogRecord("voiceio.x", logging.ERROR, __file__, 1, "boom", (), None))
|
|
143
|
+
assert state.exists()
|
|
144
|
+
assert _mode(state.parent) == 0o700
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@posix_only
|
|
148
|
+
def test_secure_write_missing_parents_are_private(tmp_path, umask_002):
|
|
149
|
+
target = tmp_path / "config" / "voiceio" / "config.toml"
|
|
150
|
+
config.secure_write(target, "x = 1\n")
|
|
151
|
+
assert _mode(target.parent) == 0o700
|
|
152
|
+
assert _mode(target.parent.parent) == 0o700
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@posix_only
|
|
156
|
+
def test_label_store_dirs_are_private(tmp_path, monkeypatch, umask_002):
|
|
157
|
+
from voiceio.learn import store
|
|
158
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share"))
|
|
159
|
+
store.add_label("a.wav", [], source="test")
|
|
160
|
+
assert _mode(store.learn_dir()) == 0o700
|
|
161
|
+
assert _mode(config.data_dir()) == 0o700
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@posix_only
|
|
165
|
+
def test_check_permissions_flags_lax_learn_dir(tmp_path, monkeypatch):
|
|
166
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share"))
|
|
167
|
+
learn = config.data_dir() / "learn"
|
|
168
|
+
learn.mkdir(parents=True)
|
|
169
|
+
learn.chmod(0o775)
|
|
170
|
+
assert learn in {p for p, _, _ in config.check_permissions()}
|
|
171
|
+
config.harden_permissions()
|
|
172
|
+
assert _mode(learn) == 0o700
|
|
173
|
+
assert learn not in {p for p, _, _ in config.check_permissions()}
|
|
174
|
+
|
|
175
|
+
|
|
88
176
|
# ── udev rule (replaces sudo chmod 0666 /dev/uinput) ─────────────────────
|
|
89
177
|
|
|
90
178
|
def test_uinput_udev_rule_content():
|
|
@@ -280,3 +280,108 @@ def test_setup_offers_nightly_learning_once(monkeypatch):
|
|
|
280
280
|
steps._offer_nightly_learning(state)
|
|
281
281
|
steps._offer_nightly_learning(state) # already on: not asked again
|
|
282
282
|
assert installed == [1] and len(asked) == 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── root commands with no terminal (agent, launcher, GUI) ────────────
|
|
286
|
+
|
|
287
|
+
def _no_terminal(monkeypatch, *, sudo_cached=False, pkexec=True, display=True):
|
|
288
|
+
monkeypatch.setattr(plat, "_has_tty", lambda: False)
|
|
289
|
+
monkeypatch.setattr(plat, "_sudo_without_password", lambda: sudo_cached)
|
|
290
|
+
monkeypatch.setattr(plat.shutil, "which",
|
|
291
|
+
lambda name: "/usr/bin/pkexec" if pkexec and name == "pkexec" else None)
|
|
292
|
+
monkeypatch.delenv("DISPLAY", raising=False)
|
|
293
|
+
if display:
|
|
294
|
+
monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0")
|
|
295
|
+
else:
|
|
296
|
+
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_privileged_argv_unchanged_with_a_terminal(monkeypatch):
|
|
300
|
+
monkeypatch.setattr(plat, "_has_tty", lambda: True)
|
|
301
|
+
argv = ["sudo", "apt", "install", "ibus"]
|
|
302
|
+
assert plat.privileged_argv(argv) == argv
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_privileged_argv_leaves_unprivileged_commands_alone(monkeypatch):
|
|
306
|
+
_no_terminal(monkeypatch, pkexec=False)
|
|
307
|
+
assert plat.privileged_argv(["brew", "install", "x"]) == ["brew", "install", "x"]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_privileged_argv_uses_pkexec_without_a_terminal(monkeypatch):
|
|
311
|
+
_no_terminal(monkeypatch)
|
|
312
|
+
assert plat.privileged_argv(["sudo", "apt", "install", "ibus", "gcc"]) == [
|
|
313
|
+
"pkexec", "apt", "install", "-y", "ibus", "gcc"]
|
|
314
|
+
assert plat.privileged_argv(["sudo", "pacman", "-S", "ibus"]) == [
|
|
315
|
+
"pkexec", "pacman", "-S", "--noconfirm", "ibus"]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_privileged_argv_runs_shell_scripts_with_inner_sudo_under_pkexec(monkeypatch):
|
|
319
|
+
_no_terminal(monkeypatch)
|
|
320
|
+
script = ["sh", "-c", "printf x | sudo tee /etc/udev/rules.d/r > /dev/null"]
|
|
321
|
+
assert plat.privileged_argv(script) == ["pkexec", *script]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_privileged_argv_prefers_cached_sudo(monkeypatch):
|
|
325
|
+
_no_terminal(monkeypatch, sudo_cached=True)
|
|
326
|
+
assert plat.privileged_argv(["sudo", "apt", "install", "x"]) == [
|
|
327
|
+
"sudo", "-n", "apt", "install", "-y", "x"]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.parametrize("pkexec,display", [(False, True), (True, False)])
|
|
331
|
+
def test_privileged_argv_gives_up_without_pkexec_or_desktop(monkeypatch, pkexec, display):
|
|
332
|
+
_no_terminal(monkeypatch, pkexec=pkexec, display=display)
|
|
333
|
+
assert plat.privileged_argv(["sudo", "apt", "install", "x"]) is None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_noninteractive_install_deps_without_terminal_prints_command(cfg_paths, monkeypatch, capsys):
|
|
337
|
+
_patch_noninteractive(monkeypatch)
|
|
338
|
+
monkeypatch.setattr("voiceio.platform.check_system_deps", lambda: ["portaudio"])
|
|
339
|
+
monkeypatch.setattr(plat, "system_deps_install_cmd", lambda keys: "sudo apt install libportaudio2")
|
|
340
|
+
monkeypatch.setattr(plat, "privileged_argv", lambda argv: None)
|
|
341
|
+
ran = []
|
|
342
|
+
monkeypatch.setattr(noninteractive.subprocess, "run", lambda *a, **k: ran.append(a))
|
|
343
|
+
code = noninteractive.run_setup_noninteractive({}, install_deps=True)
|
|
344
|
+
assert code == 4
|
|
345
|
+
assert ran == []
|
|
346
|
+
assert "sudo apt install libportaudio2" in capsys.readouterr().out
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_noninteractive_install_deps_runs_pkexec_argv(cfg_paths, monkeypatch):
|
|
350
|
+
_patch_noninteractive(monkeypatch)
|
|
351
|
+
state = {"missing": ["portaudio"]}
|
|
352
|
+
monkeypatch.setattr("voiceio.platform.check_system_deps", lambda: state["missing"])
|
|
353
|
+
monkeypatch.setattr(plat, "system_deps_install_cmd", lambda keys: "sudo apt install libportaudio2")
|
|
354
|
+
_no_terminal(monkeypatch)
|
|
355
|
+
ran = []
|
|
356
|
+
|
|
357
|
+
class _Done:
|
|
358
|
+
returncode = 0
|
|
359
|
+
|
|
360
|
+
def fake_run(argv, **kw):
|
|
361
|
+
ran.append(argv)
|
|
362
|
+
state["missing"] = []
|
|
363
|
+
return _Done()
|
|
364
|
+
|
|
365
|
+
monkeypatch.setattr(noninteractive.subprocess, "run", fake_run)
|
|
366
|
+
assert noninteractive.run_setup_noninteractive({}, install_deps=True) == 0
|
|
367
|
+
assert ran == [["pkexec", "apt", "install", "-y", "libportaudio2"]]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_doctor_privileged_fix_asks_first_and_never_runs_when_declined(monkeypatch):
|
|
371
|
+
from voiceio.cli import doctor
|
|
372
|
+
ran = []
|
|
373
|
+
monkeypatch.setattr("subprocess.run", lambda *a, **k: ran.append(a))
|
|
374
|
+
monkeypatch.setattr(doctor, "_confirm_privileged", lambda d, c: False)
|
|
375
|
+
assert doctor._run_privileged("Fix x", ["sudo", "true"], 5) is None
|
|
376
|
+
assert ran == []
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_doctor_privileged_fix_without_terminal_prints_command(monkeypatch, capsys):
|
|
380
|
+
from voiceio.cli import doctor
|
|
381
|
+
ran = []
|
|
382
|
+
monkeypatch.setattr("subprocess.run", lambda *a, **k: ran.append(a))
|
|
383
|
+
monkeypatch.setattr(doctor, "_confirm_privileged", lambda d, c: True)
|
|
384
|
+
monkeypatch.setattr(plat, "privileged_argv", lambda argv: None)
|
|
385
|
+
assert doctor._run_privileged("Fix x", ["sudo", "udevadm", "trigger"], 5) is False
|
|
386
|
+
assert ran == []
|
|
387
|
+
assert "sudo udevadm trigger" in capsys.readouterr().out
|
|
@@ -499,3 +499,54 @@ def test_chain_passes_language_to_supertonic():
|
|
|
499
499
|
from voiceio.tts import chain
|
|
500
500
|
engine = chain._create("supertonic", TTSConfig(engine="supertonic", language="de"))
|
|
501
501
|
assert engine.name == "supertonic" and engine._language == "de"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ── no engine installed: the default config stays quiet ─────────────
|
|
505
|
+
|
|
506
|
+
def _no_engines(cfg):
|
|
507
|
+
names = ["piper", "supertonic", "edge-tts", "espeak"] if cfg.engine == "auto" else [cfg.engine]
|
|
508
|
+
return [(n, ProbeResult(ok=False, reason=f"{n} not installed")) for n in names]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_doctor_tts_auto_without_engine_is_quiet_off():
|
|
512
|
+
from voiceio import health
|
|
513
|
+
with patch("voiceio.tts.probe_all", side_effect=_no_engines):
|
|
514
|
+
status = health._tts_status(TTSConfig())
|
|
515
|
+
assert status.startswith("off")
|
|
516
|
+
assert "enabled but" not in status
|
|
517
|
+
assert "⚠" not in health._feature_line("Text-to-speech", status)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def test_doctor_tts_named_engine_missing_still_warns():
|
|
521
|
+
from voiceio import health
|
|
522
|
+
with patch("voiceio.tts.probe_all", side_effect=_no_engines):
|
|
523
|
+
status = health._tts_status(TTSConfig(engine="piper"))
|
|
524
|
+
assert status.startswith("enabled but piper unavailable")
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def test_doctor_tts_on_when_engine_present():
|
|
528
|
+
from voiceio import health
|
|
529
|
+
with patch("voiceio.tts.probe_all",
|
|
530
|
+
return_value=[("piper", ProbeResult(ok=False, reason="x")),
|
|
531
|
+
("espeak", ProbeResult(ok=True))]):
|
|
532
|
+
assert health._tts_status(TTSConfig()) == "on (espeak)"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_doctor_tts_disabled_is_off():
|
|
536
|
+
from voiceio import health
|
|
537
|
+
assert health._tts_status(TTSConfig(enabled=False)) == "off"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_auto_select_without_engine_logs_no_warning(caplog):
|
|
541
|
+
import logging
|
|
542
|
+
from voiceio.tts.chain import select, _create
|
|
543
|
+
|
|
544
|
+
def none_ok(name, cfg):
|
|
545
|
+
engine = _create(name, cfg)
|
|
546
|
+
engine.probe = lambda: ProbeResult(ok=False, reason="not installed")
|
|
547
|
+
return engine
|
|
548
|
+
|
|
549
|
+
with patch("voiceio.tts.chain._create", side_effect=none_ok), \
|
|
550
|
+
caplog.at_level(logging.DEBUG, logger="voiceio.tts.chain"):
|
|
551
|
+
assert select(TTSConfig()) is None
|
|
552
|
+
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.17.1"
|
|
@@ -202,8 +202,11 @@ class VoiceIO:
|
|
|
202
202
|
# Create a second hotkey backend for TTS
|
|
203
203
|
self._tts_hotkey = hotkey_chain.select(platform, cfg.hotkey.backend)
|
|
204
204
|
log.info("TTS: engine=%s, hotkey=%s", self._tts_engine.name, cfg.tts.hotkey)
|
|
205
|
+
elif cfg.tts.engine == "auto":
|
|
206
|
+
# The default config: read aloud once an engine is installed.
|
|
207
|
+
log.info("TTS: no engine installed, read-aloud hotkey off")
|
|
205
208
|
else:
|
|
206
|
-
log.warning("TTS
|
|
209
|
+
log.warning("TTS: engine '%s' is configured but unavailable", cfg.tts.engine)
|
|
207
210
|
|
|
208
211
|
# Optional key that cancels a dictation (read only, never grabbed: the
|
|
209
212
|
# key also reaches the focused app). Needs a backend that sees keys.
|
|
@@ -1007,11 +1010,11 @@ class VoiceIO:
|
|
|
1007
1010
|
self._cancel_hotkey.start(self.cfg.hotkey.cancel_key, self.cancel)
|
|
1008
1011
|
|
|
1009
1012
|
def run(self) -> None:
|
|
1010
|
-
from voiceio.config import PID_PATH, LOG_DIR
|
|
1013
|
+
from voiceio.config import PID_PATH, LOG_DIR, private_dir
|
|
1011
1014
|
|
|
1012
1015
|
self._typers.redetect_at_start()
|
|
1013
1016
|
|
|
1014
|
-
LOG_DIR
|
|
1017
|
+
private_dir(LOG_DIR)
|
|
1015
1018
|
self._pid_fd = open(PID_PATH, "a") # not "w": the running daemon's PID stays
|
|
1016
1019
|
try:
|
|
1017
1020
|
from voiceio.pidlock import lock_pid_file
|
|
@@ -112,9 +112,9 @@ def _cmd_run(args: argparse.Namespace) -> None:
|
|
|
112
112
|
console.setLevel(getattr(logging, cfg.daemon.log_level))
|
|
113
113
|
|
|
114
114
|
# File: always log DEBUG to rotating file
|
|
115
|
-
from voiceio.config import LOG_DIR, LOG_PATH
|
|
115
|
+
from voiceio.config import LOG_DIR, LOG_PATH, private_dir
|
|
116
116
|
from logging.handlers import RotatingFileHandler
|
|
117
|
-
LOG_DIR
|
|
117
|
+
private_dir(LOG_DIR)
|
|
118
118
|
file_handler = RotatingFileHandler(
|
|
119
119
|
str(LOG_PATH), maxBytes=2_000_000, backupCount=2,
|
|
120
120
|
)
|
|
@@ -125,6 +125,8 @@ def _cmd_run(args: argparse.Namespace) -> None:
|
|
|
125
125
|
|
|
126
126
|
logging.basicConfig(level=logging.DEBUG, handlers=[console, file_handler])
|
|
127
127
|
logging.getLogger("voiceio").setLevel(getattr(logging, cfg.daemon.log_level))
|
|
128
|
+
from voiceio.errorlog import RepeatedErrorNotifier
|
|
129
|
+
logging.getLogger("voiceio").addHandler(RepeatedErrorNotifier())
|
|
128
130
|
|
|
129
131
|
log = logging.getLogger("voiceio")
|
|
130
132
|
from voiceio import __version__, platform as plat
|
|
@@ -210,12 +212,13 @@ def _entry_point() -> None:
|
|
|
210
212
|
import traceback
|
|
211
213
|
tb = traceback.format_exc()
|
|
212
214
|
try:
|
|
213
|
-
from voiceio.config import LOG_DIR
|
|
214
|
-
LOG_DIR
|
|
215
|
+
from voiceio.config import LOG_DIR, private_dir
|
|
216
|
+
private_dir(LOG_DIR)
|
|
215
217
|
with open(LOG_DIR / "crash.log", "a", encoding="utf-8") as f:
|
|
216
218
|
f.write(f"\n===== {datetime.datetime.now().isoformat()} =====\n"
|
|
217
219
|
f"argv: {sys.argv}\n{tb}")
|
|
218
|
-
print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]"
|
|
220
|
+
print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]\n"
|
|
221
|
+
"Report it: voiceio doctor --report", file=sys.stderr)
|
|
219
222
|
except Exception: # noqa: BLE001 — last resort
|
|
220
223
|
pass
|
|
221
224
|
print(tb, file=sys.stderr)
|