python-voiceio 1.9.3__tar.gz → 1.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_voiceio-1.9.3/python_voiceio.egg-info → python_voiceio-1.11.0}/PKG-INFO +12 -3
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/README.md +11 -2
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/pyproject.toml +1 -1
- {python_voiceio-1.9.3 → python_voiceio-1.11.0/python_voiceio.egg-info}/PKG-INFO +12 -3
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/SOURCES.txt +5 -0
- python_voiceio-1.11.0/tests/test_mic_gain.py +85 -0
- python_voiceio-1.11.0/tests/test_report.py +314 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_service.py +24 -2
- python_voiceio-1.11.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/app.py +34 -20
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/__init__.py +2 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/doctor.py +24 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/uninstall.py +5 -1
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/config.py +5 -1
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/feedback.py +13 -1
- python_voiceio-1.11.0/voiceio/mic_gain.py +123 -0
- python_voiceio-1.11.0/voiceio/report.py +278 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/service.py +31 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/steps.py +7 -3
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/system.py +7 -0
- python_voiceio-1.11.0/voiceio/sounds/alert.wav +0 -0
- python_voiceio-1.9.3/voiceio/__init__.py +0 -1
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/LICENSE +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/setup.cfg +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_config.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_health.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_history.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_server.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/learn.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/display.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/health.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/history.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/power.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.9.3 → python_voiceio-1.11.0}/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.11.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,7 +87,16 @@ pipx install 'python-voiceio[desktop]'
|
|
|
87
87
|
voiceio setup # model, hotkey, autostart
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
**Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Debian / Ubuntu: download voiceio_*.deb from the latest release, then
|
|
94
|
+
sudo apt install ./voiceio_*.deb
|
|
95
|
+
|
|
96
|
+
voiceio setup
|
|
97
|
+
```
|
|
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).
|
|
91
100
|
|
|
92
101
|
## Make it yours
|
|
93
102
|
|
|
@@ -121,7 +130,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
121
130
|
- **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
122
131
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
123
132
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
124
|
-
- **Distro packages**: AUR,
|
|
133
|
+
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
125
134
|
- **One model everywhere**: sync your fine-tune across your machines.
|
|
126
135
|
|
|
127
136
|
## Contributing
|
|
@@ -34,7 +34,16 @@ pipx install 'python-voiceio[desktop]'
|
|
|
34
34
|
voiceio setup # model, hotkey, autostart
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Debian / Ubuntu: download voiceio_*.deb from the latest release, then
|
|
41
|
+
sudo apt install ./voiceio_*.deb
|
|
42
|
+
|
|
43
|
+
voiceio setup
|
|
44
|
+
```
|
|
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).
|
|
38
47
|
|
|
39
48
|
## Make it yours
|
|
40
49
|
|
|
@@ -68,7 +77,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
68
77
|
- **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
69
78
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
70
79
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
71
|
-
- **Distro packages**: AUR,
|
|
80
|
+
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
72
81
|
- **One model everywhere**: sync your fine-tune across your machines.
|
|
73
82
|
|
|
74
83
|
## Contributing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,7 +87,16 @@ pipx install 'python-voiceio[desktop]'
|
|
|
87
87
|
voiceio setup # model, hotkey, autostart
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
**Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Debian / Ubuntu: download voiceio_*.deb from the latest release, then
|
|
94
|
+
sudo apt install ./voiceio_*.deb
|
|
95
|
+
|
|
96
|
+
voiceio setup
|
|
97
|
+
```
|
|
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).
|
|
91
100
|
|
|
92
101
|
## Make it yours
|
|
93
102
|
|
|
@@ -121,7 +130,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
121
130
|
- **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
122
131
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
123
132
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
124
|
-
- **Distro packages**: AUR,
|
|
133
|
+
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
125
134
|
- **One model everywhere**: sync your fine-tune across your machines.
|
|
126
135
|
|
|
127
136
|
## Contributing
|
|
@@ -52,6 +52,7 @@ tests/test_learn_review.py
|
|
|
52
52
|
tests/test_learn_targets.py
|
|
53
53
|
tests/test_llm.py
|
|
54
54
|
tests/test_llm_api.py
|
|
55
|
+
tests/test_mic_gain.py
|
|
55
56
|
tests/test_modifier_guard.py
|
|
56
57
|
tests/test_numbers.py
|
|
57
58
|
tests/test_paste_last.py
|
|
@@ -64,6 +65,7 @@ tests/test_presence.py
|
|
|
64
65
|
tests/test_prompt.py
|
|
65
66
|
tests/test_quiet_audio.py
|
|
66
67
|
tests/test_recorder_integration.py
|
|
68
|
+
tests/test_report.py
|
|
67
69
|
tests/test_retention.py
|
|
68
70
|
tests/test_robustness.py
|
|
69
71
|
tests/test_security_hardening.py
|
|
@@ -101,6 +103,7 @@ voiceio/history.py
|
|
|
101
103
|
voiceio/idle_audio.py
|
|
102
104
|
voiceio/llm.py
|
|
103
105
|
voiceio/llm_api.py
|
|
106
|
+
voiceio/mic_gain.py
|
|
104
107
|
voiceio/numbers.py
|
|
105
108
|
voiceio/pidlock.py
|
|
106
109
|
voiceio/platform.py
|
|
@@ -112,6 +115,7 @@ voiceio/privacy.py
|
|
|
112
115
|
voiceio/prompt.py
|
|
113
116
|
voiceio/quiet_audio.py
|
|
114
117
|
voiceio/recorder.py
|
|
118
|
+
voiceio/report.py
|
|
115
119
|
voiceio/retention.py
|
|
116
120
|
voiceio/service.py
|
|
117
121
|
voiceio/streaming.py
|
|
@@ -187,6 +191,7 @@ voiceio/setup/steps.py
|
|
|
187
191
|
voiceio/setup/system.py
|
|
188
192
|
voiceio/setup/ui.py
|
|
189
193
|
voiceio/sounds/__init__.py
|
|
194
|
+
voiceio/sounds/alert.wav
|
|
190
195
|
voiceio/sounds/commit.wav
|
|
191
196
|
voiceio/sounds/start.wav
|
|
192
197
|
voiceio/sounds/stop.wav
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Input gain follows the recordings: down on clipping, up when very quiet,
|
|
2
|
+
never back to a level that clipped."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from voiceio import mic_gain
|
|
6
|
+
from voiceio.mic_gain import MicGain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeSource:
|
|
10
|
+
def __init__(self, volume):
|
|
11
|
+
self.volume = volume
|
|
12
|
+
self.sets = []
|
|
13
|
+
|
|
14
|
+
def get(self):
|
|
15
|
+
return self.volume
|
|
16
|
+
|
|
17
|
+
def set(self, v):
|
|
18
|
+
self.sets.append(v)
|
|
19
|
+
self.volume = v
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _gain(volume=0.5):
|
|
24
|
+
g = MicGain(enabled=False)
|
|
25
|
+
g._source = FakeSource(volume)
|
|
26
|
+
return g
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_clipping_lowers_by_a_fifth():
|
|
30
|
+
g = _gain(0.5)
|
|
31
|
+
change = g.after_recording(peak=1.0, clip_ratio=0.02, secs=10)
|
|
32
|
+
assert change.reason == "clipping" and g._source.volume == 0.4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_never_below_the_floor():
|
|
36
|
+
g = _gain(0.11)
|
|
37
|
+
g.after_recording(1.0, 0.02, 10)
|
|
38
|
+
assert g._source.volume == mic_gain.FLOOR
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_three_quiet_recordings_raise_but_not_to_a_level_that_clipped():
|
|
42
|
+
g = _gain(0.5)
|
|
43
|
+
g.after_recording(1.0, 0.02, 10) # 0.5 clipped -> 0.4
|
|
44
|
+
for _ in range(2):
|
|
45
|
+
assert g.after_recording(0.05, 0.0, 10) is None
|
|
46
|
+
assert g.after_recording(0.05, 0.0, 10) is None # third quiet one: would reach 0.5
|
|
47
|
+
assert g._source.volume == 0.4 # capped at 0.5 * 0.8: no oscillation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_quiet_without_any_clipping_history_raises():
|
|
51
|
+
g = _gain(0.3)
|
|
52
|
+
for _ in range(3):
|
|
53
|
+
change = g.after_recording(0.05, 0.0, 10)
|
|
54
|
+
assert change.new == round(0.3 / mic_gain.STEP, 2)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_short_or_dead_recordings_dont_count():
|
|
58
|
+
g = _gain(0.3)
|
|
59
|
+
for _ in range(5):
|
|
60
|
+
assert g.after_recording(0.05, 0.0, 1.0) is None # too short
|
|
61
|
+
assert g.after_recording(0.0, 0.0, 10) is None # muted mic: not a gain problem
|
|
62
|
+
assert g._source.sets == []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_normal_level_resets_the_quiet_streak():
|
|
66
|
+
g = _gain(0.3)
|
|
67
|
+
g.after_recording(0.05, 0.0, 10)
|
|
68
|
+
g.after_recording(0.05, 0.0, 10)
|
|
69
|
+
g.after_recording(0.5, 0.0, 10)
|
|
70
|
+
assert g.after_recording(0.05, 0.0, 10) is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_a_chosen_device_is_left_alone():
|
|
74
|
+
assert not MicGain(enabled=True, device="hw:1").active
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_off_without_volume_tools(monkeypatch):
|
|
78
|
+
# conftest: quiet_audio._which finds nothing in tests
|
|
79
|
+
assert not MicGain(enabled=True).active
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_failures_never_raise():
|
|
83
|
+
g = _gain(0.5)
|
|
84
|
+
g._source.get = lambda: (_ for _ in ()).throw(RuntimeError("boom"))
|
|
85
|
+
assert g.after_recording(1.0, 0.02, 10) is None
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""`voiceio doctor --report` and the post-setup feedback line: what goes public."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from voiceio import report
|
|
11
|
+
from voiceio.config import Config
|
|
12
|
+
from voiceio.health import BackendStatus, HealthReport
|
|
13
|
+
from voiceio.platform import Platform
|
|
14
|
+
|
|
15
|
+
FAKE_HOME = "/home/zelda-tester"
|
|
16
|
+
FAKE_USER = "zelda-tester"
|
|
17
|
+
FAKE_HOST = "zeldas-thinkpad"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def identity(monkeypatch):
|
|
22
|
+
"""A fake user, home and hostname that must never reach a report."""
|
|
23
|
+
monkeypatch.setenv("HOME", FAKE_HOME)
|
|
24
|
+
monkeypatch.setenv("USER", FAKE_USER)
|
|
25
|
+
monkeypatch.setattr(report.getpass, "getuser", lambda: FAKE_USER)
|
|
26
|
+
monkeypatch.setattr(report.socket, "gethostname", lambda: FAKE_HOST)
|
|
27
|
+
monkeypatch.setattr(report._py_platform, "node", lambda: FAKE_HOST)
|
|
28
|
+
monkeypatch.setattr(report, "_homes", lambda: [FAKE_HOME])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _health(**kw) -> HealthReport:
|
|
32
|
+
defaults = dict(
|
|
33
|
+
platform=Platform(os="linux", display_server="wayland", desktop="gnome"),
|
|
34
|
+
hotkey_backends=[BackendStatus("evdev", True)],
|
|
35
|
+
typer_backends=[BackendStatus("ibus", True), BackendStatus("ydotool", False, "not installed")],
|
|
36
|
+
audio_ok=True, cli_in_path=True, tray_ok=True,
|
|
37
|
+
)
|
|
38
|
+
defaults.update(kw)
|
|
39
|
+
return HealthReport(**defaults)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _assert_private(text: str) -> None:
|
|
43
|
+
for secret in (FAKE_HOME, FAKE_USER, FAKE_HOST, "zelda"):
|
|
44
|
+
assert secret not in text.lower(), (secret, text)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── scrubbing ──
|
|
48
|
+
|
|
49
|
+
def test_scrub_removes_home_user_host_and_addresses(identity):
|
|
50
|
+
raw = (f"cannot open {FAKE_HOME}/.config/voiceio/x.toml as {FAKE_USER}@{FAKE_HOST}; "
|
|
51
|
+
"also /home/someoneelse/.cache, 192.168.1.40, fe80:0:0:0:1:2:3:4, "
|
|
52
|
+
"aa:bb:cc:dd:ee:ff, /dev/input/by-id/usb-Logitech_K120_SN12345-event-kbd, "
|
|
53
|
+
"ZELDA-TESTER shouted")
|
|
54
|
+
out = report.scrub(raw)
|
|
55
|
+
_assert_private(out)
|
|
56
|
+
for leaked in ("someoneelse", "192.168", "fe80", "aa:bb", "SN12345", "Logitech"):
|
|
57
|
+
assert leaked not in out
|
|
58
|
+
assert "~/.config/voiceio/x.toml" in out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_scrub_keeps_versions_and_backend_names(identity):
|
|
62
|
+
out = report.scrub("voiceio 1.10.0.2 · ibus · python 3.12.3")
|
|
63
|
+
assert out == "voiceio 1.10.0.2 · ibus · python 3.12.3"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_report_scrubs_check_messages_and_comment(identity):
|
|
67
|
+
health = _health(
|
|
68
|
+
hotkey_backends=[BackendStatus("evdev", False, f"{FAKE_HOME}/dev denied for {FAKE_USER}")],
|
|
69
|
+
audio_ok=False, audio_reason=f"device on {FAKE_HOST} busy",
|
|
70
|
+
warnings=[f"see {FAKE_HOME}/.local/state/voiceio/voiceio.log"],
|
|
71
|
+
)
|
|
72
|
+
text = report.build_report(health, comment=f"broke in {FAKE_HOME}/src, I'm {FAKE_USER}",
|
|
73
|
+
cfg=Config())
|
|
74
|
+
_assert_private(text)
|
|
75
|
+
assert "failed: hotkey evdev: ~/dev denied" in text
|
|
76
|
+
assert "audio: device on <host> busy" in text
|
|
77
|
+
assert "comment: broke in ~/src" in text
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_report_contains_only_backend_and_model_names(identity):
|
|
81
|
+
cfg = Config()
|
|
82
|
+
cfg.model.name = f"{FAKE_HOME}/models/my-tune"
|
|
83
|
+
cfg.model.vocabulary_file = f"{FAKE_HOME}/vocab.txt"
|
|
84
|
+
cfg.cloud.api_key = "sk-secret"
|
|
85
|
+
cfg.hotkey.key = "ctrl+alt+zelda"
|
|
86
|
+
text = report.build_report(_health(), cfg=cfg)
|
|
87
|
+
_assert_private(text)
|
|
88
|
+
assert "sk-secret" not in text and "vocab" not in text and "ctrl+alt" not in text
|
|
89
|
+
assert "model: custom (local path)" in text
|
|
90
|
+
assert "hotkey evdev" in text and "typer ibus" in text
|
|
91
|
+
assert "fallbacks unavailable: ydotool" in text
|
|
92
|
+
assert "failed: none" in text
|
|
93
|
+
assert text.splitlines()[0].startswith("voiceio ")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_report_lists_failures_and_warnings():
|
|
97
|
+
health = _health(typer_backends=[BackendStatus("ibus", False, "daemon down")],
|
|
98
|
+
cli_in_path=False, tray_ok=False, tray_reason="no AppIndicator")
|
|
99
|
+
text = report.build_report(health, missing_deps=["portaudio"], perm_issues=2, cfg=Config())
|
|
100
|
+
assert "typer none" in text
|
|
101
|
+
assert "typer ibus: daemon down" in text
|
|
102
|
+
assert "missing system deps" in text
|
|
103
|
+
assert "cli not in PATH" in text and "tray: no AppIndicator" in text
|
|
104
|
+
assert "insecure permissions on 2 file(s)" in text
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_comment_is_one_bounded_line():
|
|
108
|
+
text = report.build_report(_health(), comment="a\nb\tc " + "x" * 1000, cfg=Config())
|
|
109
|
+
line = next(ln for ln in text.splitlines() if ln.startswith("comment:"))
|
|
110
|
+
assert line.startswith("comment: a b c x")
|
|
111
|
+
assert len(line) <= len("comment: ") + report.MAX_COMMENT
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── posting ──
|
|
115
|
+
|
|
116
|
+
class _Resp(io.BytesIO):
|
|
117
|
+
def __enter__(self):
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, *a):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_post_success_reads_receipt(monkeypatch):
|
|
125
|
+
seen = []
|
|
126
|
+
|
|
127
|
+
def fake(url, timeout):
|
|
128
|
+
seen.append((url, timeout))
|
|
129
|
+
return _Resp(json.dumps({"ok": True, "receipt": {"id": "r123"}}).encode())
|
|
130
|
+
|
|
131
|
+
monkeypatch.setattr(report, "_urlopen", fake)
|
|
132
|
+
assert report.post("hello world") == (True, "r123")
|
|
133
|
+
url, timeout = seen[0]
|
|
134
|
+
assert url.startswith(report.POST_URL + "?")
|
|
135
|
+
assert "text=hello+world" in url and "format=json" in url and "request_id=voiceio-" in url
|
|
136
|
+
assert timeout <= 10
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize("body", [b'{"ok": false, "error": "rate limited"}', b"[]", b"not json",
|
|
140
|
+
b'{"ok": true}'])
|
|
141
|
+
def test_post_bad_response_is_failure(monkeypatch, body):
|
|
142
|
+
monkeypatch.setattr(report, "_urlopen", lambda url, timeout: _Resp(body))
|
|
143
|
+
ok, why = report.post("x")
|
|
144
|
+
assert not ok and why
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_post_network_error_never_raises():
|
|
148
|
+
# conftest refuses every connection: this is the offline path.
|
|
149
|
+
ok, why = report.post("x")
|
|
150
|
+
assert not ok and "ConnectionRefused" in why
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── confirm / --yes / no TTY ──
|
|
154
|
+
|
|
155
|
+
@pytest.fixture
|
|
156
|
+
def posted(monkeypatch):
|
|
157
|
+
calls: list[str] = []
|
|
158
|
+
monkeypatch.setattr(report, "post", lambda text: (calls.append(text), (True, "r1"))[1])
|
|
159
|
+
return calls
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _tty(monkeypatch, tty: bool):
|
|
163
|
+
monkeypatch.setattr(report, "_interactive", lambda: tty)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_share_asks_and_posts_on_yes(monkeypatch, capsys, posted):
|
|
167
|
+
_tty(monkeypatch, True)
|
|
168
|
+
monkeypatch.setattr("builtins.input", lambda prompt: "y")
|
|
169
|
+
assert report.share("the report") is True
|
|
170
|
+
out = capsys.readouterr().out
|
|
171
|
+
assert "PUBLICLY" in out and "the report" in out and report.BOARD_URL in out
|
|
172
|
+
assert posted == ["the report"]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize("answer", ["", "n", "no", "whatever"])
|
|
176
|
+
def test_share_defaults_to_no(monkeypatch, capsys, posted, answer):
|
|
177
|
+
_tty(monkeypatch, True)
|
|
178
|
+
monkeypatch.setattr("builtins.input", lambda prompt: answer)
|
|
179
|
+
assert report.share("r") is False
|
|
180
|
+
assert posted == []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_share_eof_is_no(monkeypatch, posted):
|
|
184
|
+
_tty(monkeypatch, True)
|
|
185
|
+
|
|
186
|
+
def eof(prompt):
|
|
187
|
+
raise EOFError
|
|
188
|
+
|
|
189
|
+
monkeypatch.setattr("builtins.input", eof)
|
|
190
|
+
assert report.share("r") is False and posted == []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_share_without_tty_prints_but_never_posts(monkeypatch, capsys, posted):
|
|
194
|
+
_tty(monkeypatch, False)
|
|
195
|
+
monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
|
|
196
|
+
assert report.share("the report") is False
|
|
197
|
+
out = capsys.readouterr().out
|
|
198
|
+
assert "the report" in out and "--yes" in out
|
|
199
|
+
assert posted == []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_share_yes_skips_prompt(monkeypatch, posted):
|
|
203
|
+
_tty(monkeypatch, False)
|
|
204
|
+
monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
|
|
205
|
+
assert report.share("r", yes=True) is True and posted == ["r"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_share_failure_is_reported_not_raised(monkeypatch, capsys):
|
|
209
|
+
assert report.share("r", yes=True) is False
|
|
210
|
+
assert "Could not confirm the post" in capsys.readouterr().out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── CLI wiring ──
|
|
214
|
+
|
|
215
|
+
@pytest.fixture
|
|
216
|
+
def doctor_env(monkeypatch, identity):
|
|
217
|
+
from voiceio import config, health
|
|
218
|
+
from voiceio import platform as plat
|
|
219
|
+
monkeypatch.setattr(health, "check_health", lambda: _health(
|
|
220
|
+
warnings=[f"something at {FAKE_HOME}/.config"]))
|
|
221
|
+
monkeypatch.setattr(health, "format_report", lambda r: "DOCTOR OUTPUT")
|
|
222
|
+
monkeypatch.setattr(health, "usage_findings", lambda: [])
|
|
223
|
+
monkeypatch.setattr(plat, "check_system_deps", lambda: [])
|
|
224
|
+
monkeypatch.setattr(config, "check_permissions", lambda: [])
|
|
225
|
+
monkeypatch.setattr("voiceio.hints.hint", lambda *a, **k: None)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _doctor(monkeypatch, capsys, *argv):
|
|
229
|
+
from voiceio.cli import main
|
|
230
|
+
monkeypatch.setattr(sys, "argv", ["voiceio", "doctor", *argv])
|
|
231
|
+
with pytest.raises(SystemExit):
|
|
232
|
+
main()
|
|
233
|
+
return capsys.readouterr().out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_doctor_report_yes_posts_scrubbed_comment(monkeypatch, capsys, doctor_env, posted):
|
|
237
|
+
out = _doctor(monkeypatch, capsys, "--report", "--yes",
|
|
238
|
+
"--comment", f"worked, {FAKE_USER} here")
|
|
239
|
+
assert len(posted) == 1
|
|
240
|
+
_assert_private(posted[0])
|
|
241
|
+
_assert_private(out)
|
|
242
|
+
assert "comment: worked, <user> here" in posted[0]
|
|
243
|
+
assert report.BOARD_URL in out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_doctor_report_without_tty_or_yes_does_not_post(monkeypatch, capsys, doctor_env, posted):
|
|
247
|
+
_tty(monkeypatch, False)
|
|
248
|
+
out = _doctor(monkeypatch, capsys, "--report")
|
|
249
|
+
assert posted == []
|
|
250
|
+
assert "voiceio " in out and "Not posted" in out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_doctor_without_report_never_posts(monkeypatch, capsys, doctor_env, posted):
|
|
254
|
+
out = _doctor(monkeypatch, capsys)
|
|
255
|
+
assert posted == [] and "PUBLICLY" not in out
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── after setup ──
|
|
259
|
+
|
|
260
|
+
def test_after_setup_enter_skips_silently(monkeypatch, capsys, posted):
|
|
261
|
+
_tty(monkeypatch, True)
|
|
262
|
+
monkeypatch.setattr("builtins.input", lambda p: "")
|
|
263
|
+
monkeypatch.setattr(report, "gather", lambda c: pytest.fail("no checks on skip"))
|
|
264
|
+
report.offer_after_setup()
|
|
265
|
+
assert posted == [] and "PUBLICLY" not in capsys.readouterr().out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_after_setup_comment_is_shown_confirmed_and_posted(monkeypatch, capsys, posted):
|
|
269
|
+
_tty(monkeypatch, True)
|
|
270
|
+
answers = iter(["went fine", "y"])
|
|
271
|
+
monkeypatch.setattr("builtins.input", lambda p: next(answers))
|
|
272
|
+
monkeypatch.setattr(report, "gather", lambda c: (None, f"voiceio x report\ncomment: {c}"))
|
|
273
|
+
report.offer_after_setup()
|
|
274
|
+
assert posted == ["voiceio x report\ncomment: went fine"]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_after_setup_never_raises(monkeypatch, capsys):
|
|
278
|
+
_tty(monkeypatch, True)
|
|
279
|
+
monkeypatch.setattr("builtins.input", lambda p: "it broke")
|
|
280
|
+
|
|
281
|
+
def boom(c):
|
|
282
|
+
raise RuntimeError("probe exploded")
|
|
283
|
+
|
|
284
|
+
monkeypatch.setattr(report, "gather", boom)
|
|
285
|
+
report.offer_after_setup() # must not raise
|
|
286
|
+
assert "Skipped" in capsys.readouterr().out
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_after_setup_needs_a_terminal(monkeypatch, posted):
|
|
290
|
+
_tty(monkeypatch, False)
|
|
291
|
+
monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
|
|
292
|
+
report.offer_after_setup()
|
|
293
|
+
assert posted == []
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_setup_wizard_offers_the_report(monkeypatch):
|
|
297
|
+
from voiceio.cli import main
|
|
298
|
+
calls = []
|
|
299
|
+
monkeypatch.setattr("voiceio.setup.run_wizard", lambda: calls.append("wizard"))
|
|
300
|
+
monkeypatch.setattr(report, "offer_after_setup", lambda: calls.append("offer"))
|
|
301
|
+
monkeypatch.setattr("voiceio.hints.hint", lambda *a, **k: None)
|
|
302
|
+
monkeypatch.setattr(sys, "argv", ["voiceio", "setup"])
|
|
303
|
+
main()
|
|
304
|
+
assert calls == ["wizard", "offer"]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_network_guard_blocks_real_hosts():
|
|
308
|
+
import socket
|
|
309
|
+
s = socket.socket()
|
|
310
|
+
try:
|
|
311
|
+
with pytest.raises(ConnectionRefusedError):
|
|
312
|
+
s.connect(("93.184.216.34", 443))
|
|
313
|
+
finally:
|
|
314
|
+
s.close()
|
|
@@ -25,6 +25,28 @@ def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
|
|
|
25
25
|
assert not (local_bin / "voiceio-doctor").is_symlink()
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def test_packaged_install_links_nothing_and_serves_its_own_bin(monkeypatch, tmp_path):
|
|
29
|
+
prefix = tmp_path / "usr-lib-voiceio"
|
|
30
|
+
(prefix / "bin").mkdir(parents=True)
|
|
31
|
+
(prefix / "bin" / "voiceio").write_text("#!/bin/sh\n")
|
|
32
|
+
(prefix / service.PACKAGED_MARKER).write_text("deb\n")
|
|
33
|
+
local_bin = tmp_path / "local-bin"
|
|
34
|
+
monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
|
|
35
|
+
monkeypatch.setattr(sys, "prefix", str(prefix))
|
|
36
|
+
|
|
37
|
+
assert service.packaged_by() == "deb"
|
|
38
|
+
assert service.package_remove_cmd() == "sudo apt remove voiceio"
|
|
39
|
+
service.install_symlinks()
|
|
40
|
+
assert not local_bin.exists()
|
|
41
|
+
# The unit's ExecStart is the package's own path, stable across upgrades.
|
|
42
|
+
assert service._find_voiceio_bin() == str((prefix / "bin" / "voiceio").resolve())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_unpackaged_install_has_no_package(monkeypatch, tmp_path):
|
|
46
|
+
monkeypatch.setattr(sys, "prefix", str(tmp_path))
|
|
47
|
+
assert service.packaged_by() is None and service.package_remove_cmd() is None
|
|
48
|
+
|
|
49
|
+
|
|
28
50
|
def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
29
51
|
calls = []
|
|
30
52
|
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
@@ -44,7 +66,7 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
|
44
66
|
"MemoryHigh=25%", "OOMScoreAdjust=500"):
|
|
45
67
|
assert f"\n{line}\n" in unit, line
|
|
46
68
|
timer = service.LEARN_TIMER_PATH.read_text()
|
|
47
|
-
assert "Persistent=true" in timer and "OnCalendar
|
|
69
|
+
assert "Persistent=true" in timer and "OnCalendar=Sun *-*-* 03:30" in timer
|
|
48
70
|
assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
|
|
49
71
|
|
|
50
72
|
service.uninstall_learn_timer()
|
|
@@ -109,7 +131,7 @@ def test_learn_schedule_comes_from_config(monkeypatch, tmp_path):
|
|
|
109
131
|
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
|
|
110
132
|
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
|
|
111
133
|
service.install_learn_timer()
|
|
112
|
-
assert "OnCalendar
|
|
134
|
+
assert "OnCalendar=Sun *-*-* 03:30" in service.LEARN_TIMER_PATH.read_text() # weekly by default
|
|
113
135
|
|
|
114
136
|
config.set_value("learn", "at", "2:05")
|
|
115
137
|
config.set_value("learn", "every", "week")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.11.0"
|