python-voiceio 1.17.0__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.17.0/python_voiceio.egg-info → python_voiceio-1.17.1}/PKG-INFO +1 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/pyproject.toml +1 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_security_hardening.py +88 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_setup.py +105 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_tts.py +51 -0
- python_voiceio-1.17.1/voiceio/__init__.py +1 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/app.py +6 -3
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/__init__.py +4 -4
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/doctor.py +45 -10
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/learn.py +2 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/uninstall.py +6 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/config.py +40 -3
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/__init__.py +2 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/corrections.py +3 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/errorlog.py +2 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/health.py +25 -11
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hints.py +2 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/history.py +1 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/install.py +2 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/augment.py +2 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/evaluate.py +2 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/importer.py +1 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/store.py +3 -7
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/llm.py +6 -4
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/platform.py +65 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/retention.py +2 -4
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/configfile.py +2 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/noninteractive.py +6 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/steps.py +15 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/system.py +7 -2
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/chain.py +3 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/vocab_stats.py +2 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/vocabulary.py +5 -1
- python_voiceio-1.17.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/LICENSE +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/README.md +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/setup.cfg +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_announce.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_cancel.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_cli.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_commands.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_config.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_corrections.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_decoders.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_errorlog.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_fallback.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_focus.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_gpu_fallback.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_health.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_hints.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_history.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_llm.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_numbers.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_platform.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_presence.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_prompt.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_report.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_retention.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_robustness.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_server.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_service.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_sherpa.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_spool.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_streaming.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_suggest.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_tokens.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_tray.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_vad.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/__main__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/announce.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/backends.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/commands.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/consent.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/demo.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/display.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/feedback.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/focus.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/numbers.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/power.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/presence.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/privacy.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/prompt.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/recorder.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/report.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/server/app.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/server/config.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/service.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/spool.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/streaming.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/suggest.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tokens.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/ui.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/vad.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/wordfreq.py +0 -0
|
@@ -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
|
)
|
|
@@ -212,8 +212,8 @@ def _entry_point() -> None:
|
|
|
212
212
|
import traceback
|
|
213
213
|
tb = traceback.format_exc()
|
|
214
214
|
try:
|
|
215
|
-
from voiceio.config import LOG_DIR
|
|
216
|
-
LOG_DIR
|
|
215
|
+
from voiceio.config import LOG_DIR, private_dir
|
|
216
|
+
private_dir(LOG_DIR)
|
|
217
217
|
with open(LOG_DIR / "crash.log", "a", encoding="utf-8") as f:
|
|
218
218
|
f.write(f"\n===== {datetime.datetime.now().isoformat()} =====\n"
|
|
219
219
|
f"argv: {sys.argv}\n{tb}")
|
|
@@ -42,6 +42,35 @@ def _confirm_privileged(description: str, command_str: str) -> bool:
|
|
|
42
42
|
return answer in ("y", "yes")
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _run_privileged(description: str, argv: list[str], timeout: float,
|
|
46
|
+
cmd_str: str | None = None) -> bool | None:
|
|
47
|
+
"""Ask first, then run a root command: sudo with a terminal, else pkexec
|
|
48
|
+
(see platform.privileged_argv), else print it for the user to run.
|
|
49
|
+
|
|
50
|
+
Returns True when it ran, False when it failed or could not run, None
|
|
51
|
+
when the user declined. Raises nothing.
|
|
52
|
+
"""
|
|
53
|
+
import shlex
|
|
54
|
+
import subprocess
|
|
55
|
+
|
|
56
|
+
from voiceio import platform as plat
|
|
57
|
+
cmd_str = cmd_str or shlex.join(argv)
|
|
58
|
+
if not _confirm_privileged(description, cmd_str):
|
|
59
|
+
return None
|
|
60
|
+
run_argv = plat.privileged_argv(argv)
|
|
61
|
+
if run_argv is None:
|
|
62
|
+
print(" No terminal for sudo to ask your password, and no pkexec. Run it yourself:")
|
|
63
|
+
print(f" {cmd_str}")
|
|
64
|
+
return False
|
|
65
|
+
try:
|
|
66
|
+
subprocess.run(run_argv, check=True, timeout=timeout)
|
|
67
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
68
|
+
print(f" Failed: {e} — run the command above manually.")
|
|
69
|
+
return False
|
|
70
|
+
print(" Done.")
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
45
74
|
def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
46
75
|
"""Run diagnostic health check, offer to fix issues."""
|
|
47
76
|
if not sys.platform.startswith("linux"):
|
|
@@ -96,12 +125,17 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
96
125
|
import subprocess
|
|
97
126
|
|
|
98
127
|
if missing_deps and dep_cmd:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
print(f"
|
|
128
|
+
if dep_cmd.startswith("sudo "):
|
|
129
|
+
if _run_privileged("Install system packages", dep_cmd.split(), 600,
|
|
130
|
+
cmd_str=dep_cmd) is None:
|
|
131
|
+
print(" Skipped system packages (declined).")
|
|
132
|
+
else:
|
|
133
|
+
print(f" Fixing system packages: {dep_cmd}")
|
|
134
|
+
try:
|
|
135
|
+
subprocess.run(dep_cmd.split(), check=True, timeout=600)
|
|
136
|
+
print(" Done.")
|
|
137
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
138
|
+
print(f" Failed: {e} — run the command above manually.")
|
|
105
139
|
|
|
106
140
|
fixed_any = False
|
|
107
141
|
|
|
@@ -114,11 +148,12 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
114
148
|
if not b.ok and b.fix_cmd:
|
|
115
149
|
cmd_str = " ".join(b.fix_cmd)
|
|
116
150
|
if _is_privileged_cmd(b.fix_cmd):
|
|
117
|
-
|
|
151
|
+
ran = _run_privileged(f"Fix {b.name}", b.fix_cmd, 120, cmd_str=cmd_str)
|
|
152
|
+
if ran is None:
|
|
118
153
|
print(f" Skipped {b.name} (declined).")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
fixed_any = fixed_any or bool(ran)
|
|
155
|
+
continue
|
|
156
|
+
print(f" Fixing {b.name}: {cmd_str}")
|
|
122
157
|
try:
|
|
123
158
|
subprocess.run(b.fix_cmd, check=True, timeout=30)
|
|
124
159
|
print(" Done.")
|
|
@@ -536,7 +536,8 @@ def cmd_maintain(args: argparse.Namespace) -> None:
|
|
|
536
536
|
from voiceio.learn import quiet
|
|
537
537
|
learn_cfg = _maintain_settings(args)
|
|
538
538
|
if getattr(args, "when_idle", False) and os.environ.get(quiet.WORKER_ENV) != "1":
|
|
539
|
-
|
|
539
|
+
from voiceio.config import private_dir
|
|
540
|
+
private_dir(store.learn_dir())
|
|
540
541
|
code = quiet.run_while_away(
|
|
541
542
|
quiet.worker_argv(), max_load=learn_cfg.max_load,
|
|
542
543
|
away_minutes=learn_cfg.away_minutes, wait_hours=learn_cfg.wait_hours,
|
|
@@ -165,8 +165,13 @@ def _cmd_uninstall() -> None:
|
|
|
165
165
|
continue
|
|
166
166
|
cmd = f"sudo rm {rule_path}"
|
|
167
167
|
if _confirm_privileged(f"Remove the voiceio {what} udev rule", cmd):
|
|
168
|
+
from voiceio.platform import privileged_argv
|
|
169
|
+
argv = privileged_argv(["sudo", "rm", rule_path])
|
|
170
|
+
if argv is None:
|
|
171
|
+
print(f" No terminal for sudo to ask your password. Run manually: {cmd}")
|
|
172
|
+
continue
|
|
168
173
|
try:
|
|
169
|
-
subprocess.run(
|
|
174
|
+
subprocess.run(argv, timeout=120)
|
|
170
175
|
removed.append(rule_path)
|
|
171
176
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
172
177
|
print(f" Failed. Run manually: {cmd}")
|
|
@@ -247,6 +247,9 @@ class PostCorrectConfig:
|
|
|
247
247
|
|
|
248
248
|
@dataclass
|
|
249
249
|
class TTSConfig:
|
|
250
|
+
# With engine = "auto" this means "read aloud once an engine is
|
|
251
|
+
# installed": no engine is a quiet no-op (doctor says "off"), never a
|
|
252
|
+
# warning. Naming an engine that is missing does warn.
|
|
250
253
|
enabled: bool = True
|
|
251
254
|
engine: str = "auto" # "auto" | "piper" | "supertonic" | "espeak" | "edge-tts"
|
|
252
255
|
hotkey: str = "ctrl+alt+s" # "s" for speak
|
|
@@ -452,9 +455,17 @@ def _content_files() -> list[Path]:
|
|
|
452
455
|
]
|
|
453
456
|
|
|
454
457
|
|
|
458
|
+
def data_dir() -> Path:
|
|
459
|
+
"""$XDG_DATA_HOME/voiceio: the learning corpus and the IBus launcher.
|
|
460
|
+
Read at call time, like the learn paths built on it."""
|
|
461
|
+
base = os.environ.get("XDG_DATA_HOME", "")
|
|
462
|
+
root = Path(base) if os.path.isabs(base) else Path.home() / ".local" / "share"
|
|
463
|
+
return root / "voiceio"
|
|
464
|
+
|
|
465
|
+
|
|
455
466
|
def _content_dirs() -> list[Path]:
|
|
456
467
|
"""Directories that hold user content (0700)."""
|
|
457
|
-
return [CONFIG_DIR, LOG_DIR, RECORDINGS_DIR]
|
|
468
|
+
return [CONFIG_DIR, LOG_DIR, RECORDINGS_DIR, data_dir(), data_dir() / "learn"]
|
|
458
469
|
|
|
459
470
|
|
|
460
471
|
def _chmod(path: Path, mode: int) -> None:
|
|
@@ -467,6 +478,33 @@ def _chmod(path: Path, mode: int) -> None:
|
|
|
467
478
|
log.debug("Could not chmod %s", path, exc_info=True)
|
|
468
479
|
|
|
469
480
|
|
|
481
|
+
def private_dir(path: Path) -> Path:
|
|
482
|
+
"""Create `path` (and any missing parents) as 0700 and return it.
|
|
483
|
+
|
|
484
|
+
`mkdir(parents=True)` gives every new directory the umask's mode, which
|
|
485
|
+
is 0775 under the 002 umask of user-private-group distros (Debian,
|
|
486
|
+
Fedora): the state dir was group-readable until the daemon's startup
|
|
487
|
+
hardening caught up, and never fixed when only setup, doctor or the CLI
|
|
488
|
+
ran. Missing parents (e.g. ~/.local/state) get 0700 too, as the XDG
|
|
489
|
+
base directory spec asks. Existing parents are left alone.
|
|
490
|
+
"""
|
|
491
|
+
missing: list[Path] = []
|
|
492
|
+
p = path
|
|
493
|
+
while not p.exists() and p.parent != p:
|
|
494
|
+
missing.append(p)
|
|
495
|
+
p = p.parent
|
|
496
|
+
for d in reversed(missing):
|
|
497
|
+
try:
|
|
498
|
+
d.mkdir(mode=_SECURE_DIR)
|
|
499
|
+
except FileExistsError:
|
|
500
|
+
pass
|
|
501
|
+
_chmod(d, _SECURE_DIR) # mkdir's mode is masked by the umask
|
|
502
|
+
if (not missing and sys.platform != "win32" and path.is_dir()
|
|
503
|
+
and stat.S_IMODE(path.stat().st_mode) != _SECURE_DIR):
|
|
504
|
+
_chmod(path, _SECURE_DIR)
|
|
505
|
+
return path
|
|
506
|
+
|
|
507
|
+
|
|
470
508
|
def secure_write(path: Path, text: str, *, encoding: str = "utf-8") -> None:
|
|
471
509
|
"""Write text to a file guaranteed to be 0600 with a 0700 parent dir.
|
|
472
510
|
|
|
@@ -474,8 +512,7 @@ def secure_write(path: Path, text: str, *, encoding: str = "utf-8") -> None:
|
|
|
474
512
|
over the target, so there is no world-readable window and a crash never
|
|
475
513
|
leaves a half-written file.
|
|
476
514
|
"""
|
|
477
|
-
path.parent
|
|
478
|
-
_chmod(path.parent, _SECURE_DIR)
|
|
515
|
+
private_dir(path.parent)
|
|
479
516
|
tmp = path.with_name(f".{path.name}.tmp")
|
|
480
517
|
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, _SECURE_FILE)
|
|
481
518
|
try:
|
{python_voiceio-1.17.0 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/__init__.py
RENAMED
|
@@ -117,8 +117,8 @@ class Transcriber:
|
|
|
117
117
|
"cpu_threads": self._cfg.cpu_threads,
|
|
118
118
|
"language": language,
|
|
119
119
|
})
|
|
120
|
-
from voiceio.config import LOG_DIR
|
|
121
|
-
LOG_DIR
|
|
120
|
+
from voiceio.config import LOG_DIR, private_dir
|
|
121
|
+
private_dir(LOG_DIR)
|
|
122
122
|
self._stderr_path = LOG_DIR / "worker.log"
|
|
123
123
|
# Append (rotating at 1 MB) so a crash's stack dump survives the restart.
|
|
124
124
|
if self._stderr_path.exists() and self._stderr_path.stat().st_size > 1_000_000:
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
import threading
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from voiceio import config
|
|
10
11
|
from voiceio.config import CORRECTIONS_PATH, FLAGGED_PATH
|
|
11
12
|
|
|
12
13
|
log = logging.getLogger(__name__)
|
|
@@ -59,7 +60,7 @@ class CorrectionDict:
|
|
|
59
60
|
"""Write corrections.json atomically."""
|
|
60
61
|
with self._lock:
|
|
61
62
|
data = {orig: right for orig, right in self._corrections.values()}
|
|
62
|
-
self._path.parent
|
|
63
|
+
config.private_dir(self._path.parent)
|
|
63
64
|
tmp = self._path.with_suffix(".tmp")
|
|
64
65
|
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
65
66
|
encoding="utf-8")
|
|
@@ -110,7 +111,7 @@ class CorrectionDict:
|
|
|
110
111
|
return
|
|
111
112
|
word = word.strip()
|
|
112
113
|
try:
|
|
113
|
-
self._flagged_path.parent
|
|
114
|
+
config.private_dir(self._flagged_path.parent)
|
|
114
115
|
with open(self._flagged_path, "a", encoding="utf-8") as f:
|
|
115
116
|
f.write(word + "\n")
|
|
116
117
|
log.info("Flagged word: '%s'", word)
|
|
@@ -156,7 +156,8 @@ class RepeatedErrorNotifier(logging.Handler):
|
|
|
156
156
|
fire = len(seen[key]) >= REPEATS and state.get("notified") != today
|
|
157
157
|
if fire:
|
|
158
158
|
state["notified"] = today
|
|
159
|
-
|
|
159
|
+
from voiceio.config import private_dir
|
|
160
|
+
private_dir(self._path.parent)
|
|
160
161
|
self._path.write_text(json.dumps(state))
|
|
161
162
|
if fire:
|
|
162
163
|
notify = self._notify
|
|
@@ -222,17 +222,7 @@ def _check_features() -> FeatureGroups:
|
|
|
222
222
|
else:
|
|
223
223
|
groups.processing["LLM cleanup"] = "off (enable in config or re-run 'voiceio setup')"
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
if cfg.tts.enabled:
|
|
227
|
-
from voiceio.tts import probe_all as tts_probe_all
|
|
228
|
-
results = tts_probe_all(cfg.tts)
|
|
229
|
-
active = next((name for name, probe in results if probe.ok), None)
|
|
230
|
-
if active:
|
|
231
|
-
groups.processing["Text-to-speech"] = f"on ({active})"
|
|
232
|
-
else:
|
|
233
|
-
groups.processing["Text-to-speech"] = "enabled but no engine available"
|
|
234
|
-
else:
|
|
235
|
-
groups.processing["Text-to-speech"] = "off"
|
|
225
|
+
groups.processing["Text-to-speech"] = _tts_status(cfg.tts)
|
|
236
226
|
|
|
237
227
|
# ── Accuracy (learning stack) ──
|
|
238
228
|
|
|
@@ -301,6 +291,30 @@ def _check_features() -> FeatureGroups:
|
|
|
301
291
|
return groups
|
|
302
292
|
|
|
303
293
|
|
|
294
|
+
TTS_INSTALL_HINT = "pip install 'python-voiceio[tts]' or install espeak-ng"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _tts_status(tts_cfg) -> str:
|
|
298
|
+
"""Doctor's Text-to-speech line.
|
|
299
|
+
|
|
300
|
+
`enabled = true` with `engine = "auto"` (the default) means "read aloud
|
|
301
|
+
when an engine is installed", so no engine there is a quiet "off", not a
|
|
302
|
+
warning: the user never asked for a particular engine. Naming an engine
|
|
303
|
+
that is missing is a real misconfiguration and still warns.
|
|
304
|
+
"""
|
|
305
|
+
if not tts_cfg.enabled:
|
|
306
|
+
return "off"
|
|
307
|
+
from voiceio.tts import probe_all as tts_probe_all
|
|
308
|
+
results = tts_probe_all(tts_cfg)
|
|
309
|
+
active = next((name for name, probe in results if probe.ok), None)
|
|
310
|
+
if active:
|
|
311
|
+
return f"on ({active})"
|
|
312
|
+
if tts_cfg.engine == "auto":
|
|
313
|
+
return f"off (no engine installed: {TTS_INSTALL_HINT})"
|
|
314
|
+
reason = next((probe.reason for _, probe in results if probe.reason), "")
|
|
315
|
+
return f"enabled but {tts_cfg.engine} unavailable" + (f" ({reason})" if reason else "")
|
|
316
|
+
|
|
317
|
+
|
|
304
318
|
def _decoder_status(cfg) -> dict[str, str]:
|
|
305
319
|
"""For a sherpa-onnx model: whether it can be biased, and why not. For
|
|
306
320
|
Whisper: whether Parakeet would do better here."""
|
|
@@ -52,7 +52,8 @@ def _load_state() -> dict:
|
|
|
52
52
|
|
|
53
53
|
def _save_state(state: dict) -> None:
|
|
54
54
|
try:
|
|
55
|
-
|
|
55
|
+
from voiceio.config import private_dir
|
|
56
|
+
private_dir(_HINTS_PATH.parent)
|
|
56
57
|
_HINTS_PATH.write_text(json.dumps(state))
|
|
57
58
|
except Exception:
|
|
58
59
|
log.debug("Failed to save hints state", exc_info=True)
|
|
@@ -51,8 +51,7 @@ def append(
|
|
|
51
51
|
return
|
|
52
52
|
p = path or HISTORY_PATH
|
|
53
53
|
try:
|
|
54
|
-
p.parent
|
|
55
|
-
config._chmod(p.parent, config._SECURE_DIR)
|
|
54
|
+
config.private_dir(p.parent)
|
|
56
55
|
from voiceio import __version__
|
|
57
56
|
# The version lets `doctor` judge the current release on its own
|
|
58
57
|
# numbers instead of months of older ones.
|
|
@@ -95,7 +95,8 @@ def refresh_stale_launcher() -> bool:
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def _write_launcher() -> None:
|
|
98
|
-
|
|
98
|
+
from voiceio.config import private_dir
|
|
99
|
+
private_dir(LAUNCHER_DIR)
|
|
99
100
|
LAUNCHER_PATH.write_text(_launcher_script())
|
|
100
101
|
LAUNCHER_PATH.chmod(LAUNCHER_PATH.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
101
102
|
|