python-voiceio 1.16.1__tar.gz → 1.17.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.16.1/python_voiceio.egg-info → python_voiceio-1.17.0}/PKG-INFO +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/README.md +1 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/pyproject.toml +1 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.0/python_voiceio.egg-info}/PKG-INFO +2 -2
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
- python_voiceio-1.17.0/tests/test_errorlog.py +136 -0
- python_voiceio-1.17.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/__init__.py +4 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/doctor.py +21 -6
- python_voiceio-1.17.0/voiceio/errorlog.py +177 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/report.py +5 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tray/__init__.py +1 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tray/_indicator.py +4 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tray/_pystray.py +1 -0
- python_voiceio-1.16.1/voiceio/__init__.py +0 -1
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/LICENSE +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/setup.cfg +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_announce.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_config.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_gpu_fallback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_health.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_history.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_report.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_server.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_service.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_sherpa.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_spool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_tray.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/announce.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/app.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/learn.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/config.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/display.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/health.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/history.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/power.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/service.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/spool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.16.1 → python_voiceio-1.17.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.17.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
96
96
|
voiceio setup
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
99
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
100
100
|
|
|
101
101
|
## Make it yours
|
|
102
102
|
|
|
@@ -43,7 +43,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
43
43
|
voiceio setup
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
46
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
47
47
|
|
|
48
48
|
## Make it yours
|
|
49
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
|
|
|
96
96
|
voiceio setup
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
99
|
+
Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
|
|
100
100
|
|
|
101
101
|
## Make it yours
|
|
102
102
|
|
|
@@ -24,6 +24,7 @@ tests/test_corrections.py
|
|
|
24
24
|
tests/test_dead_mic.py
|
|
25
25
|
tests/test_decoders.py
|
|
26
26
|
tests/test_engine.py
|
|
27
|
+
tests/test_errorlog.py
|
|
27
28
|
tests/test_evdev_uaccess.py
|
|
28
29
|
tests/test_fallback.py
|
|
29
30
|
tests/test_fcitx_bridge.py
|
|
@@ -101,6 +102,7 @@ voiceio/context_terms.py
|
|
|
101
102
|
voiceio/corrections.py
|
|
102
103
|
voiceio/demo.py
|
|
103
104
|
voiceio/display.py
|
|
105
|
+
voiceio/errorlog.py
|
|
104
106
|
voiceio/feedback.py
|
|
105
107
|
voiceio/focus.py
|
|
106
108
|
voiceio/health.py
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""The last error in a report, and the service's repeated-error nudge."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from voiceio import errorlog
|
|
8
|
+
|
|
9
|
+
NOW = dt.datetime(2026, 11, 5, 15, 0, 0)
|
|
10
|
+
SECRET = "please wire the money to my sister on Friday"
|
|
11
|
+
PKG = errorlog._PKG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _log(tmp_path, text):
|
|
15
|
+
path = tmp_path / "voiceio.log"
|
|
16
|
+
path.write_text(text)
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SERVICE_LOG = f"""\
|
|
21
|
+
2026-11-05 12:58:10,961 voiceio INFO Recording started
|
|
22
|
+
2026-11-05 12:58:11,000 voiceio.streaming ERROR Streaming failed on: {SECRET}
|
|
23
|
+
Traceback (most recent call last):
|
|
24
|
+
File "{PKG}/core/decoders/sherpa.py", line 280, in decode
|
|
25
|
+
stream = self._rec.create_stream(hotwords)
|
|
26
|
+
AttributeError: 'NoneType' object has no attribute 'create_stream'
|
|
27
|
+
|
|
28
|
+
The above exception was the direct cause of the following exception:
|
|
29
|
+
|
|
30
|
+
Traceback (most recent call last):
|
|
31
|
+
File "{PKG}/streaming.py", line 488, in _transcribe_and_apply
|
|
32
|
+
result = self._decoder.decode(tail)
|
|
33
|
+
File "/usr/lib/python3/dist-packages/numpy/core.py", line 9, in f
|
|
34
|
+
pass
|
|
35
|
+
voiceio.core.decoders.base.DecodeError: decode failed: {SECRET}
|
|
36
|
+
Word
|
|
37
|
+
{SECRET}
|
|
38
|
+
2026-11-05 12:58:12,000 voiceio INFO voiceio stopped
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_last_error_names_only_type_frame_and_age(tmp_path):
|
|
43
|
+
got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
|
|
44
|
+
assert got == "DecodeError at voiceio/streaming.py:488 (2 h ago)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_dictated_text_in_a_log_never_reaches_the_report(tmp_path):
|
|
48
|
+
from voiceio.health import BackendStatus, HealthReport
|
|
49
|
+
from voiceio.platform import Platform
|
|
50
|
+
from voiceio.report import build_report
|
|
51
|
+
from voiceio.config import Config
|
|
52
|
+
|
|
53
|
+
err = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
|
|
54
|
+
health = HealthReport(
|
|
55
|
+
platform=Platform(os="linux", display_server="wayland", desktop="gnome"),
|
|
56
|
+
hotkey_backends=[BackendStatus("evdev", True)],
|
|
57
|
+
typer_backends=[BackendStatus("ibus", True)], audio_ok=True)
|
|
58
|
+
text = build_report(health, last_error=err, cfg=Config())
|
|
59
|
+
assert "last error: DecodeError at voiceio/streaming.py:488" in text
|
|
60
|
+
for word in ("money", "sister", "Friday", "Word", "NoneType", "create_stream"):
|
|
61
|
+
assert word not in text
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_an_error_without_traceback_names_its_logger(tmp_path):
|
|
65
|
+
log = f"2026-11-05 14:30:00,000 voiceio.recorder ERROR mic gone: {SECRET}\n"
|
|
66
|
+
got = errorlog.last_error(_log(tmp_path, log), tmp_path / "none", now=NOW)
|
|
67
|
+
assert got == "logged by voiceio.recorder (30 min ago)"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_errors_older_than_a_week_are_left_out(tmp_path):
|
|
71
|
+
old = SERVICE_LOG.replace("2026-11-05 12:58:11", "2026-10-20 12:58:11")
|
|
72
|
+
assert errorlog.last_error(_log(tmp_path, old), tmp_path / "none", now=NOW) == ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_crash_log_keeps_the_frame_not_the_argv(tmp_path):
|
|
76
|
+
crash = tmp_path / "crash.log"
|
|
77
|
+
crash.write_text(f"""
|
|
78
|
+
===== 2026-11-04T14:00:00.123456 =====
|
|
79
|
+
argv: ['voiceio', 'say', '{SECRET}']
|
|
80
|
+
Traceback (most recent call last):
|
|
81
|
+
File "/home/someone/src/voiceio/voiceio/cli/__init__.py", line 205, in _entry_point
|
|
82
|
+
main()
|
|
83
|
+
File "/home/someone/.venv/lib/python3.13/site-packages/numpy/__init__.py", line 1, in <module>
|
|
84
|
+
x
|
|
85
|
+
ImportError: cannot import {SECRET}
|
|
86
|
+
""")
|
|
87
|
+
got = errorlog.last_error(tmp_path / "none", crash, now=NOW)
|
|
88
|
+
assert got == "ImportError at voiceio/cli/__init__.py:205 (1 d ago)"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_the_newest_of_service_and_crash_errors_wins(tmp_path):
|
|
92
|
+
crash = tmp_path / "crash.log"
|
|
93
|
+
crash.write_text("===== 2026-11-05T14:59:00 =====\nargv: []\n"
|
|
94
|
+
"Traceback (most recent call last):\n"
|
|
95
|
+
f' File "{PKG}/app.py", line 7, in run\n x\nKeyError: 1\n')
|
|
96
|
+
got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), crash, now=NOW)
|
|
97
|
+
assert got == "KeyError at voiceio/app.py:7 (1 min ago)"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _record(msg, *args):
|
|
101
|
+
return logging.LogRecord("voiceio.streaming", logging.ERROR, __file__, 1, msg, args, None)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_the_same_error_three_times_notifies_once_a_day(tmp_path):
|
|
105
|
+
sent = []
|
|
106
|
+
h = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
|
|
107
|
+
notify=lambda title, body: sent.append(body))
|
|
108
|
+
for n in range(2):
|
|
109
|
+
h.emit(_record("decode failed: %s", f"words {n}"))
|
|
110
|
+
h.emit(_record("something else"))
|
|
111
|
+
assert sent == []
|
|
112
|
+
h.emit(_record("decode failed: %s", "more words"))
|
|
113
|
+
assert sent == ["Run 'voiceio doctor --report' to tell us."]
|
|
114
|
+
# A restarted daemon reads the same state: no second nudge today.
|
|
115
|
+
h2 = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
|
|
116
|
+
notify=lambda title, body: sent.append(body))
|
|
117
|
+
for _ in range(3):
|
|
118
|
+
h2.emit(_record("decode failed: %s", "x"))
|
|
119
|
+
assert len(sent) == 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_doctor_offers_a_report_only_after_a_failure_in_a_terminal(monkeypatch):
|
|
123
|
+
import argparse
|
|
124
|
+
|
|
125
|
+
from voiceio import report
|
|
126
|
+
from voiceio.cli.doctor import _offer_report
|
|
127
|
+
|
|
128
|
+
asked = []
|
|
129
|
+
monkeypatch.setattr(report, "_ask", lambda prompt: asked.append(prompt) or "y")
|
|
130
|
+
monkeypatch.setattr(report, "_interactive", lambda: True)
|
|
131
|
+
args = argparse.Namespace(yes=False)
|
|
132
|
+
assert not _offer_report(args, healthy=True)
|
|
133
|
+
assert not _offer_report(argparse.Namespace(yes=True), healthy=False)
|
|
134
|
+
assert _offer_report(args, healthy=False) and "Something isn't working" in asked[0]
|
|
135
|
+
monkeypatch.setattr(report, "_interactive", lambda: False)
|
|
136
|
+
assert not _offer_report(args, healthy=False)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.17.0"
|
|
@@ -125,6 +125,8 @@ def _cmd_run(args: argparse.Namespace) -> None:
|
|
|
125
125
|
|
|
126
126
|
logging.basicConfig(level=logging.DEBUG, handlers=[console, file_handler])
|
|
127
127
|
logging.getLogger("voiceio").setLevel(getattr(logging, cfg.daemon.log_level))
|
|
128
|
+
from voiceio.errorlog import RepeatedErrorNotifier
|
|
129
|
+
logging.getLogger("voiceio").addHandler(RepeatedErrorNotifier())
|
|
128
130
|
|
|
129
131
|
log = logging.getLogger("voiceio")
|
|
130
132
|
from voiceio import __version__, platform as plat
|
|
@@ -215,7 +217,8 @@ def _entry_point() -> None:
|
|
|
215
217
|
with open(LOG_DIR / "crash.log", "a", encoding="utf-8") as f:
|
|
216
218
|
f.write(f"\n===== {datetime.datetime.now().isoformat()} =====\n"
|
|
217
219
|
f"argv: {sys.argv}\n{tb}")
|
|
218
|
-
print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]"
|
|
220
|
+
print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]\n"
|
|
221
|
+
"Report it: voiceio doctor --report", file=sys.stderr)
|
|
219
222
|
except Exception: # noqa: BLE001 — last resort
|
|
220
223
|
pass
|
|
221
224
|
print(tb, file=sys.stderr)
|
|
@@ -86,9 +86,10 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
86
86
|
print("Run 'voiceio doctor --fix' to tighten file permissions.")
|
|
87
87
|
from voiceio.hints import hint
|
|
88
88
|
hint("vocab_suggest", "Run 'voiceio vocab suggest' to find names it keeps mishearing")
|
|
89
|
-
|
|
89
|
+
healthy = report.all_ok and not perm_issues and not missing_deps
|
|
90
|
+
if args.report or _offer_report(args, healthy and not report.warnings):
|
|
90
91
|
_share_report(args, report, missing_deps, [p for p, _, _ in perm_issues])
|
|
91
|
-
sys.exit(0 if
|
|
92
|
+
sys.exit(0 if healthy else 1)
|
|
92
93
|
|
|
93
94
|
# Auto-fix mode
|
|
94
95
|
print("\nAttempting fixes...\n")
|
|
@@ -197,21 +198,35 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
197
198
|
report = check_health()
|
|
198
199
|
print(format_report(report))
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
missing_deps = plat.check_system_deps()
|
|
202
|
+
perm_issues = _config.check_permissions()
|
|
203
|
+
healthy = report.all_ok and not perm_issues and not missing_deps and not report.warnings
|
|
204
|
+
if args.report or _offer_report(args, healthy):
|
|
205
|
+
_share_report(args, report, missing_deps, [p for p, _, _ in perm_issues])
|
|
203
206
|
sys.exit(0 if report.all_ok else 1)
|
|
204
207
|
|
|
205
208
|
|
|
209
|
+
def _offer_report(args: argparse.Namespace, healthy: bool) -> bool:
|
|
210
|
+
"""After a failed check, in a terminal, ask once whether to share a
|
|
211
|
+
report (default no). The report is still shown before it is posted."""
|
|
212
|
+
from voiceio.report import _ask, _interactive
|
|
213
|
+
if healthy or args.yes or not _interactive():
|
|
214
|
+
return False
|
|
215
|
+
answer = _ask("\nSomething isn't working. Post a report to the public feedback board? "
|
|
216
|
+
"You'll see it first. [y/N] ")
|
|
217
|
+
return answer.strip().lower() in ("y", "yes")
|
|
218
|
+
|
|
219
|
+
|
|
206
220
|
def _share_report(args: argparse.Namespace, report, missing_deps: list[str],
|
|
207
221
|
perm_issues: list) -> None:
|
|
222
|
+
from voiceio.errorlog import last_error
|
|
208
223
|
from voiceio.report import _interactive, ask_details, build_report, share
|
|
209
224
|
comment, works = args.comment or "", ""
|
|
210
225
|
if not comment and not args.yes and _interactive():
|
|
211
226
|
comment, works = ask_details()
|
|
212
227
|
try:
|
|
213
228
|
text = build_report(report, missing_deps=missing_deps, perm_issues=perm_issues,
|
|
214
|
-
comment=comment, works=works)
|
|
229
|
+
comment=comment, works=works, last_error=last_error())
|
|
215
230
|
except Exception as e: # noqa: BLE001 — the report must not take doctor down
|
|
216
231
|
print(f"\nCould not build the report ({type(e).__name__}); nothing was posted.")
|
|
217
232
|
return
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""voiceio's most recent error, in a form safe to post, and a nudge to report
|
|
2
|
+
errors the service hits on its own.
|
|
3
|
+
|
|
4
|
+
Logs and crash tracebacks can quote dictated text (an exception message, an
|
|
5
|
+
argv), so nothing here ever copies a message. A report gets the exception
|
|
6
|
+
type, the innermost voiceio `file:line` and how long ago; a notification
|
|
7
|
+
says only that something failed.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime as dt
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
MAX_AGE = dt.timedelta(days=7)
|
|
20
|
+
# The same error this many times within the window is worth one notification.
|
|
21
|
+
REPEATS = 3
|
|
22
|
+
REPEAT_WINDOW_SECS = 600
|
|
23
|
+
|
|
24
|
+
_LOG_LINE = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d),\d+ (\S+) (\w+) ")
|
|
25
|
+
_CRASH_HEADER = re.compile(r"^===== (\S+) =====$")
|
|
26
|
+
_FRAME = re.compile(r'^\s+File "([^"]+)", line (\d+)')
|
|
27
|
+
# The final line of a traceback: a dotted exception name, alone or before ":".
|
|
28
|
+
_EXC_LINE = re.compile(r"^([A-Za-z_][\w.]{0,120})(?::|$)")
|
|
29
|
+
_NAME = re.compile(r"^[A-Za-z_][\w.]{0,120}$")
|
|
30
|
+
_PKG = Path(__file__).resolve().parent
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def last_error(log_path: Path | None = None, crash_path: Path | None = None,
|
|
34
|
+
now: dt.datetime | None = None) -> str:
|
|
35
|
+
"""'DecodeError at voiceio/streaming.py:488 (2 h ago)' for the newest
|
|
36
|
+
error of the last week in the service log or crash.log; '' if none."""
|
|
37
|
+
from voiceio.config import LOG_DIR, LOG_PATH
|
|
38
|
+
now = now or dt.datetime.now()
|
|
39
|
+
found = [e for path, parse in ((log_path or LOG_PATH, _log_errors),
|
|
40
|
+
(crash_path or LOG_DIR / "crash.log", _crash_errors))
|
|
41
|
+
for e in parse(_read(path))]
|
|
42
|
+
recent = [e for e in found if now - e[0] <= MAX_AGE]
|
|
43
|
+
if not recent:
|
|
44
|
+
return ""
|
|
45
|
+
when, exc, where = max(recent, key=lambda e: e[0])
|
|
46
|
+
what = f"{exc} at {where}" if exc and where else exc or f"logged by {where}"
|
|
47
|
+
return f"{what} ({_age(now - when)})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read(path: Path) -> list[str]:
|
|
51
|
+
try:
|
|
52
|
+
return path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
53
|
+
except OSError:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _log_errors(lines: list[str]):
|
|
58
|
+
"""(time, exception type, location) per ERROR/CRITICAL in the service log;
|
|
59
|
+
without a traceback the location is the logger's name."""
|
|
60
|
+
for i, line in enumerate(lines):
|
|
61
|
+
m = _LOG_LINE.match(line)
|
|
62
|
+
if not m or m.group(3) not in ("ERROR", "CRITICAL"):
|
|
63
|
+
continue
|
|
64
|
+
tail = []
|
|
65
|
+
for nxt in lines[i + 1:]:
|
|
66
|
+
if _LOG_LINE.match(nxt):
|
|
67
|
+
break
|
|
68
|
+
tail.append(nxt)
|
|
69
|
+
exc, where = _traceback(tail)
|
|
70
|
+
logger = m.group(2) if _NAME.match(m.group(2)) else ""
|
|
71
|
+
yield _time(m.group(1)), exc, where or logger
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _crash_errors(lines: list[str]):
|
|
75
|
+
starts = [(i, m.group(1)) for i, line in enumerate(lines)
|
|
76
|
+
if (m := _CRASH_HEADER.match(line))]
|
|
77
|
+
for n, (i, stamp) in enumerate(starts):
|
|
78
|
+
end = starts[n + 1][0] if n + 1 < len(starts) else len(lines)
|
|
79
|
+
when = _time(stamp)
|
|
80
|
+
if when is not None:
|
|
81
|
+
yield (when, *_traceback(lines[i + 1:end]))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _traceback(lines: list[str]) -> tuple[str, str]:
|
|
85
|
+
"""The last exception's type and its innermost voiceio frame."""
|
|
86
|
+
exc, where, after_frames = "", "", False
|
|
87
|
+
for line in lines:
|
|
88
|
+
if f := _FRAME.match(line):
|
|
89
|
+
after_frames = True
|
|
90
|
+
if rel := _voiceio_path(f.group(1)):
|
|
91
|
+
where = f"{rel}:{f.group(2)}"
|
|
92
|
+
elif line.startswith("Traceback ("):
|
|
93
|
+
exc, where = "", "" # a chained traceback: the last one wins
|
|
94
|
+
elif after_frames and not line.startswith((" ", "\t")):
|
|
95
|
+
# Only the line right after the frames names the exception; the
|
|
96
|
+
# message's own later lines could be anything, dictation included.
|
|
97
|
+
after_frames = False
|
|
98
|
+
if m := _EXC_LINE.match(line):
|
|
99
|
+
exc = m.group(1).rsplit(".", 1)[-1]
|
|
100
|
+
return exc, where
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _voiceio_path(path: str) -> str:
|
|
104
|
+
"""'voiceio/app.py' for a file of this package; '' for anything else."""
|
|
105
|
+
try:
|
|
106
|
+
return "voiceio/" + Path(path).resolve().relative_to(_PKG).as_posix()
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
pass
|
|
109
|
+
if "site-packages/" in path: # another install of voiceio, or a library
|
|
110
|
+
rest = path.rsplit("site-packages/", 1)[1]
|
|
111
|
+
return rest if rest.startswith("voiceio/") else ""
|
|
112
|
+
return "voiceio/" + path.rsplit("/voiceio/", 1)[1] if "/voiceio/" in path else ""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _time(stamp: str) -> dt.datetime | None:
|
|
116
|
+
try:
|
|
117
|
+
when = dt.datetime.fromisoformat(stamp)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return None
|
|
120
|
+
return when.replace(tzinfo=None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _age(delta: dt.timedelta) -> str:
|
|
124
|
+
secs = max(0, int(delta.total_seconds()))
|
|
125
|
+
if secs < 3600:
|
|
126
|
+
return f"{max(1, secs // 60)} min ago"
|
|
127
|
+
if secs < 86400:
|
|
128
|
+
return f"{secs // 3600} h ago"
|
|
129
|
+
return f"{secs // 86400} d ago"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RepeatedErrorNotifier(logging.Handler):
|
|
133
|
+
"""The service has no terminal to tell anyone, so when it logs the same
|
|
134
|
+
error REPEATS times in REPEAT_WINDOW_SECS, one desktop notification a day
|
|
135
|
+
suggests a report. Never posts anything itself."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, state_path: Path | None = None, notify=None):
|
|
138
|
+
super().__init__(level=logging.ERROR)
|
|
139
|
+
from voiceio.config import LOG_DIR
|
|
140
|
+
self._path = state_path or LOG_DIR / "error-notify.json"
|
|
141
|
+
self._notify = notify
|
|
142
|
+
self._lock = threading.Lock()
|
|
143
|
+
|
|
144
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
145
|
+
# The format string, not the message: same error, different details.
|
|
146
|
+
key = f"{record.name}:{record.msg}"[:200]
|
|
147
|
+
now = time.time()
|
|
148
|
+
today = dt.date.today().isoformat()
|
|
149
|
+
try:
|
|
150
|
+
with self._lock:
|
|
151
|
+
state = self._load()
|
|
152
|
+
seen = {k: [t for t in ts if now - t < REPEAT_WINDOW_SECS]
|
|
153
|
+
for k, ts in state["recent"].items() if isinstance(ts, list)}
|
|
154
|
+
seen[key] = [*seen.get(key, []), now][-REPEATS:]
|
|
155
|
+
state["recent"] = {k: ts for k, ts in seen.items() if ts}
|
|
156
|
+
fire = len(seen[key]) >= REPEATS and state.get("notified") != today
|
|
157
|
+
if fire:
|
|
158
|
+
state["notified"] = today
|
|
159
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
self._path.write_text(json.dumps(state))
|
|
161
|
+
if fire:
|
|
162
|
+
notify = self._notify
|
|
163
|
+
if notify is None:
|
|
164
|
+
from voiceio.feedback import notify
|
|
165
|
+
notify("voiceio hit an error",
|
|
166
|
+
"Run 'voiceio doctor --report' to tell us.")
|
|
167
|
+
except Exception: # noqa: BLE001 — logging must never fail the daemon
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def _load(self) -> dict:
|
|
171
|
+
try:
|
|
172
|
+
state = json.loads(self._path.read_text())
|
|
173
|
+
if isinstance(state, dict) and isinstance(state.get("recent"), dict):
|
|
174
|
+
return state
|
|
175
|
+
except (OSError, ValueError):
|
|
176
|
+
pass
|
|
177
|
+
return {"recent": {}}
|
|
@@ -133,6 +133,7 @@ def build_report(
|
|
|
133
133
|
perm_issues: Sequence[Path] = (),
|
|
134
134
|
comment: str = "",
|
|
135
135
|
works: str = "",
|
|
136
|
+
last_error: str = "",
|
|
136
137
|
cfg=None,
|
|
137
138
|
) -> str:
|
|
138
139
|
"""The exact public text: short, plain, and scrubbed."""
|
|
@@ -197,6 +198,8 @@ def build_report(
|
|
|
197
198
|
lines.append("failed: " + "; ".join(failed) if failed else "failed: none")
|
|
198
199
|
if warned:
|
|
199
200
|
lines.append("warnings: " + "; ".join(warned))
|
|
201
|
+
if last_error:
|
|
202
|
+
lines.append(f"last error: {_short(last_error)}")
|
|
200
203
|
comment = "".join(c if c.isprintable() else " " for c in _ESCAPE.sub("", comment))
|
|
201
204
|
if works in WORKS:
|
|
202
205
|
lines.append(f"works: {works}")
|
|
@@ -305,6 +308,7 @@ def gather(comment: str = "") -> tuple[HealthReport, str]:
|
|
|
305
308
|
"""Run the doctor checks and build the report from them."""
|
|
306
309
|
from voiceio import config
|
|
307
310
|
from voiceio import platform as plat
|
|
311
|
+
from voiceio.errorlog import last_error
|
|
308
312
|
from voiceio.health import check_health
|
|
309
313
|
health = check_health()
|
|
310
314
|
return health, build_report(
|
|
@@ -312,6 +316,7 @@ def gather(comment: str = "") -> tuple[HealthReport, str]:
|
|
|
312
316
|
missing_deps=plat.check_system_deps(),
|
|
313
317
|
perm_issues=[p for p, _, _ in config.check_permissions()],
|
|
314
318
|
comment=comment,
|
|
319
|
+
last_error=last_error(),
|
|
315
320
|
)
|
|
316
321
|
|
|
317
322
|
|
|
@@ -86,6 +86,10 @@ def main() -> None:
|
|
|
86
86
|
doctor_item.connect("activate", _emit("menu:doctor"))
|
|
87
87
|
menu.append(doctor_item)
|
|
88
88
|
|
|
89
|
+
report_item = Gtk.MenuItem(label="Report a problem...")
|
|
90
|
+
report_item.connect("activate", _emit("menu:report"))
|
|
91
|
+
menu.append(report_item)
|
|
92
|
+
|
|
89
93
|
logs_item = Gtk.MenuItem(label="View logs...")
|
|
90
94
|
logs_item.connect("activate", _emit("menu:logs"))
|
|
91
95
|
menu.append(logs_item)
|
|
@@ -61,6 +61,7 @@ def start(
|
|
|
61
61
|
pystray.Menu.SEPARATOR,
|
|
62
62
|
pystray.MenuItem("Demo...", _open_terminal("demo")),
|
|
63
63
|
pystray.MenuItem("Doctor...", _open_terminal("doctor")),
|
|
64
|
+
pystray.MenuItem("Report a problem...", _open_terminal("report")),
|
|
64
65
|
pystray.MenuItem("View logs...", _open_terminal("logs")),
|
|
65
66
|
pystray.Menu.SEPARATOR,
|
|
66
67
|
pystray.MenuItem("Quit", lambda: quit_callback()),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.16.1"
|
|
File without changes
|
{python_voiceio-1.16.1 → python_voiceio-1.17.0}/python_voiceio.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|